Compare commits

...

468 Commits

Author SHA1 Message Date
Ly-sec
a0a3a58668 Release v2.9.0
- **Floating Mode**: Added floating option for more flexible bar positioning
- **Vertical Orientation**: New vertical bar layout support

- **Exclusive Mode**: Added exclusive setting to prevent windows from rendering behind the dock
- **Floating Distance Control**: Added control for adjusting floating distance
- **Layout Refinements**: Various layout fixes for better visual consistency

- **Enhanced Navigation**: More panels now support closing with the Escape key
- **Settings Overhaul**: Complete revamp of the settings window tab content

- **Layout Editor**: Added ability to edit keyboard layouts directly

- **GPU Temperature**: Removed GPU temperature monitoring (resolved NVIDIA compatibility issues)

- **Compact Mode**: New compact version for space-constrained layouts

- **Hyprland Stability**: Added numerous null checks for improved Hyprland compatibility
- **Niri Support**: Fixed active window detection for the Niri compositor
- **Workspace Visibility**: Added toggle to hide unoccupied workspaces

- **Monochrome Theme**: Added new monochrome color scheme option

- **Bluetooth Stability**: More stable connections and adapter state management
- **Toast Notifications**: Fixed odd toast notification behavior
- **Font Service**: Improved font service reliability and added fuzzy search for the font selection in General Tab
2025-09-14 21:51:58 +02:00
Ly-sec
b3abe44d65 Bar: remove Qt5Compat import 2025-09-14 21:37:56 +02:00
LemmyCook
5b603472bd Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-14 15:25:59 -04:00
LemmyCook
57b0fe8a21 Wi-Fi: connect and disconnect toast messages 2025-09-14 15:25:57 -04:00
Ly-sec
f5561da3cc Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-14 21:25:14 +02:00
Ly-sec
11f6475b9f Some layout fixes to toggle and slider
NSlider(withLabel): fix some small layout issues
NToggle: fix vertical centering of the thumb
2025-09-14 21:24:11 +02:00
LemmyCook
fb2c5e0470 SysMon: removed unecessary Item {} 2025-09-14 15:14:45 -04:00
LemmyCook
b1764fddc8 SysMon: larger margin 2025-09-14 15:12:08 -04:00
Ly-sec
bb7f957e44 Clock: change to mono font 2025-09-14 21:10:16 +02:00
LemmyCook
0682315c9d Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-14 15:07:03 -04:00
LemmyCook
dd100597ed SysMon: better lookin 2025-09-14 15:05:34 -04:00
Ly-sec
6bc6380ee1 FontService: even more mono font fixes 2025-09-14 21:04:46 +02:00
Ly-sec
966089e471 FontService: more mono font fixes 2025-09-14 20:47:36 +02:00
Ly-sec
3956461254 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-14 20:38:32 +02:00
Ly-sec
02d114a05e FontService: more reliable mono lookup 2025-09-14 20:36:52 +02:00
LemmyCook
3764edafa8 Widgets: improved centering 2025-09-14 14:35:15 -04:00
Ly-sec
1cd0376381 Style: reduce vertical bar to 39 2025-09-14 20:30:26 +02:00
LemmyCook
2b154e2cdb Settings: added missing end divider to tabs 2025-09-14 13:58:00 -04:00
LemmyCook
933dfc402b Wifi+BT: added right click 2025-09-14 13:48:12 -04:00
LemmyCook
34d037d7dc Toast: improved clickability around toast 2025-09-14 13:37:15 -04:00
LemmyCook
76b6626073 Trying to match all buttons left/right click.
- left click: mute/unmute, cycle between functionality
- right click: open settings
- middle click: open external settings
2025-09-14 13:35:24 -04:00
LemmyCook
d348cfc2b0 Toast: refactored service vs UI. 2025-09-14 13:29:20 -04:00
LemmyCook
f9d7de2e3c Volume: Fixed missing externalHideTimer 2025-09-14 12:32:05 -04:00
LemmyCook
2ea00fffa5 NSlider: simplification, no Halo + some rounding 2025-09-14 12:22:26 -04:00
LemmyCook
b163dab241 NCheckbox+NToggle: better look 2025-09-14 12:03:15 -04:00
LemmyCook
af0f4818d8 Autoformatting 2025-09-14 11:51:04 -04:00
LemmyCook
8b6c7632af Tray: fixed with vertical bar 2025-09-14 11:50:18 -04:00
LemmyCook
d6d51d24c9 NPill: fix NPill icon color to match or icons (mOnSurface, even tho the bg is mSurfaceVariant) 2025-09-14 11:33:48 -04:00
LemmyCook
0c6aea7154 Vertical Bar! 2025-09-14 11:26:36 -04:00
LemmyCook
a61b2edd07 Settings: fully cleanup and aligned 2025-09-14 11:23:20 -04:00
LemmyCook
c108e7707a Settings: cleanup, almost there! 2025-09-14 11:14:37 -04:00
LemmyCook
f3123ba5b1 Settings: more cleanup - wip 2025-09-14 10:57:24 -04:00
LemmyCook
c09a93af48 NHeader: use label instead of title (matches NLabel) 2025-09-14 10:24:09 -04:00
LemmyCook
2a262999ce Merge branch 'vertical-bar' of github.com:noctalia-dev/noctalia-shell into vertical-bar 2025-09-14 10:17:30 -04:00
LemmyCook
7d952dc226 Settings: new display tab 2025-09-14 10:17:28 -04:00
Ly-sec
3cb838b455 NCheckbox: edit sizing
NToggle: edit sizing, fix thumb vertical center
2025-09-14 16:01:00 +02:00
Ly-sec
7594651e05 SettingsTabs: use NHeader, move display settings around 2025-09-14 15:50:23 +02:00
Ly-sec
0d611fc891 Merge branch 'vertical-bar' of https://github.com/noctalia-dev/noctalia-shell into vertical-bar 2025-09-14 13:46:05 +02:00
Ly-sec
8982909fae Edit Style.qml so barHeight check for vertical bar
SystemMonitor.qml: edit layout a little bit
2025-09-14 13:45:17 +02:00
ItsLemmy
132b331c7c Dock: fix floating distance when bar is at the bottom 2025-09-14 07:42:10 -04:00
ItsLemmy
85cef214c8 Merge branch 'vertical-bar' of github.com:Ly-sec/Noctalia into vertical-bar 2025-09-14 07:27:16 -04:00
ItsLemmy
80b4dad199 NPill: using monospace font 2025-09-14 07:27:14 -04:00
Ly-sec
aadbc9596d NSearchableComboBox: small layout change 2025-09-14 13:25:02 +02:00
Ly-sec
0949d154c1 Merge branch 'vertical-bar' of https://github.com/noctalia-dev/noctalia-shell into vertical-bar 2025-09-14 13:22:49 +02:00
Ly-sec
a86a0d33c1 NSearchableComboBox: created, uses fuzzy find
GeneralTab: replace NComboBox with NSearchableComboBox
2025-09-14 13:22:17 +02:00
ItsLemmy
e6372a2473 VerticalBar: smaller spacing and margin 2025-09-14 07:21:49 -04:00
ItsLemmy
e3d9ab5679 NPill better naming so files stay closeby 2025-09-14 07:21:32 -04:00
Ly-sec
ccd7458ea3 KeyboardLayout: fix language detection/parsing
Bar: add a tiny bit more spacing between widgets
NHorizontalPill: fix layout
MediaMini: set size to 0 if no media is playing
2025-09-14 10:21:53 +02:00
Ly-sec
d41b59d563 KeyboardLayout: fix ukranian iso code 2025-09-14 09:31:07 +02:00
Ly-sec
290ba4ac03 Fix N*Pill expanded text layout 2025-09-14 09:26:05 +02:00
Ly-sec
1ee14df915 Make things more readable 2025-09-14 09:05:51 +02:00
LemmyCook
76376a9783 Dock: do not show dock if no app/toplevel available 2025-09-13 22:13:02 -04:00
LemmyCook
880ac93662 autoformatting 2025-09-13 22:12:44 -04:00
LemmyCook
1157c8e21d FloatingBar: Wip 2025-09-13 22:04:36 -04:00
LemmyCook
2082cfe7c7 Merge branch 'main' into vertical-bar 2025-09-13 15:27:55 -04:00
LemmyCook
9a9f2886e0 Floating Bar: Fix for #265 (overlapping panels, toasts and notifications) 2025-09-13 15:23:27 -04:00
Ly-sec
0035fbcc4e NPill: act as loder for NVerticalPill and NHorizontalPill
NHorizontalPill: should be used for anything that expands horizontal
NVerticalPill: should be used for anything that expands vertical
2025-09-13 20:52:20 +02:00
Lysec
46103062d0 Merge pull request #266 from povvke/fix-app2unit-steam-games
Fix steam games not launching with app2unit
2025-09-13 20:02:10 +02:00
povvke
78a41c236c use the exec string itself to launch non terminal apps 2025-09-13 20:48:34 +03:00
Ly-sec
9dfac69e9e More spacing fixes 2025-09-13 19:28:44 +02:00
LemmyCook
de72236fe5 Merge branch 'main' into vertical-bar 2025-09-13 13:06:21 -04:00
LemmyCook
101e3125a9 Vertical bar: simpler management 2025-09-13 13:06:17 -04:00
Ly-sec
b443c9f492 Add compact clock again 2025-09-13 17:46:38 +02:00
Ly-sec
2a1e7832d6 Revert 8c81514 2025-09-13 17:44:31 +02:00
Ly-sec
8c815146e6 More fixes 2025-09-13 17:34:13 +02:00
LemmyCook
acae2b8c21 Dock: border alpha follows bg opacity 2025-09-13 11:09:55 -04:00
Ly-sec
004836fc8f More layout fixes 2025-09-13 17:00:49 +02:00
Ly-sec
b51f2d16cb Change Notification location 2025-09-13 16:47:32 +02:00
Ly-sec
6fba9d9f22 NPanel positioning fixes 2025-09-13 16:45:22 +02:00
LemmyCook
335e38d461 Floating Bar: simplified settings 2025-09-13 10:16:54 -04:00
Ly-sec
ee50d84a53 Fix spacing for vertical bar 2025-09-13 15:51:21 +02:00
Ly-sec
e706dabef3 Add BarService, use signals to check state of bar and update widgets accordingly 2025-09-13 15:31:23 +02:00
Ly-sec
dcedae46e5 Horizontal bar: try to get better spacing 2025-09-13 15:21:36 +02:00
LemmyCook
f27f9d35b0 Merge branch 'hyprland-smarter-detect' 2025-09-13 09:10:50 -04:00
Ly-sec
4f5acb7114 First iteration of vertical bar 2025-09-13 14:26:20 +02:00
Lysec
25ba27cbdd Merge pull request #262 from Mtendekuyokwa19/nixpkg
invalid nixpkg change
2025-09-13 13:14:26 +02:00
Mtende Kuyokwa
74da975ed4 invalid nixpkg change 2025-09-13 13:04:54 +02:00
Ly-sec
6f6a5b364a Bar: proper top/bottom margin check 2025-09-13 10:55:14 +02:00
Ly-sec
f670f88804 NPanel: add margin if bar is floating (except for SettingsPanel) 2025-09-13 10:25:50 +02:00
Ly-sec
814cb774a6 Notification: added margin if bar is floating 2025-09-13 10:13:51 +02:00
Ly-sec
50d8b54adf Bar: add floating setting 2025-09-13 10:11:57 +02:00
LemmyCook
ae931b791f Icons: replaced most left over filled icons by the outlined counterparts.
- only kept a few filled for basic controls (carets, play, pause,
etc...)
2025-09-12 23:20:33 -04:00
LemmyCook
dd4641eedd CompositorService: improved Hyprland detection so there is no warning on Niri. 2025-09-12 22:55:13 -04:00
LemmyCook
b66bb46fc1 We don't use qmlformat, we do use qmlfmt 2025-09-12 21:18:24 -04:00
LemmyCook
5079fc78d3 Removed ArchUpdateService 2025-09-12 21:16:50 -04:00
LemmyCook
7d2eaa46e6 qmlfmt: increase line-length to 360 to avoid hard-wrap.
+ cleaned up power menu/panel
2025-09-12 21:07:11 -04:00
LemmyCook
1043eaa39f Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-12 20:45:08 -04:00
LemmyCook
f16798f6e3 LockScreen: shorter tooltips 2025-09-12 20:45:06 -04:00
Lemmy
96acb1a679 Update README.md 2025-09-12 20:42:37 -04:00
LemmyCook
0f93797ab3 LockScreen: tooltip uniformisation 2025-09-12 18:22:12 -04:00
LemmyCook
3186a84d6b Settings: using "cloud-sun" for weather tab 2025-09-12 18:18:12 -04:00
Lemmy
d3ee66d845 Merge pull request #261 from MrDowntempo/monochrome
Updated Monochrome to not be more grayscale and pretty
2025-09-12 18:05:10 -04:00
Corey Woodworth
a8837283ab Updated Monochrome to not be more grayscale and pretty 2025-09-12 16:59:08 -04:00
LemmyCook
6fe0784c00 Autoformatting 2025-09-12 16:45:35 -04:00
Lemmy
59ce164b40 Merge pull request #257 from mkuritsu/main
Add toggle to hide unoccupied workspaces
2025-09-12 16:42:36 -04:00
mkuritsu
70144eb06f Fixed redundant comparison 2025-09-12 21:33:12 +01:00
Ly-sec
5136af5d95 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-12 22:20:48 +02:00
Ly-sec
ff42244c6d Revert hardcoded font change 2025-09-12 22:20:46 +02:00
mkuritsu
3a2bb40117 Add toggle to hide unoccupied workspaces 2025-09-12 21:16:44 +01:00
LemmyCook
bcd3100849 Merge branch 'dock-better-peeking' 2025-09-12 16:10:45 -04:00
LemmyCook
c8886629ad Dock: Float improvements, can click below dock and on the side. Should fix #237 2025-09-12 16:10:03 -04:00
Ly-sec
be4a69f6e0 Replace hardcoded font with check for default fonts, fall back to
inter/roboto
Settings: use font detection function
GeneralTab: let user know that it uses default fonts and falls back to
inter/roboto
FontService: add proper checks for default fonts (sans & mono)
2025-09-12 22:00:05 +02:00
LemmyCook
62b12d5436 Bar ethernet icon: unfilled 2025-09-12 14:50:09 -04:00
LemmyCook
99e75d51b8 Bar settings icon: unfilled 2025-09-12 14:48:13 -04:00
Lemmy
079c8f0803 Merge pull request #259 from ThatOneCalculator/settings-icons
fix: consistent settings icons
2025-09-12 14:46:26 -04:00
Kainoa Kanter
16f87cbfa3 fix: consistent settings icons 2025-09-12 11:22:41 -07:00
LemmyCook
380f31fbd9 BaBar Widgets: pass a proper section name instead of a longer string. 2025-09-12 12:54:09 -04:00
LemmyCook
28677d6888 Panels: added kb focus to BTPanel, NotifHistory, SidePanel, so they close with ESC. 2025-09-12 11:29:46 -04:00
Lemmy
07e94b0f0e Update feature_request.md 2025-09-12 10:58:27 -04:00
Lemmy
307318918d Update bug_report.md 2025-09-12 10:58:11 -04:00
Lemmy
3f6662182e Merge pull request #258 from matejc/feat/notifications-close-on-clear
feat(Modules/Notification): auto-close history panel on clear history
2025-09-12 10:52:11 -04:00
Matej Cotman
be532fa146 feat(Modules/Notification): auto-close history panel on clear history 2025-09-12 17:44:14 +03:00
mkuritsu
722a59da80 add qmlformat simple file, add .gitignore with .qmlls.ini 2025-09-12 11:48:56 +01:00
Lemmy
3c97acf00f Merge pull request #255 from MrDowntempo/monochrome
Add Monochrome (Black & White) color scheme
2025-09-12 00:03:47 -04:00
Corey Woodworth
2d4fa59c41 Add Monochrome (Black & White) color scheme 2025-09-11 23:31:52 -04:00
LemmyCook
c5ca758d3e Settings: New Dock tab. 2025-09-11 23:31:40 -04:00
LemmyCook
6f70a98b83 ColorSchemeTab: fixed currently selected scheme to match wallpaper. 2025-09-11 23:18:13 -04:00
LemmyCook
424594a11a Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-11 22:46:21 -04:00
LemmyCook
f5ac42c692 Dock: Slightly more compact 2025-09-11 22:46:19 -04:00
Lemmy
675f96d0e6 Merge pull request #254 from SeraphimRP/patch-1
Fix a missing semicolon in Nix instructions.
2025-09-11 21:48:23 -04:00
Rdr. Seraphim Pardee
9570688294 Fix a missing semicolon in Nix instructions. 2025-09-11 20:45:48 -04:00
LemmyCook
626b745ce3 Settings: Added a ScreenCorners section in the general tab. 2025-09-11 19:22:07 -04:00
Lemmy
4afb98cf4c Merge pull request #244 from juvevood/screen-corners-radius
Screen Corners use general radius ratio of settings
2025-09-11 19:17:42 -04:00
LemmyCook
df2a9a246d Dock: New "exclusive" settings to ensure no windows go below. 2025-09-11 19:14:19 -04:00
Lemmy
d80e9ba3d5 Merge pull request #252 from BinaryQuantumSoul/patch-1
Update nix readme instructions
2025-09-11 19:01:49 -04:00
Lemmy
130c68b3e2 Merge pull request #250 from matejc/fix/bluetooth-switch
fix(bluetooth): rename wifiSwitch to bluetoothSwitch
2025-09-11 18:59:33 -04:00
Lemmy
6eea4a17a4 Merge pull request #253 from SailorSnoW/fix/screen-recorder-aarch
import gpu-screen-recorder only on x86_64 in nix flake
2025-09-11 18:56:29 -04:00
SailorSnoW
40dc8633ec import gpu-screen-recorder only on x86_64 2025-09-12 00:38:38 +02:00
QuantumSoul
12ac91d125 Update README.md 2025-09-12 00:31:41 +02:00
LemmyCook
87d86911d7 BarSectionEdit: fix click in the background closing panel, fix ghost bg color when dragging 2025-09-11 18:06:12 -04:00
LemmyCook
2872a7b5c9 Using NScrollView and NListView where it matters.
Not using them in tiny ListViews (ex: NComboBox, and Media player
dropdown)
2025-09-11 17:58:28 -04:00
LemmyCook
4067896434 New components: NScrollView + NListView
Allow controlling the handle color and stuf...
2025-09-11 17:56:47 -04:00
LemmyCook
78443451e4 Bar Widgets: Hover color switched from mPrimary to mTertiary for consistency 2025-09-11 17:30:52 -04:00
LemmyCook
719f5a20e7 Bar widget editor: better colors + autoformatting 2025-09-11 16:45:33 -04:00
LemmyCook
d8b12e6d6b Rosepine: revamped light theme by following RosePine Dawn 2025-09-11 16:29:11 -04:00
LemmyCook
9a0746d737 PowerToggle: was not receiving scaling which led to a broken bar. 2025-09-11 15:56:10 -04:00
LemmyCook
77f8b3937c RosePine: improve dark theme 2025-09-11 15:13:05 -04:00
Matej Cotman
3f4313635a fix(bluetooth): rename wifiSwitch to bluetoothSwitch to fix the toggle switch 2025-09-11 21:23:22 +03:00
LemmyCook
004d92a85d SidePanelToggle: use Noctalia logo by default 2025-09-11 13:34:04 -04:00
LemmyCook
720c17258b Weather: use the regular "sun" icon (unfilled) for better uniformity 2025-09-11 11:52:33 -04:00
LemmyCook
a8b312f3a7 SidePanel: even more robust with sizing forced everywhere 2025-09-11 11:47:09 -04:00
LemmyCook
4d6361dfe5 Updated font 2025-09-11 11:46:50 -04:00
LemmyCook
1f75819795 Tabler icons: commented out all broken icons (due to Qt's font rendering) 2025-09-11 11:26:50 -04:00
LemmyCook
50ddd2916c autoformatting 2025-09-11 11:26:29 -04:00
Ly-sec
d30e14f611 CompositorService: add tons of null checks to perhaps prevent QS crashes
(and add some logging)
ActiveWindow: added debounce for icons
KeyboardLayoutService: remove console logs
2025-09-11 17:18:25 +02:00
LemmyCook
227b0dd962 removed extra logs 2025-09-11 09:46:45 -04:00
LemmyCook
ac61086c95 Autoformatting 2025-09-11 09:45:26 -04:00
LemmyCook
0980f65751 Cloud-sun icon 2025-09-11 09:45:21 -04:00
LemmyCook
7aa3da2ff4 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-11 09:45:05 -04:00
LemmyCook
83fbb8f95d Clock: factorized many settings in a single combobox 2025-09-11 09:43:52 -04:00
Ly-sec
a029463527 CompositorService: use idx for niri workspaces 2025-09-11 14:31:36 +02:00
Ly-sec
baafe54d13 Clock: small changes to compact mode 2025-09-11 13:53:45 +02:00
Ly-sec
a1cbd35202 Clock: add compact mode with nnumeric/verbose date options 2025-09-11 13:03:39 +02:00
LemmyCook
1337a35a1e Removed video 2025-09-10 22:37:28 -04:00
Lysec
61006fbed0 Update README.md 2025-09-11 04:21:55 +02:00
Ly-sec
eff4337d35 README: more updates 2025-09-11 04:18:33 +02:00
Juve
f0733f19dd add a separate configuration item for edge of screen 2025-09-11 10:11:01 +08:00
Ly-sec
818df48787 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-11 04:01:02 +02:00
Ly-sec
0eedfba071 README: update preview 2025-09-11 04:00:49 +02:00
LemmyCook
2dc9e2f212 Bluetooth: proper synchronisation of the adapter state with the cached setting 2025-09-10 21:29:11 -04:00
LemmyCook
62a3b343cf Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-10 21:14:04 -04:00
LemmyCook
76be93a84d NPanel: fix 3 minor warnings 2025-09-10 21:14:01 -04:00
Ly-sec
b59c56170e Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-11 03:02:44 +02:00
Ly-sec
c9285d8c5b SystemMonitor: remove GPU temp 2025-09-11 03:02:36 +02:00
LemmyCook
b157d855a8 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-10 20:45:56 -04:00
LemmyCook
82ac49ce85 NPanel: simplified screen/scaling management 2025-09-10 20:45:50 -04:00
Ly-sec
7247a26586 KeyboardLayout: add tons of layouts, add Commons/KeyboardLayout.qml for ease of adding new ones 2025-09-11 01:40:25 +02:00
Ly-sec
be0b568f1f KeyboardLayout: increase font size and make it all caps 2025-09-11 01:10:04 +02:00
Lysec
e4b54e518c Merge pull request #243 from juvevood/fix-powerpanel-shortcut
fix for PowerPanel Shortcut invalid
2025-09-11 00:53:50 +02:00
Lysec
6ea1e2b4c7 Update README.md 2025-09-11 00:15:52 +02:00
Ly-sec
5b4c57eae2 Set version to dev 2025-09-10 23:28:23 +02:00
Ly-sec
6e5efc3244 Release v2.8.0
We've been busy squashing bugs and adding some nice improvements based on your feedback.
What's New
New Icon Set - Swapped out Material Symbols for Tabler icons. They look great and load faster since they're built right in.
Works on Any Linux Distro - Dropped the Arch-specific update checker so this works properly on whatever distro you're running. You can build your own update notifications with Custom Buttons if you want.
Icon Picker - Added a proper icon picker for custom button widgets. No more guessing icon names.
Smarter Audio Visualizer - The Cava visualizer actually pays attention now - it only kicks in when you're playing music or videos instead of running all the time.
Better Notifications - Notifications now show actual app names like "Firefox" instead of cryptic IDs like "org.mozilla.firefox".
Less Noise - Turned a bunch of those persistent notification popups into toast notifications so they don't stick around cluttering your screen.
Fixes

Active Window widget finally shows the right app icon and title consistently
Fixed a nasty crash on Hyprland
Screen recorder button disables itself if the recording software isn't installed
Added a force-enable option for Night Light so you can turn it on manually whenever
2025-09-10 23:19:22 +02:00
Ly-sec
271a887bbf Release Notes
We've been busy squashing bugs and adding some nice improvements based on your feedback.
What's New
New Icon Set - Swapped out Material Symbols for Tabler icons. They look great and load faster since they're built right in.
Works on Any Linux Distro - Dropped the Arch-specific update checker so this works properly on whatever distro you're running. You can build your own update notifications with Custom Buttons if you want.
Icon Picker - Added a proper icon picker for custom button widgets. No more guessing icon names.
Smarter Audio Visualizer - The Cava visualizer actually pays attention now - it only kicks in when you're playing music or videos instead of running all the time.
Better Notifications - Notifications now show actual app names like "Firefox" instead of cryptic IDs like "org.mozilla.firefox".
Less Noise - Turned a bunch of those persistent notification popups into toast notifications so they don't stick around cluttering your screen.
Fixes

Active Window widget finally shows the right app icon and title consistently
Fixed a nasty crash on Hyprland
Screen recorder button disables itself if the recording software isn't installed
Added a force-enable option for Night Light so you can turn it on manually whenever
2025-09-10 23:14:58 +02:00
Ly-sec
0571ba7325 test commit 2025-09-10 23:14:39 +02:00
Ly-sec
c2f6c39016 Revert "Release v2.8.0"
This reverts commit 2de2908509.
2025-09-10 23:13:02 +02:00
Ly-sec
2de2908509 Release v2.8.0
We've been busy squashing bugs and adding some nice improvements based on your feedback.
What's New
New Icon Set - Swapped out Material Symbols for Tabler icons. They look great and load faster since they're built right in.
Updater Widget - Dropped the Arch-specific update checker so this works properly on whatever distro you're running. You can build your own update widget with Custom Buttons if you want.
Icon Picker - Added a proper icon picker for custom button widgets. No more guessing icon names.
Better Notifications - Notifications now show actual app names like "Firefox" instead of cryptic IDs like "org.mozilla.firefox".
Less Noise - Turned a bunch of those persistent notification popups into toast notifications so they don't stick around cluttering your screen.
Fixes

Active Window widget finally shows the right app icon and title consistently
Fixed a nasty crash on Hyprland
Screen recorder button disables itself if the recording software isn't installed
Added a force-enable option for Night Light so you can turn it on manually whenever

That's what claude had  to offer😄
2025-09-10 23:07:54 +02:00
LemmyCook
99d56687ef SysStat: house keeping (keep cpu stuff grouped) 2025-09-10 14:34:27 -04:00
LemmyCook
434b8273f0 SystemStats: better gpu logging 2025-09-10 14:31:05 -04:00
LemmyCook
663382c81c Icons: "trash" instead of "trash-filled" 2025-09-10 10:56:31 -04:00
LemmyCook
3f388bdb4b Widgets Drag&Drop: fix for panel closing when clicking rapidly in the background of a widget. 2025-09-10 09:33:08 -04:00
LemmyCook
0a4317f712 More drag and drop fixes 2025-09-10 09:11:13 -04:00
LemmyCook
b9dbbf7bdd Widgets Drag&Drop: drop indicator and improved behavior 2025-09-10 09:02:09 -04:00
LemmyCook
6ed9a8c5ae SysMon: smaller font 2025-09-10 08:30:38 -04:00
LemmyCook
73de564bb6 IconPicker: fixed at 6 columns with slightly bigger icons 2025-09-10 08:13:10 -04:00
LemmyCook
1f62cdedb5 Icons: cloud-fog 2025-09-10 08:04:18 -04:00
Ly-sec
7ed0e894ec Icons: updated TablerIcons, NightLight 2025-09-10 13:51:37 +02:00
Ly-sec
d39a9a85bf SystemMonitor: add GPU temperature option 2025-09-10 13:17:35 +02:00
Ly-sec
d16d1c1d26 NotificationHistory: even more fixes for appIcon 2025-09-10 12:55:56 +02:00
Ly-sec
291ffac102 NotificationHistory: possible visibility fix for app icons 2025-09-10 12:52:32 +02:00
Ly-sec
2b18ed3c41 NotificationHistory: add app icon display 2025-09-10 12:47:04 +02:00
Ly-sec
3b50efc7d0 ColorScheme: possible fix for selecting colorscheme & dark mode toggle 2025-09-10 12:39:15 +02:00
Ly-sec
d91a635781 NightLight: add force activation 2025-09-10 12:34:52 +02:00
Juve
4afe2d8448 Screen Corners use gerneral radius ratio of settings 2025-09-10 12:55:50 +08:00
LemmyCook
74fce51c2d Icons: new aliases image => photo 2025-09-09 23:59:21 -04:00
Juve
44cdbfe5d7 fix for PowerPanel Shortcut invalid 2025-09-10 11:45:49 +08:00
LemmyCook
4fbb8314eb Icons: settings-network: sitemap-filled 2025-09-09 23:13:06 -04:00
LemmyCook
851a5a6f58 Icon: settings-network using 'sitemap' same as lan 2025-09-09 23:08:08 -04:00
LemmyCook
833808152e Icons: added icons to settings main content title + slightly smaller NCircleStat badges 2025-09-09 22:17:48 -04:00
LemmyCook
84706cab4b Icons: sun-wind for weather partly-cloudy 2025-09-09 21:42:43 -04:00
LemmyCook
e571f26583 Icons: improved ethernet icon 2025-09-09 21:33:31 -04:00
LemmyCook
16cea533da Bluetooth: added a button to enable/disable straight from the panel + minor improvements. 2025-09-09 21:23:57 -04:00
LemmyCook
b1f501f3f9 Added tabler icons license 2025-09-09 21:08:13 -04:00
LemmyCook
afcba942c7 Better settings icons 2025-09-09 20:38:16 -04:00
LemmyCook
5e6f77f875 More icons improvements 2025-09-09 20:32:19 -04:00
LemmyCook
1f9247c429 More icons fixes 2025-09-09 19:34:31 -04:00
LemmyCook
d089966249 NetworkService: bugfix the interface could not longer be enabled or disabled. 2025-09-09 18:53:53 -04:00
LemmyCook
b2d629e6a1 More icons 2025-09-09 18:43:39 -04:00
LemmyCook
032087b611 Battery: disabled test mode 2025-09-09 18:31:59 -04:00
LemmyCook
22dd2bf75c All power icons 2025-09-09 18:30:44 -04:00
LemmyCook
7adc7f43cc Bluetooth devices icons 2025-09-09 18:18:44 -04:00
LemmyCook
a38f49cb35 More icons work 2025-09-09 18:10:25 -04:00
LemmyCook
ca7684c944 ArchUpdater: permanently removed 2025-09-09 18:10:11 -04:00
LemmyCook
955369ab13 More icons work 2025-09-09 17:34:14 -04:00
LemmyCook
48f6c0705b New icons: more icons and cleanup 2025-09-09 17:02:57 -04:00
LemmyCook
43eec0e387 Refactor icons font wip 2025-09-09 14:46:11 -04:00
LemmyCook
b1f9609cd3 Renamed Icons.qml to AppIcons.qml for clarity 2025-09-09 14:16:37 -04:00
Ly-sec
4f731b67d1 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-09 18:38:48 +02:00
Ly-sec
6549b0fc57 NotificationHistoryPanel: possible solution for #235 2025-09-09 18:38:43 +02:00
LemmyCook
ffb972f7c6 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-09 12:28:12 -04:00
LemmyCook
6ed2daa386 NIconButton/CustomButton: added an extra flag to allow click when the button is disabled.
Helps with custom button to get redirected to the settings
2025-09-09 12:28:09 -04:00
LemmyCook
8a4042913b Bootstrap: make the icons map readonly 2025-09-09 12:27:25 -04:00
Lysec
fb3c2f3bb2 Merge pull request #241 from lonerOrz/fix/power
Fix: Incorrect 0% battery warning at startup
2025-09-09 18:20:35 +02:00
Ly-sec
5dc4ba504c PowerProfileService: don't show toast on non valid power profile 2025-09-09 18:15:44 +02:00
loner
c31dc75c63 Fix: Incorrect 0% battery warning at startup 2025-09-10 00:13:00 +08:00
Ly-sec
1f0be929d7 Edit README and also the flake
README: remove breaking change notice (we use a fontloader for the
bootstrap font)
flake: remove material and bootstrap font dependency
2025-09-09 17:57:04 +02:00
LemmyCook
56ea1f92fa Merge branch 'bootstrap-icons' 2025-09-09 11:44:27 -04:00
Ly-sec
5042d4d747 BootstrapIcons: bundle font with noctalia, use fontloader 2025-09-09 17:41:16 +02:00
Ly-sec
144406ae0e PowerProfile: create PowerProfileService, use it for the BarWidget and
PowerProfilesCard
2025-09-09 17:12:56 +02:00
Ly-sec
3d51f758f8 README: small change 2025-09-09 17:03:28 +02:00
Ly-sec
107f6fdfce README: Update breaking changes text 2025-09-09 16:31:19 +02:00
Ly-sec
e4d499b550 Revert "README: Update breaking changes text"
This reverts commit 9c3726bdb1.
2025-09-09 16:30:36 +02:00
Ly-sec
9c3726bdb1 README: Update breaking changes text 2025-09-09 16:28:47 +02:00
Ly-sec
a4534b1611 Merge branch 'bootstrap-icons' 2025-09-09 16:14:18 +02:00
Ly-sec
e4cad6ed20 Update README and flake.nix
README: inform users about breaking changes (due to the font change)
flake: attempt to install the bootstrap-icons font
2025-09-09 15:44:11 +02:00
LemmyCook
5f1cfb9072 CustomButton: no border 2025-09-09 09:27:46 -04:00
LemmyCook
a6ccc8b0da NButton: fix issue when no icon defined 2025-09-09 09:22:05 -04:00
LemmyCook
fe139c208a CustomIcomButton: changed default icon to "heart" 2025-09-09 09:16:48 -04:00
LemmyCook
64c1e842f9 Merge branch 'bootstrap-icons' of github.com:noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 09:13:29 -04:00
LemmyCook
cfd7dec04d WeatherCard: Vertical centering of icons 2025-09-09 09:13:27 -04:00
Ly-sec
a00676f5db Merge branch 'bootstrap-icons' of https://github.com/noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 15:12:49 +02:00
Ly-sec
61cf7ab843 CustomButtonWidget: add icon picker to improve usability 2025-09-09 15:12:46 +02:00
LemmyCook
d76d1c628a NIconButton: animation on color (bg+fg) 2025-09-09 08:56:30 -04:00
LemmyCook
f6b3f6d2ec ProfileCard: more discrete System uptime 2025-09-09 08:49:08 -04:00
LemmyCook
24863c2527 Merge branch 'bootstrap-icons' of github.com:noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 08:27:46 -04:00
LemmyCook
ecd6141739 Toast: better spacing/margin 2025-09-09 08:27:44 -04:00
Ly-sec
ee44920fa4 Merge branch 'bootstrap-icons' of https://github.com/noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 14:26:29 +02:00
Ly-sec
864cbfcfab NSpinBox: remove unicode, use Bootstrap.qml 2025-09-09 14:26:27 +02:00
LemmyCook
73541eec49 ActiveWindow + MediaMini: width boosted to 6% 2025-09-09 08:25:25 -04:00
LemmyCook
87425efa88 Merge branch 'bootstrap-icons' of github.com:noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 08:23:36 -04:00
LemmyCook
4455074493 ActiveWindow+MediaMini: auto min & max width 2025-09-09 08:23:34 -04:00
LemmyCook
5e23476089 NIconButton: font size auto determined by button size 2025-09-09 08:17:00 -04:00
Ly-sec
1232c0268c Merge branch 'bootstrap-icons' of https://github.com/noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 14:12:29 +02:00
Ly-sec
ed9ee65885 ActiveWindow: add guarding for null title/icon (Hyprland)
CompositorService: turn title, appId and id into strings to perhaps
prevent crashing (Hyprland)
2025-09-09 14:11:18 +02:00
LemmyCook
bc7fe21d27 Widget Settings: always use MetaData as default + Removed non existing settting from space (debugMode leftovers) 2025-09-09 08:09:22 -04:00
LemmyCook
f7b0a28b1e Icon: different memory usage icon (cpu) 2025-09-09 08:06:14 -04:00
Ly-sec
b422a419cd BatteryWidget: add low battery threshold
NSpinBox: add bootstrap icons
2025-09-09 13:26:15 +02:00
Ly-sec
663f3abff5 Merge branch 'bootstrap-icons' of https://github.com/noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 13:21:09 +02:00
Ly-sec
3c9ce6f8b5 ScreenRecorder: check for availability 2025-09-09 13:20:46 +02:00
LemmyCook
c9a128e439 Merge branch 'bootstrap-icons' of github.com:Ly-sec/Noctalia into bootstrap-icons 2025-09-09 07:18:46 -04:00
LemmyCook
e8f356f5ac ActiveWindow+MediaMini: Shifted one color each: mPrimary, mSecondary 2025-09-09 07:17:50 -04:00
Ly-sec
94d64a91b8 Add toasts & tooltips to a lot of things, add Disk Usage 2025-09-09 13:08:48 +02:00
LemmyCook
fdfe9ea2e1 WifiPanel: fix missing device icon 2025-09-09 01:45:32 -04:00
LemmyCook
d4d7b06b64 NPill + Clock color uniformisation 2025-09-09 01:44:14 -04:00
LemmyCook
56d87ecfcf Polishing
- Volume: better spread/usage of the 3 icons
- Rosepine colors: more contrast to compare to matugen
- NPill: different look when pile is always opened
2025-09-09 01:02:53 -04:00
LemmyCook
76ef2469e8 Shaders: path from root for easier maintenance + cleanup fallback icons 2025-09-09 00:35:12 -04:00
LemmyCook
16bd4b41dc Icons: ArchUpdater, LockScreen, PowerMenu, BT Device List 2025-09-08 23:32:35 -04:00
LemmyCook
0e4b79fd16 SystemStats / network: dont show bytes 2025-09-08 22:34:56 -04:00
LemmyCook
ad73f11b69 Removed: old system-stats script 2025-09-08 22:23:02 -04:00
LemmyCook
bacd65b274 Icons: 99% done 2025-09-08 22:21:18 -04:00
LemmyCook
1f8c55d581 Icons: huge cleanup 2025-09-08 22:05:57 -04:00
LemmyCook
ccdb4e0664 Icons: more icons 2025-09-08 21:37:01 -04:00
LemmyCook
c77784b5c1 Icons: most settings tabs 2025-09-08 21:23:57 -04:00
LemmyCook
74cf71755b Icons: battery + bt 2025-09-08 21:10:03 -04:00
LemmyCook
a4107c87c0 Icons: WIP using a proper mapping table 2025-09-08 21:05:48 -04:00
LemmyCook
8da2cdf430 Icons: better nightlight and notification history 2025-09-08 20:29:11 -04:00
LemmyCook
7e93e29f66 Icons: duo for nightlight 2025-09-08 20:20:40 -04:00
LemmyCook
b2e11137d4 Weather icon: fix thunderstorm 2025-09-08 20:11:26 -04:00
LemmyCook
d086d64d5f Icons: half of BT 2025-09-08 18:51:05 -04:00
LemmyCook
fa970986dc Icons: more icons 2025-09-08 18:45:09 -04:00
LemmyCook
4c9e89915e Icons: more icons 2025-09-08 17:53:55 -04:00
LemmyCook
97c7fd8073 Icons: more icons 2025-09-08 17:26:21 -04:00
LemmyCook
29167de546 Icons: picking from the right range 2025-09-08 17:11:24 -04:00
LemmyCook
d6f629d4bb Icon test 2025-09-08 16:50:13 -04:00
LemmyCook
b13c40e238 Icon: new speed icon 2025-09-08 16:46:42 -04:00
LemmyCook
170fbea7a4 Settings: better alignment with new icons + check icon on wallpaper selector 2025-09-08 16:17:34 -04:00
LemmyCook
08d2747f1e Icons: color picker + better tab alignment in settings
+ autoformatting
2025-09-08 16:08:53 -04:00
LemmyCook
b91112fc7a Icons: Plus and Minus
+ removed vertical hack in NIcon
2025-09-08 16:02:21 -04:00
LemmyCook
ea6b8e0c02 Icons: Brightness and battery 2025-09-08 15:53:50 -04:00
LemmyCook
404a1d3e8b New icons + some warning fixes 2025-09-08 15:22:43 -04:00
LemmyCook
6f1b88e76d more icons 2025-09-08 14:44:28 -04:00
LemmyCook
6169f88d90 Default skull icon 2025-09-08 14:33:20 -04:00
LemmyCook
6f4a4bb764 volume icons 2025-09-08 13:55:48 -04:00
LemmyCook
242ae17d0a panel icon 2025-09-08 13:29:17 -04:00
LemmyCook
736979c4dc more icons 2025-09-08 13:25:03 -04:00
LemmyCook
4b775fc29d Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-08 12:35:43 -04:00
LemmyCook
ed78b6b3f5 Basic bootstrap icons test 2025-09-08 12:35:29 -04:00
Lysec
a0494d1759 Merge pull request #236 from ThatOneCalculator/fix/animation-speed-logic
fix: divide instead of multiply animation speed
2025-09-08 18:27:41 +02:00
Kainoa Kanter
1c0c4e955a divide instead of multiply animation speed 2025-09-08 09:22:21 -07:00
LemmyCook
b639c3632d autoformatting 2025-09-08 11:51:32 -04:00
LemmyCook
6c93b1b768 Settings: Fix widget settings upgrade on startup, to never overwrite an existing setting with default value. 2025-09-08 10:32:26 -04:00
Ly-sec
d05255c15b Notification: show resolved app name instead of id (possibly fixes #230) 2025-09-08 15:38:29 +02:00
Lysec
59ef26af1c Merge pull request #233 from msdevpt/fix-clear-message
fix: message when no items on clipboard
2025-09-08 15:29:54 +02:00
LemmyCook
33c6ade8f8 Cava: runs only when MediaService is playing 2025-09-08 09:01:27 -04:00
LemmyCook
8c115b8bb0 Settings: fixed faulty widget upgrade 2025-09-08 08:59:30 -04:00
LemmyCook
3271fa1d23 Init: better widget upgrading process + less warnings when starting up without config or cache 2025-09-08 08:39:56 -04:00
LemmyCook
b43b065cf2 Wallpaper: minor optimizations/simplification 2025-09-08 07:51:01 -04:00
Ly-sec
66a4618d09 switch to dev version 2025-09-08 12:33:17 +02:00
Ly-sec
983e3c5cbe Release v2.7.0
Network: Even more improvements
SysStat: Remove bash script
Notification: Pore image support
NotificationHistory: Proper unread count
Settings: Migrate Bar widgets to new settings
BarWidgets: Easier to access, edit
Background: add default wallpaper (if none is set)
SystemMonitor: add % support for RAM
BarTab:
- remove global settings for widgets
- add settings button per bar widget, this makes it possible to have separate settings of the same kind with different settings. This also makes it way easier to configure.

A decent amount of QoL changes & fixes
2025-09-08 12:28:35 +02:00
Ly-sec
c02d3e3d22 Merge branch 'bartab-overhaul' 2025-09-08 12:21:18 +02:00
Ly-sec
c0900b105b Background: add default wallpaper 2025-09-08 08:46:10 +02:00
Ly-sec
b6166a2a7c SystemMonitor: add % support for RAM usage 2025-09-08 08:04:18 +02:00
Ly-sec
38928abab7 Fix first start noctalia settings & color creation 2025-09-08 07:51:49 +02:00
LemmyCook
849f3c52d7 Notifications badge: hidden by default 2025-09-08 01:10:48 -04:00
LemmyCook
f9e55c8f8d Workspace: removed extra transparent padding around. 2025-09-08 01:03:58 -04:00
LemmyCook
993a7965fd NPill: fixed look at high scaling 2025-09-08 01:00:38 -04:00
LemmyCook
d4f6462e8a Battery: deactivated test mode 2025-09-08 00:40:12 -04:00
LemmyCook
8bfde2f6d8 NPill: fixed, finally! 2025-09-08 00:39:07 -04:00
LemmyCook
b3eea2215d Bar Add Widget: taller NComboBox 2025-09-08 00:05:58 -04:00
LemmyCook
4d7bc811c4 Widget Settings: load settings before triggering the loader to avoid async loading. 2025-09-08 00:02:15 -04:00
LemmyCook
74ec5ea606 Cava: running at all time as its getting to know if a widget needs it. 2025-09-07 23:59:22 -04:00
LemmyCook
dda0266798 Autoformatting 2025-09-07 23:51:31 -04:00
LemmyCook
99d9dbe218 WidgetSettings: replaced all checkboxes by the usual toggles. 2025-09-07 23:51:09 -04:00
LemmyCook
89c7f05782 NLabel: always full width even when there is no description 2025-09-07 23:45:13 -04:00
LemmyCook
d9c36a81c4 NightLight: fixed rightclick to open settings 2025-09-07 23:18:27 -04:00
LemmyCook
91747c71f2 Main Settings: cleaned tabs since we removed many settings 2025-09-07 23:18:10 -04:00
LemmyCook
5a1231a17e Settings: completed migration of old settings on startup 2025-09-07 22:55:28 -04:00
LemmyCook
517c7c97d4 Bar Widgets FrontEnd: Simplified access to editable widget settings 2025-09-07 22:23:45 -04:00
LemmyCook
45af873a6f Bar Widget Settings: One file per Widget settings, refactor - wip 2025-09-07 21:45:28 -04:00
LemmyCook
c01167c9da Settings tabs: adapt to new sizing of NComboBox 2025-09-07 21:24:53 -04:00
LemmyCook
a68b3f49b0 NComboBox: better sizing 2025-09-07 21:13:45 -04:00
LemmyCook
e03042c411 NCheckBox: fast animation speed like the others 2025-09-07 21:13:31 -04:00
LemmyCook
3065bec6c9 BarSectionEditor: Buttons are now easier to click + reverted back to 5 basic colors 2025-09-07 20:03:14 -04:00
LemmyCook
dae1d12b6f NPill: smoother animation when opening and closing (no instant width jump) 2025-09-07 18:50:21 -04:00
LemmyCook
c4846cd977 NPill: improved text centering 2025-09-07 18:42:39 -04:00
LemmyCook
f95c9b76d4 Clock fully migrated to new user settings 2025-09-07 14:40:33 -04:00
LemmyCook
fb01392bc3 Settings: cleanup 2025-09-07 14:29:14 -04:00
LemmyCook
498ee478e7 Settings: centralized migration to user settings. wip 2025-09-07 14:28:50 -04:00
M.Silva
53ff6cc21a fix: message when no items on clipboard 2025-09-07 18:48:50 +01:00
LemmyCook
ba33451957 Network/Wi-Fi: many fixes and robustness improvements
- proper detection when password is wrong
- prevent a new connection while already connecting to a network
- new mechanism to skip scan results if a new scan is incoming (avoid UI
discrepancies)
2025-09-07 13:02:13 -04:00
Ly-sec
d6e253fe7f Replace some double with real 2025-09-07 16:25:11 +02:00
Ly-sec
c32a8a863a WeatherTab: remove useless divider 2025-09-07 16:22:07 +02:00
LemmyCook
4ba0f8d958 Network: Scanning use a more reliable backward parsing + added logs to figure potential bug. 2025-09-07 10:06:53 -04:00
Ly-sec
e4e2ed41b4 Rename TimeWeatherTab to WeatherTab, remove Time settings from said tab
WeatherTab: renamed from TimeWeatherTab, remove Time settings
Time: Time/Date is now widget driven
2025-09-07 15:48:16 +02:00
Ly-sec
888ba108e0 Edit NButton alignment 2025-09-07 15:33:47 +02:00
Ly-sec
c14eb95dba BarWidgetSettingsDialog: remove DND, rename Save to Apply 2025-09-07 15:20:24 +02:00
Ly-sec
dc0ef93680 Notification: DND just uses Settings.data.notifications.doNotDisturb now 2025-09-07 15:18:12 +02:00
Ly-sec
a2ea3c116d NotificationHistory: better display for unread notifications 2025-09-07 15:09:30 +02:00
Ly-sec
4578aad0bc NotificationHistory: properly hook up the unread counter 2025-09-07 14:57:09 +02:00
Ly-sec
57448f100c bartab-overhaul: initial commit 2025-09-07 14:48:20 +02:00
Ly-sec
835f88d71e Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-07 12:51:15 +02:00
Ly-sec
291d919b9f Notification: add -i support 2025-09-07 12:51:13 +02:00
LemmyCook
adac96ee84 SidePanel: proper height computation 2025-09-07 00:49:59 -04:00
LemmyCook
9010a1668b SysStat: fixed warning. cant assign undefined to real 2025-09-07 00:43:57 -04:00
LemmyCook
f27608947c Settings: slightly more compact tabs 2025-09-07 00:06:58 -04:00
LemmyCook
fb2d42da57 SysStat Service: less log on intel CPU 2025-09-06 23:47:17 -04:00
LemmyCook
2bc1d53b18 SysStat Service: Porting code to JS/QML instead of an external bash 2025-09-06 23:43:00 -04:00
LemmyCook
36d3a50f21 Brightness: brings back realtime brightness monitoring for internal(laptop) display.
The pill will open and show the change in real time
2025-09-06 19:27:32 -04:00
LemmyCook
9bc6479c92 NPill: for battery use a very light outline around the icon 2025-09-06 18:34:44 -04:00
LemmyCook
56993d3c00 Battery: Minimal BatteryService which only serve an appropriate icon. Trying different icons rotated 90 degrees to the left. 2025-09-06 18:16:59 -04:00
LemmyCook
86c6135def Network/Wi-Fi: improvements
- Always check for ethernet status every 30s. Should not affect battery
life.
- Less aggressive scan intervals to give more times for slow adapters.
2025-09-06 16:11:16 -04:00
LemmyCook
1bb1015fdf Dock: one tooltip per app instead of a shared tooltip. avoid a few glitches when hovering. 2025-09-06 15:25:57 -04:00
LemmyCook
ac43b6d78a Dock: autoformatting 2025-09-06 15:19:06 -04:00
LemmyCook
809f16c27e Dock: improvements, new animations, always float, better look. 2025-09-06 15:18:53 -04:00
LemmyCook
7860c41959 Network/Wi-Fi: Removed auto polling every 30sec. Factorized more code and cleaned logs 2025-09-06 14:14:47 -04:00
LemmyCook
fc1ee9fb2f Network/WiFi: improve UI with more immediate feedback on operations.
+ proper deletion of profiles when forgetting a network
2025-09-06 13:03:22 -04:00
LemmyCook
5bc8f410e7 Network/Wi-Fi: smarter logging to avoid flood 2025-09-06 09:32:02 -04:00
Ly-sec
3d9ef8c2ed switch to dev version 2025-09-06 14:20:31 +02:00
Ly-sec
0e53ce3ac0 Release v2.6.0
SettingsPanel: added keyboard navigation
BluetoothPanel: UI enhancements
WiFiPanel: UI enhancements
NotificationPanel: UI enhancements
ColorPicker: UI enhancements
Toast: handle switching between toasts much better
Notification: add DND option
Notification: add actions
LauncherTab: add app2unit toggle
Spacer: added spacer widget with configurable width
ActiveWindow: fix hyprland icon display
PowerPanel: add keybind controls
NetworkService: make it way more reliable

More QoL fixes & changes
2025-09-06 14:17:12 +02:00
Ly-sec
4131e6503b Implement keyboard controls for PowerPanel as requested in ##227
PowerPanel: add support for keyboard controls
2025-09-06 12:44:19 +02:00
Ly-sec
0aaf78fc51 ActiveWindow: fix hyprland icon display (fixes #201) 2025-09-06 12:40:29 +02:00
Ly-sec
977b2d9e7c Added a Spacer widget so people can add spacing between other widgets
(as requested in ##226).
Spacer: create variable width invisible rectangle
BarWidgetSettingsDialog: add Spacer support
BarWidgetRegistry: add Spacer
2025-09-06 12:27:06 +02:00
Ly-sec
e76b2c5497 Launcher: fix app2unit execution, implemented #202 2025-09-06 12:18:14 +02:00
Ly-sec
8658e11c1d NotificationHistoryPanel: fix layout alignment 2025-09-06 12:16:53 +02:00
LemmyCook
b3e4486699 Network: better refresh vs wifi scan 2025-09-06 01:14:40 -04:00
LemmyCook
2398961473 Wifi: more clean ups and improvements 2025-09-06 01:04:08 -04:00
LemmyCook
a57bfeba31 Background: Qt.callLater does not accept a delay as parameter. 2025-09-06 00:36:03 -04:00
LemmyCook
2f416a87f0 Wifi/Network: refactoring to something simpler to maintain 2025-09-06 00:02:32 -04:00
LemmyCook
9a6c98c134 WiFi: removed status indicator 2025-09-05 23:18:23 -04:00
LemmyCook
35ca346246 Tooltip 2025-09-05 23:17:18 -04:00
LemmyCook
0fd9ac15cd One more tooltip 2025-09-05 22:38:20 -04:00
LemmyCook
ae12d77e29 Tooltips: should end with a coma. 2025-09-05 22:37:54 -04:00
Lemmy
9065257961 Merge pull request #225 from lonerOrz/fix/keyboard-layout-alignment
fix: align KeyboardLayout widget with other bar components
2025-09-05 22:31:41 -04:00
LemmyCook
561b55cb9e Autoformatting 2025-09-05 22:18:08 -04:00
LemmyCook
4f871296ae ColorPicker: splitted in two NColorPicker + NColorPickerDialog
+ fixed layout and a few little bugs
2025-09-05 22:17:58 -04:00
loner
55b74ad38f fix: align KeyboardLayout widget with other bar components 2025-09-06 09:46:42 +08:00
LemmyCook
8426e36f46 Time: improved human readable time + fixed a few tooltips. 2025-09-05 21:08:30 -04:00
LemmyCook
85d94aca01 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 21:04:09 -04:00
LemmyCook
39c7089cbc Notification: fixed persistent DND toast. 2025-09-05 21:04:02 -04:00
Ly-sec
eb072ff88a Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-06 02:16:13 +02:00
Ly-sec
0c4046b993 ColorPicker: UI overhaul 2025-09-06 02:15:51 +02:00
LemmyCook
90cd5467fe Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 19:57:24 -04:00
LemmyCook
05bfb6fc37 Do Not Disturb: factorized logic and toast in its proper service. 2025-09-05 19:57:22 -04:00
Lemmy
966b2410d3 Update README.md 2025-09-05 19:49:01 -04:00
LemmyCook
8ec1ad7255 TaskBar converted to Layout 2025-09-05 19:12:32 -04:00
LemmyCook
1efa1f4aa3 ActiveWindow: Converted to Layout 2025-09-05 19:06:15 -04:00
LemmyCook
0a48e5f34f Clock: text was too big 2025-09-05 18:59:53 -04:00
LemmyCook
ad305b3754 Dock: converted to Layout 2025-09-05 18:53:24 -04:00
LemmyCook
78cb7d4c15 MediaMini: fix visualizer not showing when track length is unknown (twitch) 2025-09-05 18:49:59 -04:00
LemmyCook
7b5c97f38a Tray: converted to Layout 2025-09-05 18:49:34 -04:00
Ly-sec
59bf98e04c Vesktop Template: fix placeholder text 2025-09-06 00:44:05 +02:00
LemmyCook
7feab63e5b Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 18:33:53 -04:00
LemmyCook
5d7e168a57 NCircleStat + KeyboardLayout: converted to Layout 2025-09-05 18:33:51 -04:00
Ly-sec
8038b7f6a0 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-06 00:31:28 +02:00
Ly-sec
2533c52e27 Launcher: add app2unit options (hopefully) 2025-09-06 00:30:47 +02:00
LemmyCook
cf624f4d65 Notification: Converted to Layout
+ removed fontPointSize on NIconButton. use sizeRatio instead.
2025-09-05 18:29:06 -04:00
LemmyCook
a4c98f1382 NotificationHistory: fully converted to Layout 2025-09-05 18:19:27 -04:00
LemmyCook
4768485974 LockScreen: converted to Layout 2025-09-05 18:15:28 -04:00
LemmyCook
9a14a5cc10 SettingsPanel: converted to layout 2025-09-05 18:05:42 -04:00
LemmyCook
cbffc1a14c SidePanel: height fix 2025-09-05 18:05:23 -04:00
Lysec
25e1c6e759 Merge pull request #224 from ThatOneCalculator/refactor/notification-default-action-text
make default notification action text "Open"
2025-09-05 23:50:06 +02:00
Kainoa Kanter
e41c35cb5b make default notification action text "Open" 2025-09-05 14:47:32 -07:00
LemmyCook
078e111ecd Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 17:47:20 -04:00
LemmyCook
01aeceddf4 PowerPanel: converted to Layout 2025-09-05 17:47:19 -04:00
LemmyCook
93a3bc2090 SysMonCard: converted to layout 2025-09-05 17:45:17 -04:00
Ly-sec
28b0536916 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-05 23:44:52 +02:00
Ly-sec
86734f17c4 Notification: remove some logging, implement #223 2025-09-05 23:44:49 +02:00
LemmyCook
94293e4c63 Bar SysMon: converted to Layout 2025-09-05 17:44:04 -04:00
LemmyCook
f06d0f4e1e Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 17:32:54 -04:00
Ly-sec
a5fc9d9ca9 Notification: add actions
README: add fix for niri action buttons for notifications
2025-09-05 23:31:55 +02:00
LemmyCook
c85a309aeb MediaMini: converted to Layout 2025-09-05 17:23:02 -04:00
Ly-sec
4cac584409 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-05 23:07:10 +02:00
Ly-sec
b30d3df15c Notification: only display app icon/avatar if the notification requested it 2025-09-05 23:07:05 +02:00
LemmyCook
6f69654816 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 17:06:10 -04:00
LemmyCook
8fedd7612d NToast: Column => ColumnLayout 2025-09-05 17:06:09 -04:00
Ly-sec
c16e6e7423 Notification: adjust layout 2025-09-05 23:00:03 +02:00
Ly-sec
c8a056f332 Notification: add DND option to widget and notification panel as requested in #212 2025-09-05 22:42:40 +02:00
Ly-sec
60950fb461 dock: add opacity slider as requested in #222 2025-09-05 22:36:04 +02:00
Ly-sec
a3aba8d0db Toast: update visibility for newest toast 2025-09-05 22:29:20 +02:00
LemmyCook
f9a48becce SettingsPanel: finaly fixed the conflict between scrollview and textinput! 2025-09-05 15:47:07 -04:00
LemmyCook
3140039ccb NTextInput: simplified code in an attempt to fix text selection issues with mouse.
Not fixed yet, but I know where the conflict is!
2025-09-05 15:08:45 -04:00
LemmyCook
56fedcf495 HooksTab: removed ScrollView which already exists in parent (SettingsPanel.qml) 2025-09-05 15:07:31 -04:00
LemmyCook
783e9fb140 AboutTab: improved look of "Download latest release" 2025-09-05 14:46:08 -04:00
LemmyCook
b69d6f57d4 Bump dev version 2025-09-05 14:41:04 -04:00
LemmyCook
125d844e3b NInputAction simplification 2025-09-05 14:19:08 -04:00
LemmyCook
f04ac180f0 NInputAction: use proper label/description + autoformatting 2025-09-05 14:13:05 -04:00
LemmyCook
1cab452352 WiFi: small improvements to UI and service 2025-09-05 13:35:07 -04:00
LemmyCook
f3d1d15b61 NPill: added support for middle mouse button 2025-09-05 13:34:31 -04:00
Lemmy
0915071299 Merge pull request #220 from ThatOneCalculator/refactor/audio-bar-widgets-click-consistency
refactor: consistent click behavior for volume & mic bar widgets
2025-09-05 13:29:16 -04:00
Kainoa Kanter
b787080715 consistent tooltip 2025-09-05 10:28:57 -07:00
LemmyCook
a69a6eda4d FontService: tweaked logs 2025-09-05 13:27:54 -04:00
Kainoa Kanter
dd757c2114 refactor: consistent click behavior for volume & mic bar widgets
- left click: open audio settings panel (unchanged) - right click:
toggle mute - middle click: pwvucontrol
2025-09-05 10:26:46 -07:00
LemmyCook
eedea01679 NetworkService: dont report empty errors 2025-09-05 12:04:49 -04:00
LemmyCook
0567da94dd WiFi: auto formattings (removed es6 syntax for split to not break qmlfmt) 2025-09-05 11:58:30 -04:00
LemmyCook
de92c989f2 Launcher: hotfix clicking on an item would not activate it. 2025-09-05 10:33:57 -04:00
LemmyCook
507843be21 --amend 2025-09-05 08:54:13 -04:00
LemmyCook
b9c1a8a54f WiFi: improved UI and service 2025-09-05 08:36:36 -04:00
LemmyCook
35283a6923 WiFi: cleaner look, similar to BT. 2025-09-05 00:55:47 -04:00
LemmyCook
9ae78eda45 Bluetooth: more UI polish 2025-09-04 23:48:16 -04:00
LemmyCook
cc8a24f445 Bluetooth Panel: UI cleanup/factorization 2025-09-04 23:26:19 -04:00
LemmyCook
5910c65bcf SidePanel: fix #218 sidepanel should open next to the button (as other panels) 2025-09-04 20:38:24 -04:00
LemmyCook
e5aee79d47 Removed all layer.enabled as they do not play well with fractional scaling. 2025-09-04 20:36:32 -04:00
LemmyCook
a249e15c58 WiFi: Fix password input placeholder dots are cut off #203 2025-09-04 19:58:08 -04:00
LemmyCook
cdcfe328d2 NPanel: rounding x,y coordinates to avoid artifacts 2025-09-04 19:53:33 -04:00
LemmyCook
784300f690 Dock: removed unecessary bg hover rect 2025-09-04 19:51:10 -04:00
LemmyCook
8ad2bef2f5 NButton: added support for right click and middle click, removed rippled effect. 2025-09-04 18:54:41 -04:00
LemmyCook
2bd30947fc SettingsPanel: reordered code. 2025-09-04 17:30:52 -04:00
LemmyCook
84e8793a29 SettingsPanel: improved keyboard controls 2025-09-04 17:28:35 -04:00
LemmyCook
be1643c5b8 SettingsPanel: added keyboard navigation (Tab, Vim, Up/Down) to change active tab. 2025-09-04 17:14:54 -04:00
LemmyCook
b00f058eac Removed log 2025-09-04 17:06:46 -04:00
LemmyCook
8bab23cfec Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-04 17:03:13 -04:00
LemmyCook
7b26e38f33 Launcher: improved/fixed keyboard controls (Ctrl+J / Ctrl+K) 2025-09-04 17:03:11 -04:00
Lemmy
9e6bd3be76 Update README.md 2025-09-04 16:17:39 -04:00
LemmyCook
97b016b21b Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-04 16:17:33 -04:00
LemmyCook
84fdb7c647 Wallpaper: added IPC to set a wallpaper
qs -c noctalia-shell ipc call wallpaper set $path $monitor

$monitor can be a monitor name or "all" or "" to assign to all monitors.
2025-09-04 16:17:31 -04:00
Lemmy
6d70944fc8 Update bug_report.md 2025-09-04 15:48:12 -04:00
Lemmy
fcb4fa1b59 Update bug_report.md 2025-09-04 15:46:28 -04:00
Lemmy
e69086f1a6 Merge pull request #213 from ThatOneCalculator/patch-1
docs: power management
2025-09-04 15:42:28 -04:00
Kainoa Kanter
fa22607c2c docs: power management 2025-09-04 12:41:00 -07:00
180 changed files with 18485 additions and 7866 deletions

View File

@@ -1,7 +1,7 @@
---
name: Bug Report
about: Report a bug from noctalia-shell
title: "[Bug]: "
title: "[Bug] "
labels: bug
assignees: ''
---
@@ -21,9 +21,9 @@ Explain what you expected to happen.
Add screenshots if applicable.
### Environment
- Distro [e.g., CachyOS, NixOS, Arch, ...]
- Compositor [ e.g., Hyprland, Niri, ...]
- Version: [e.g., 1.0.0 or `main`]
- Distro: [e.g., CachyOS, NixOS, Arch, ...]
- Compositor: [ e.g., Hyprland, Niri, ...]
- Noctalia-shell Version: [e.g., 1.0.0, available in About tab]
### Additional Context
Add any other context about the problem here.
Add any other context about the problem here.

View File

@@ -1,7 +1,7 @@
---
name: Feature Request
about: Suggest a new feature or improvement
title: "[Feature]: "
title: "[Feature] "
labels: enhancement
assignees: ''
---

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.qmlls.ini

View File

@@ -0,0 +1,34 @@
{
"dark": {
"mPrimary": "#aaaaaa",
"mOnPrimary": "#111111",
"mSecondary": "#a7a7a7",
"mOnSecondary": "#111111",
"mTertiary": "#cccccc",
"mOnTertiary": "#111111",
"mError": "#dddddd",
"mOnError": "#111111",
"mSurface": "#111111",
"mOnSurface": "#828282",
"mSurfaceVariant": "#191919",
"mOnSurfaceVariant": "#5d5d5d",
"mOutline": "#3c3c3c",
"mShadow": "#000000"
},
"light": {
"mPrimary": "#555555",
"mOnPrimary": "#eeeeee",
"mSecondary": "#505058",
"mOnSecondary": "#eeeeee",
"mTertiary": "#333333",
"mOnTertiary": "#eeeeee",
"mError": "#222222",
"mOnError": "#efefef",
"mSurface": "#d4d4d4",
"mOnSurface": "#696969",
"mSurfaceVariant": "#e8e8e8",
"mOnSurfaceVariant": "#9e9e9e",
"mOutline": "#c3c3c3",
"mShadow": "#fafafa"
}
}

View File

@@ -1,34 +1,34 @@
{
"dark": {
"mPrimary": "#ebbcba",
"mOnPrimary": "#1f1d2e",
"mOnPrimary": "#191724",
"mSecondary": "#9ccfd8",
"mOnSecondary": "#1f1d2e",
"mTertiary": "#f6c177",
"mOnTertiary": "#1f1d2e",
"mOnSecondary": "#191724",
"mTertiary": "#524f67",
"mOnTertiary": "#e0def4",
"mError": "#eb6f92",
"mOnError": "#1f1d2e",
"mSurface": "#1f1d2e",
"mOnError": "#191724",
"mSurface": "#191724",
"mOnSurface": "#e0def4",
"mSurfaceVariant": "#26233a",
"mOnSurfaceVariant": "#908caa",
"mOutline": "#403d52",
"mShadow": "#1f1d2e"
"mShadow": "#191724"
},
"light": {
"mPrimary": "#d46e6b",
"mPrimary": "#d7827e",
"mOnPrimary": "#faf4ed",
"mSecondary": "#56949f",
"mOnSecondary": "#faf4ed",
"mTertiary": "#31748f",
"mOnTertiary": "#232136",
"mTertiary": "#cecacd",
"mOnTertiary": "#575279",
"mError": "#b4637a",
"mOnError": "#f2e9e1",
"mSurface": "#e0def4",
"mOnSurface": "#232136",
"mSurfaceVariant": "#bcb8e7",
"mOnError": "#faf4ed",
"mSurface": "#faf4ed",
"mOnSurface": "#575279",
"mSurfaceVariant": "#f2e9e1",
"mOnSurfaceVariant": "#797593",
"mOutline": "#9893a5",
"mShadow": "#575279"
"mOutline": "#dfdad9",
"mShadow": "#faf4ed"
}
}

View File

@@ -0,0 +1,16 @@
Tabler Licenses - Detailed Usage Rights and Guidelines
This is a legal agreement between you, the Purchaser, and Tabler. Purchasing or downloading of any Tabler product (Tabler Admin Template, Tabler Icons, Tabler Emails, Tabler Illustrations), constitutes your acceptance of the terms of this license, Tabler terms of service and Tabler private policy.
Tabler Admin Template and Tabler Icons License*
Tabler Admin Template and Tabler Icons are available under MIT License.
Copyright (c) 2018-2025 Tabler
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
See more at Tabler Admin Template MIT License See more at Tabler Icons MIT License

Binary file not shown.

View File

@@ -51,22 +51,19 @@ Singleton {
lines.push("\n[templates.ghostty]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/ghostty.conf"')
lines.push('output_path = "~/.config/ghostty/themes/noctalia"')
lines.push(
"post_hook = \"grep -q '^theme *= *' ~/.config/ghostty/config; and sed -i 's/^theme *= *.*/theme = noctalia/' ~/.config/ghostty/config; or echo 'theme = noctalia' >> ~/.config/ghostty/config\"")
lines.push("post_hook = \"grep -q '^theme *= *' ~/.config/ghostty/config; and sed -i 's/^theme *= *.*/theme = noctalia/' ~/.config/ghostty/config; or echo 'theme = noctalia' >> ~/.config/ghostty/config\"")
}
if (Settings.data.matugen.foot) {
lines.push("\n[templates.foot]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/foot.conf"')
lines.push('output_path = "~/.config/foot/themes/noctalia"')
lines.push(
'post_hook = "sed -i /themes/d ~/.config/foot/foot.ini && echo include=~/.config/foot/themes/noctalia >> ~/.config/foot/foot.ini"')
lines.push('post_hook = "sed -i /themes/d ~/.config/foot/foot.ini && echo include=~/.config/foot/themes/noctalia >> ~/.config/foot/foot.ini"')
}
if (Settings.data.matugen.fuzzel) {
lines.push("\n[templates.fuzzel]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/fuzzel.conf"')
lines.push('output_path = "~/.config/fuzzel/themes/noctalia"')
lines.push(
'post_hook = "sed -i /themes/d ~/.config/fuzzel/fuzzel.ini && echo include=~/.config/fuzzel/themes/noctalia >> ~/.config/fuzzel/fuzzel.ini"')
lines.push('post_hook = "sed -i /themes/d ~/.config/fuzzel/fuzzel.ini && echo include=~/.config/fuzzel/themes/noctalia >> ~/.config/fuzzel/fuzzel.ini"')
}
if (Settings.data.matugen.vesktop) {
lines.push("\n[templates.vesktop]")

View File

@@ -510,7 +510,7 @@
}
.visual-refresh.theme-dark .slateTextArea_ec4baf > div:first-child .emptyText__1464f::before {
content: "Message #general" !important;
content: "send a message" !important;
color: {{colors.on_surface_variant.default.hex}} !important;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -4,4 +4,4 @@
# Can be installed from AUR "qmlfmt-git"
# Requires qt6-5compat
find . -name "*.qml" -print -exec qmlfmt -e -b 120 -t 2 -i 2 -w {} \;
find . -name "*.qml" -print -exec qmlfmt -e -b 360 -t 2 -i 2 -w {} \;

View File

@@ -1,270 +0,0 @@
#!/usr/bin/env -S bash
# A Bash script to monitor system stats and output them in JSON format.
# --- Configuration ---
# Default sleep duration in seconds. Can be overridden by the first argument.
SLEEP_DURATION=3
# --- Argument Parsing ---
# Check if a command-line argument is provided for the sleep duration.
if [[ -n "$1" ]]; then
# Basic validation to ensure the argument is a number (integer or float).
if [[ "$1" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
SLEEP_DURATION=$1
else
# Output to stderr if the format is invalid.
echo "Warning: Invalid duration format '$1'. Using default of ${SLEEP_DURATION}s." >&2
fi
fi
# --- Global Cache Variables ---
# These variables will store the discovered CPU temperature sensor path and type
# to avoid searching for it on every loop iteration.
TEMP_SENSOR_PATH=""
TEMP_SENSOR_TYPE=""
# Network speed monitoring variables
PREV_RX_BYTES=0
PREV_TX_BYTES=0
PREV_TIME=0
# --- Data Collection Functions ---
#
# Gets memory usage in GB, MB, and as a percentage.
#
get_memory_info() {
awk '
/MemTotal/ {total=$2}
/MemAvailable/ {available=$2}
END {
if (total > 0) {
usage_kb = total - available
usage_gb = usage_kb / 1000000
usage_percent = (usage_kb / total) * 100
printf "%.1f %.0f\n", usage_gb, usage_percent
} else {
# Fallback if /proc/meminfo is unreadable or empty.
print "0.0 0 0"
}
}
' /proc/meminfo
}
#
# Gets the usage percentage of the root filesystem ("/").
#
get_disk_usage() {
# df gets disk usage. --output=pcent shows only the percentage for the root path.
# tail -1 gets the data line, and tr removes the '%' sign and whitespace.
df --output=pcent / | tail -1 | tr -d ' %'
}
#
# Calculates current CPU usage over a short interval.
#
get_cpu_usage() {
# Read all 10 CPU time fields to prevent errors on newer kernels.
read -r cpu prev_user prev_nice prev_system prev_idle prev_iowait prev_irq prev_softirq prev_steal prev_guest prev_guest_nice < /proc/stat
# Calculate previous total and idle times.
local prev_total_idle=$((prev_idle + prev_iowait))
local prev_total=$((prev_user + prev_nice + prev_system + prev_idle + prev_iowait + prev_irq + prev_softirq + prev_steal + prev_guest + prev_guest_nice))
# Wait for a short period.
sleep 0.05
# Read all 10 CPU time fields again for the second measurement.
read -r cpu user nice system idle iowait irq softirq steal guest guest_nice < /proc/stat
# Calculate new total and idle times.
local total_idle=$((idle + iowait))
local total=$((user + nice + system + idle + iowait + irq + softirq + steal + guest + guest_nice))
# Add a check to prevent division by zero if total hasn't changed.
if (( total <= prev_total )); then
echo "0.0"
return
fi
# Calculate the difference over the interval.
local diff_total=$((total - prev_total))
local diff_idle=$((total_idle - prev_total_idle))
# Use awk for floating-point calculation and print the percentage.
awk -v total="$diff_total" -v idle="$diff_idle" '
BEGIN {
if (total > 0) {
# Formula: 100 * (Total - Idle) / Total
usage = 100 * (total - idle) / total
printf "%.1f\n", usage
} else {
print "0.0"
}
}'
}
#
# Finds and returns the CPU temperature in degrees Celsius.
# Caches the sensor path for efficiency.
#
get_cpu_temp() {
# If the sensor path hasn't been found yet, search for it.
if [[ -z "$TEMP_SENSOR_PATH" ]]; then
for dir in /sys/class/hwmon/hwmon*; do
# Check if the 'name' file exists and read it.
if [[ -f "$dir/name" ]]; then
local name
name=$(<"$dir/name")
# Check for supported sensor types.
if [[ "$name" == "coretemp" || "$name" == "k10temp" || "$name" == "zenpower" ]]; then
TEMP_SENSOR_PATH=$dir
TEMP_SENSOR_TYPE=$name
break # Found it, no need to keep searching.
fi
fi
done
fi
# If after searching no sensor was found, return 0.
if [[ -z "$TEMP_SENSOR_PATH" ]]; then
echo 0
return
fi
# --- Get temp based on sensor type ---
if [[ "$TEMP_SENSOR_TYPE" == "coretemp" ]]; then
# For Intel 'coretemp', average all available temperature sensors.
local total_temp=0
local sensor_count=0
# Use a for loop with a glob to iterate over all temp input files.
# This is more efficient than 'find' for this simple case.
for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do
# The glob returns the pattern itself if no files match,
# so we must check if the file actually exists.
if [[ -f "$temp_file" ]]; then
total_temp=$((total_temp + $(<"$temp_file")))
sensor_count=$((sensor_count + 1))
fi
done
if (( sensor_count > 0 )); then
# Use awk for the final division to handle potential floating point numbers
# and convert from millidegrees to integer degrees Celsius.
awk -v total="$total_temp" -v count="$sensor_count" 'BEGIN { print int(total / count / 1000) }'
else
# If no sensor files were found, return 0.
echo 0
fi
elif [[ "$TEMP_SENSOR_TYPE" == "k10temp" ]]; then
# For AMD 'k10temp', find the 'Tctl' sensor, which is the control temperature.
local tctl_input=""
for label_file in "$TEMP_SENSOR_PATH"/temp*_label; do
if [[ -f "$label_file" ]] && [[ $(<"$label_file") == "Tctl" ]]; then
# The input file has the same name but with '_input' instead of '_label'.
tctl_input="${label_file%_label}_input"
break
fi
done
if [[ -f "$tctl_input" ]]; then
# Read the temperature and convert from millidegrees to degrees.
echo "$(( $(<"$tctl_input") / 1000 ))"
else
echo 0 # Fallback
fi
elif [[ "$TEMP_SENSOR_TYPE" == "zenpower" ]]; then
# For zenpower, read the first available temp sensor
for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do
if [[ -f "$temp_file" ]]; then
local temp_value
temp_value=$(cat "$temp_file" | tr -d '\n\r') # Remove any newlines
echo "$((temp_value / 1000))"
return
fi
done
echo 0
if [[ -f "$tctl_input" ]]; then
# Read the temperature and convert from millidegrees to degrees.
echo "$(($(<"$tctl_input") / 1000))"
else
echo 0 # Fallback
fi
else
echo 0 # Should not happen if cache logic is correct.
fi
}
# --- Main Loop ---
# This loop runs indefinitely, gathering and printing stats.
while true; do
# Call the functions to gather all the data.
# get_memory_info
read -r mem_gb mem_per <<< "$(get_memory_info)"
# Command substitution captures the single output from the other functions.
disk_per=$(get_disk_usage)
cpu_usage=$(get_cpu_usage)
cpu_temp=$(get_cpu_temp)
# Get network speeds
current_time=$(date +%s.%N)
total_rx=0
total_tx=0
# Read total bytes from /proc/net/dev for all interfaces
while IFS=: read -r interface stats; do
# Skip only loopback interface, allow other interfaces
if [[ "$interface" =~ ^lo[[:space:]]*$ ]]; then
continue
fi
# Extract rx and tx bytes (fields 1 and 9 in the stats part)
rx_bytes=$(echo "$stats" | awk '{print $1}')
tx_bytes=$(echo "$stats" | awk '{print $9}')
# Add to totals if they are valid numbers
if [[ "$rx_bytes" =~ ^[0-9]+$ ]] && [[ "$tx_bytes" =~ ^[0-9]+$ ]]; then
total_rx=$((total_rx + rx_bytes))
total_tx=$((total_tx + tx_bytes))
fi
done < <(tail -n +3 /proc/net/dev)
# Calculate speeds if we have previous data
rx_speed=0
tx_speed=0
if [[ "$PREV_TIME" != "0" ]]; then
time_diff=$(awk -v current="$current_time" -v prev="$PREV_TIME" 'BEGIN { printf "%.3f", current - prev }')
rx_diff=$((total_rx - PREV_RX_BYTES))
tx_diff=$((total_tx - PREV_TX_BYTES))
# Calculate speeds in bytes per second using awk
rx_speed=$(awk -v rx="$rx_diff" -v time="$time_diff" 'BEGIN { printf "%.0f", rx / time }')
tx_speed=$(awk -v tx="$tx_diff" -v time="$time_diff" 'BEGIN { printf "%.0f", tx / time }')
fi
# Update previous values for next iteration
PREV_RX_BYTES=$total_rx
PREV_TX_BYTES=$total_tx
PREV_TIME=$current_time
# Use printf to format the final JSON output string, adding the mem_mb key.
printf '{"cpu": "%s", "cputemp": "%s", "memgb":"%s", "memper": "%s", "diskper": "%s", "rx_speed": "%s", "tx_speed": "%s"}\n' \
"$cpu_usage" \
"$cpu_temp" \
"$mem_gb" \
"$mem_per" \
"$disk_per" \
"$rx_speed" \
"$tx_speed"
# Wait for the specified duration before the next update.
sleep "$SLEEP_DURATION"
done

View File

@@ -9,3 +9,24 @@ for i in {1..8}; do
done
echo "All notifications sent!"
# Additional tests for icon/image handling
if command -v notify-send >/dev/null 2>&1; then
echo "Sending icon/image tests..."
# 1) Themed icon name
notify-send -i dialog-information "Icon name test" "Should resolve from theme (dialog-information)"
# 2) Absolute path if a sample image exists
SAMPLE_IMG="/usr/share/pixmaps/debian-logo.png"
if [ -f "$SAMPLE_IMG" ]; then
notify-send -i "$SAMPLE_IMG" "Absolute path test" "Should show the provided image path"
fi
# 3) file:// URL form
if [ -f "$SAMPLE_IMG" ]; then
notify-send -i "file://$SAMPLE_IMG" "file:// URL test" "Should display after stripping scheme"
fi
echo "Icon/image tests sent!"
fi

53
Commons/AppIcons.qml Normal file
View File

@@ -0,0 +1,53 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Services
Singleton {
id: root
function iconFromName(iconName, fallbackName) {
const fallback = fallbackName || "application-x-executable"
try {
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
const p = Quickshell.iconPath(iconName, fallback)
if (p && p !== "")
return p
}
} catch (e) {
// ignore and fall back
}
try {
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""
} catch (e2) {
return ""
}
}
// Resolve icon path for a DesktopEntries appId - safe on missing entries
function iconForAppId(appId, fallbackName) {
const fallback = fallbackName || "application-x-executable"
if (!appId)
return iconFromName(fallback, fallback)
try {
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
return iconFromName(fallback, fallback)
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId)
const name = entry && entry.icon ? entry.icon : ""
return iconFromName(name || fallback, fallback)
} catch (e) {
return iconFromName(fallback, fallback)
}
}
// Distro logo helper (absolute path or empty string)
function distroLogoPath() {
try {
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""
} catch (e) {
return ""
}
}
}

View File

@@ -102,7 +102,8 @@ Singleton {
// FileView to load custom colors data from colors.json
FileView {
id: customColorsFile
path: Settings.configDir + "colors.json"
path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : undefined
printErrors: false
watchChanges: true
onFileChanged: {
Logger.log("Color", "Reloading colors from disk")
@@ -112,6 +113,13 @@ Singleton {
Logger.log("Color", "Writing colors to disk")
writeAdapter()
}
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path !== undefined) {
reload()
}
}
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) {
// File doesn't exist, create it with default values

View File

@@ -1,54 +1,49 @@
pragma Singleton
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Services
import qs.Commons
import qs.Commons.IconsSets
Singleton {
id: icons
id: root
function iconFromName(iconName, fallbackName) {
const fallback = fallbackName || "application-x-executable"
try {
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
const p = Quickshell.iconPath(iconName, fallback)
if (p && p !== "")
return p
// Expose the font family name for easy access
readonly property string fontFamily: fontLoader.name
readonly property string defaultIcon: TablerIcons.defaultIcon
readonly property var icons: TablerIcons.icons
readonly property var aliases: TablerIcons.aliases
readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.ttf"
Component.onCompleted: {
Logger.log("Icons", "Service started")
}
function get(iconName) {
// Check in aliases first
if (aliases[iconName] !== undefined) {
iconName = aliases[iconName]
}
// Find the appropriate codepoint
return icons[iconName]
}
FontLoader {
id: fontLoader
source: Quickshell.shellDir + fontPath
}
// Monitor font loading status
Connections {
target: fontLoader
function onStatusChanged() {
if (fontLoader.status === FontLoader.Ready) {
Logger.log("Icons", "Font loaded successfully:", fontFamily)
} else if (fontLoader.status === FontLoader.Error) {
Logger.error("Icons", "Font failed to load")
}
} catch (e) {
// ignore and fall back
}
try {
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""
} catch (e2) {
return ""
}
}
// Resolve icon path for a DesktopEntries appId - safe on missing entries
function iconForAppId(appId, fallbackName) {
const fallback = fallbackName || "application-x-executable"
if (!appId)
return iconFromName(fallback, fallback)
try {
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
return iconFromName(fallback, fallback)
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(
appId) : DesktopEntries.byId(appId)
const name = entry && entry.icon ? entry.icon : ""
return iconFromName(name || fallback, fallback)
} catch (e) {
return iconFromName(fallback, fallback)
}
}
// Distro logo helper (absolute path or empty string)
function distroLogoPath() {
try {
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""
} catch (e) {
return ""
}
}
}

File diff suppressed because it is too large Load Diff

205
Commons/KeyboardLayout.qml Normal file
View File

@@ -0,0 +1,205 @@
pragma Singleton
import QtQuick
QtObject {
id: root
// Comprehensive language name to ISO code mapping
property var languageMap: {
"english"// English variants
: "us",
"american": "us",
"united states": "us",
"us english": "us",
"british": "gb",
"uk": "ua",
"united kingdom"// FIXED: Ukrainian language code should map to Ukraine
: "gb",
"english (uk)": "gb",
"canadian": "ca",
"canada": "ca",
"canadian english": "ca",
"australian": "au",
"australia": "au",
"swedish"// Nordic countries
: "se",
"svenska": "se",
"sweden": "se",
"norwegian": "no",
"norsk": "no",
"norway": "no",
"danish": "dk",
"dansk": "dk",
"denmark": "dk",
"finnish": "fi",
"suomi": "fi",
"finland": "fi",
"icelandic": "is",
"íslenska": "is",
"iceland": "is",
"german"// Western/Central European Germanic
: "de",
"deutsch": "de",
"germany": "de",
"austrian": "at",
"austria": "at",
"österreich": "at",
"swiss": "ch",
"switzerland": "ch",
"schweiz": "ch",
"suisse": "ch",
"dutch": "nl",
"nederlands": "nl",
"netherlands": "nl",
"holland": "nl",
"belgian": "be",
"belgium": "be",
"belgië": "be",
"belgique": "be",
"french"// Romance languages (Western/Southern Europe)
: "fr",
"français": "fr",
"france": "fr",
"canadian french": "ca",
"spanish": "es",
"español": "es",
"spain": "es",
"castilian": "es",
"italian": "it",
"italiano": "it",
"italy": "it",
"portuguese": "pt",
"português": "pt",
"portugal": "pt",
"catalan": "ad",
"català": "ad",
"andorra": "ad",
"romanian"// Eastern European Romance
: "ro",
"română": "ro",
"romania": "ro",
"russian"// Slavic languages (Eastern Europe)
: "ru",
"русский": "ru",
"russia": "ru",
"polish": "pl",
"polski": "pl",
"poland": "pl",
"czech": "cz",
"čeština": "cz",
"czech republic": "cz",
"slovak": "sk",
"slovenčina": "sk",
"slovakia": "sk",
"uk": "ua",
"ukrainian"// Ukrainian language code
: "ua",
"українська": "ua",
"ukraine": "ua",
"bulgarian": "bg",
"български": "bg",
"bulgaria": "bg",
"serbian": "rs",
"srpski": "rs",
"serbia": "rs",
"croatian": "hr",
"hrvatski": "hr",
"croatia": "hr",
"slovenian": "si",
"slovenščina": "si",
"slovenia": "si",
"bosnian": "ba",
"bosanski": "ba",
"bosnia": "ba",
"macedonian": "mk",
"македонски": "mk",
"macedonia": "mk",
"irish"// Celtic languages (Western Europe)
: "ie",
"gaeilge": "ie",
"ireland": "ie",
"welsh": "gb",
"cymraeg": "gb",
"wales": "gb",
"scottish": "gb",
"gàidhlig": "gb",
"scotland": "gb",
"estonian"// Baltic languages (Northern Europe)
: "ee",
"eesti": "ee",
"estonia": "ee",
"latvian": "lv",
"latviešu": "lv",
"latvia": "lv",
"lithuanian": "lt",
"lietuvių": "lt",
"lithuania": "lt",
"hungarian"// Other European languages
: "hu",
"magyar": "hu",
"hungary": "hu",
"greek": "gr",
"ελληνικά": "gr",
"greece": "gr",
"albanian": "al",
"shqip": "al",
"albania": "al",
"maltese": "mt",
"malti": "mt",
"malta": "mt",
"turkish"// West/Southwest Asian languages
: "tr",
"türkçe": "tr",
"turkey": "tr",
"arabic": "ar",
"العربية": "ar",
"arab": "ar",
"hebrew": "il",
"עברית": "il",
"israel": "il",
"brazilian"// South American languages
: "br",
"brazilian portuguese": "br",
"brasil": "br",
"brazil": "br",
"japanese"// East Asian languages
: "jp",
"日本語": "jp",
"japan": "jp",
"korean": "kr",
"한국어": "kr",
"korea": "kr",
"south korea": "kr",
"chinese": "cn",
"中文": "cn",
"china": "cn",
"simplified chinese": "cn",
"traditional chinese": "tw",
"taiwan": "tw",
"繁體中文": "tw",
"thai"// Southeast Asian languages
: "th",
"ไทย": "th",
"thailand": "th",
"vietnamese": "vn",
"tiếng việt": "vn",
"vietnam": "vn",
"hindi"// South Asian languages
: "in",
"हिन्दी": "in",
"india": "in",
"afrikaans"// African languages
: "za",
"south africa": "za",
"south african": "za",
"qwerty"// Layout variants
: "us",
"dvorak": "us",
"colemak": "us",
"workman": "us",
"azerty": "fr",
"norman": "fr",
"qwertz": "de"
}
}

View File

@@ -13,24 +13,23 @@ Singleton {
// Default config directory: ~/.config/noctalia
// Default cache directory: ~/.cache/noctalia
property string shellName: "noctalia"
property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME")
|| Quickshell.env(
"HOME") + "/.config") + "/" + shellName + "/"
property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env(
"HOME") + "/.cache") + "/" + shellName + "/"
property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/"
property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache") + "/" + shellName + "/"
property string cacheDirImages: cacheDir + "images/"
property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json")
property string defaultAvatar: Quickshell.env("HOME") + "/.face"
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos"
property string defaultLocation: "Tokyo"
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
// Used to access via Settings.data.xxx.yyy
readonly property alias data: adapter
property bool isLoaded: false
property bool directoriesCreated: false
// Signal emitted when settings are loaded after startupcale changes
signal settingsLoaded
@@ -56,8 +55,7 @@ Singleton {
}
}
if (!hasValidBarMonitor) {
Logger.warn("Settings",
"No configured bar monitors found on system, clearing bar monitor list to show on all screens")
Logger.warn("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens")
adapter.bar.monitors = []
} else {
@@ -71,34 +69,116 @@ Singleton {
// -----------------------------------------------------
// If the settings structure has changed, ensure
// backward compatibility
// backward compatibility by upgrading the settings
function upgradeSettingsData() {
for (var i = 0; i < adapter.bar.widgets.left.length; i++) {
var obj = adapter.bar.widgets.left[i]
if (typeof obj === "string") {
adapter.bar.widgets.left[i] = {
"id": obj
const sections = ["left", "center", "right"]
// -----------------
// 1st. check our settings are not super old, when we only had the widget type as a plain string
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
if (typeof widget === "string") {
adapter.bar.widgets[sectionName][i] = {
"id": widget
}
}
}
}
for (var i = 0; i < adapter.bar.widgets.center.length; i++) {
var obj = adapter.bar.widgets.center[i]
if (typeof obj === "string") {
adapter.bar.widgets.center[i] = {
"id": obj
// -----------------
// 2nd. remove any non existing widget type
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
const widgets = adapter.bar.widgets[sectionName]
// Iterate backward through the widgets array, so it does not break when removing a widget
for (var i = widgets.length - 1; i >= 0; i--) {
var widget = widgets[i]
if (!BarWidgetRegistry.hasWidget(widget.id)) {
widgets.splice(i, 1)
Logger.warn(`Settings`, `Deleted invalid widget ${widget.id}`)
}
}
}
for (var i = 0; i < adapter.bar.widgets.right.length; i++) {
var obj = adapter.bar.widgets.right[i]
if (typeof obj === "string") {
adapter.bar.widgets.right[i] = {
"id": obj
// -----------------
// 3nd. migrate global settings to user settings
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
// Check if widget registry supports user settings, if it does not, then there is nothing to do
const reg = BarWidgetRegistry.widgetMetadata[widget.id]
if ((reg === undefined) || (reg.allowUserSettings === undefined) || !reg.allowUserSettings) {
continue
}
if (upgradeWidget(widget)) {
Logger.log("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget))
}
}
}
}
// -----------------------------------------------------
function upgradeWidget(widget) {
// Backup the widget definition before altering
const widgetBefore = JSON.stringify(widget)
// Migrate old bar settings to proper per widget settings
switch (widget.id) {
case "ActiveWindow":
widget.showIcon = widget.showIcon !== undefined ? widget.showIcon : adapter.bar.showActiveWindowIcon
break
case "Battery":
widget.alwaysShowPercentage = widget.alwaysShowPercentage !== undefined ? widget.alwaysShowPercentage : adapter.bar.alwaysShowBatteryPercentage
break
case "Clock":
widget.use12HourClock = widget.use12HourClock !== undefined ? widget.use12HourClock : adapter.location.use12HourClock
widget.reverseDayMonth = widget.reverseDayMonth !== undefined ? widget.reverseDayMonth : adapter.location.reverseDayMonth
if (widget.showDate !== undefined) {
widget.displayFormat = "time-date"
} else if (widget.showSeconds) {
widget.displayFormat = "time-seconds"
}
delete widget.showDate
delete widget.showSeconds
break
case "MediaMini":
widget.showAlbumArt = widget.showAlbumArt !== undefined ? widget.showAlbumArt : adapter.audio.showMiniplayerAlbumArt
widget.showVisualizer = widget.showVisualizer !== undefined ? widget.showVisualizer : adapter.audio.showMiniplayerCava
break
case "SidePanelToggle":
widget.useDistroLogo = widget.useDistroLogo !== undefined ? widget.useDistroLogo : adapter.bar.useDistroLogo
break
case "SystemMonitor":
widget.showNetworkStats = widget.showNetworkStats !== undefined ? widget.showNetworkStats : adapter.bar.showNetworkStats
break
case "Workspace":
widget.labelMode = widget.labelMode !== undefined ? widget.labelMode : adapter.bar.showWorkspaceLabel
break
}
// Inject missing default setting (metaData) from BarWidgetRegistry
const keys = Object.keys(BarWidgetRegistry.widgetMetadata[widget.id])
for (var i = 0; i < keys.length; i++) {
const k = keys[i]
if (k === "id" || k === "allowUserSettings") {
continue
}
if (widget[k] === undefined) {
widget[k] = BarWidgetRegistry.widgetMetadata[widget.id][k]
}
}
// Compare settings, to detect if something has been upgraded
const widgetAfter = JSON.stringify(widget)
return (widgetAfter !== widgetBefore)
}
// -----------------------------------------------------
// Kickoff essential services
function kickOffServices() {
@@ -114,17 +194,20 @@ Singleton {
FontService.init()
HooksService.init()
BluetoothService.init()
}
// -----------------------------------------------------
Item {
Component.onCompleted: {
// Ensure directories exist before FileView tries to read files
Component.onCompleted: {
// ensure settings dir exists
Quickshell.execDetached(["mkdir", "-p", configDir])
Quickshell.execDetached(["mkdir", "-p", cacheDir])
Quickshell.execDetached(["mkdir", "-p", cacheDirImages])
// ensure settings dir exists
Quickshell.execDetached(["mkdir", "-p", configDir])
Quickshell.execDetached(["mkdir", "-p", cacheDir])
Quickshell.execDetached(["mkdir", "-p", cacheDirImages])
}
// Mark directories as created and trigger file loading
directoriesCreated = true
}
// Don't write settings to disk immediately
@@ -138,18 +221,22 @@ Singleton {
FileView {
id: settingsFileView
path: settingsFile
path: directoriesCreated ? settingsFile : undefined
printErrors: false
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: saveTimer.start()
Component.onCompleted: function () {
reload()
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path !== undefined) {
reload()
}
}
onLoaded: function () {
if (!isLoaded) {
Logger.log("Settings", "----------------------------")
Logger.log("Settings", "Settings loaded successfully")
isLoaded = true
upgradeSettingsData()
@@ -157,6 +244,8 @@ Singleton {
kickOffServices()
isLoaded = true
// Emit the signal
root.settingsLoaded()
}
@@ -170,19 +259,25 @@ Singleton {
JsonAdapter {
id: adapter
property int settingsVersion: 1
property int settingsVersion: 2
// bar
property JsonObject bar: JsonObject {
property string position: "top" // Possible values: "top", "bottom"
property bool showActiveWindowIcon: true
property bool alwaysShowBatteryPercentage: false
property bool showNetworkStats: false
property string position: "top" // "top", "bottom", "left", or "right"
property real backgroundOpacity: 1.0
property bool useDistroLogo: false
property string showWorkspaceLabel: "none"
property list<string> monitors: []
// Floating bar settings
property bool floating: false
property real marginVertical: 0.25
property real marginHorizontal: 0.25
property bool showActiveWindowIcon: true // TODO: delete
property bool alwaysShowBatteryPercentage: false // TODO: delete
property bool showNetworkStats: false // TODO: delete
property bool useDistroLogo: false // TODO: delete
property string showWorkspaceLabel: "none" // TODO: delete
// Widget configuration for modular bar system
property JsonObject widgets
widgets: JsonObject {
@@ -228,6 +323,7 @@ Singleton {
property bool dimDesktop: false
property bool showScreenCorners: false
property real radiusRatio: 1.0
property real screenRadiusRatio: 1.0
// Animation speed multiplier (0.1x - 2.0x)
property real animationSpeed: 1.0
}
@@ -236,9 +332,10 @@ Singleton {
property JsonObject location: JsonObject {
property string name: defaultLocation
property bool useFahrenheit: false
property bool reverseDayMonth: false
property bool use12HourClock: false
property bool showDateWithClock: false
property bool reverseDayMonth: false // TODO: delete
property bool use12HourClock: false // TODO: delete
property bool showDateWithClock: false // TODO: delete
}
// screen recorder
@@ -278,12 +375,15 @@ Singleton {
property string position: "center"
property real backgroundOpacity: 1.0
property list<string> pinnedExecs: []
property bool useApp2Unit: false
}
// dock
property JsonObject dock: JsonObject {
property bool autoHide: false
property bool exclusive: false
property real backgroundOpacity: 1.0
property real floatingRatio: 1.0
property list<string> monitors: []
}
@@ -295,26 +395,33 @@ Singleton {
// notifications
property JsonObject notifications: JsonObject {
property bool doNotDisturb: false
property list<string> monitors: []
// Last time the user opened the notification history (ms since epoch)
property real lastSeenTs: 0
// Duration settings for different urgency levels (in seconds)
property int lowUrgencyDuration: 3
property int normalUrgencyDuration: 8
property int criticalUrgencyDuration: 15
}
// audio
property JsonObject audio: JsonObject {
property bool showMiniplayerAlbumArt: false
property bool showMiniplayerCava: false
property string visualizerType: "linear"
property int volumeStep: 5
property int cavaFrameRate: 60
// MPRIS controls
property string visualizerType: "linear"
property list<string> mprisBlacklist: []
property string preferredPlayer: ""
property bool showMiniplayerAlbumArt: false // TODO: delete
property bool showMiniplayerCava: false // TODO: delete
}
// ui
property JsonObject ui: JsonObject {
property string fontDefault: "Roboto" // Default font for all text
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
property string fontDefault: "Roboto"
property string fontFixed: "DejaVu Sans Mono"
property string fontBillboard: "Inter"
property list<var> monitorsScaling: []
property bool idleInhibitorEnabled: false
}
@@ -348,6 +455,7 @@ Singleton {
// night light
property JsonObject nightLight: JsonObject {
property bool enabled: false
property bool forced: false
property bool autoSchedule: true
property string nightTemp: "4000"
property string dayTemp: "6500"

View File

@@ -29,11 +29,15 @@ Singleton {
property int fontWeightBold: 700
// Radii
property int radiusXXS: 4 * Settings.data.general.radiusRatio
property int radiusXS: 8 * Settings.data.general.radiusRatio
property int radiusS: 12 * Settings.data.general.radiusRatio
property int radiusM: 16 * Settings.data.general.radiusRatio
property int radiusL: 20 * Settings.data.general.radiusRatio
//screen Radii
property int screenRadius: 20 * Settings.data.general.screenRadiusRatio
// Border
property int borderS: 1
property int borderM: 2
@@ -56,14 +60,15 @@ Singleton {
property real opacityFull: 1.0
// Animation duration (ms)
property int animationFast: Math.round(150 * Settings.data.general.animationSpeed)
property int animationNormal: Math.round(300 * Settings.data.general.animationSpeed)
property int animationSlow: Math.round(450 * Settings.data.general.animationSpeed)
property int animationFast: Math.round(150 / Settings.data.general.animationSpeed)
property int animationNormal: Math.round(300 / Settings.data.general.animationSpeed)
property int animationSlow: Math.round(450 / Settings.data.general.animationSpeed)
property int animationSlowest: Math.round(750 / Settings.data.general.animationSpeed)
// Dimensions
property int barHeight: 36
property int barHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 39 : 37
property int capsuleHeight: (barHeight * 0.73)
property int baseWidgetSize: 32
property int baseWidgetSize: (barHeight * 0.9)
property int sliderWidth: 200
// Delays

View File

@@ -9,52 +9,38 @@ Singleton {
id: root
property var date: new Date()
property string time: {
let timeFormat = Settings.data.location.use12HourClock ? "h:mm AP" : "HH:mm"
let timeString = Qt.formatDateTime(date, timeFormat)
if (Settings.data.location.showDateWithClock) {
let dayName = date.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
let day = date.getDate()
let month = date.toLocaleDateString(Qt.locale(), "MMM")
return timeString + " - " + (Settings.data.location.reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
}
return timeString
// Returns a Unix Timestamp (in seconds)
readonly property int timestamp: {
return Math.floor(date / 1000)
}
readonly property string dateString: {
function formatDate(reverseDayMonth = true) {
let now = date
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
let day = now.getDate()
let suffix
if (day > 3 && day < 21)
suffix = 'th'
suffix = 'th'
else
switch (day % 10) {
switch (day % 10) {
case 1:
suffix = "st"
break
suffix = "st"
break
case 2:
suffix = "nd"
break
suffix = "nd"
break
case 3:
suffix = "rd"
break
suffix = "rd"
break
default:
suffix = "th"
}
suffix = "th"
}
let month = now.toLocaleDateString(Qt.locale(), "MMMM")
let year = now.toLocaleDateString(Qt.locale(), "yyyy")
return `${dayName}, `
+ (Settings.data.location.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
}
// Returns a Unix Timestamp (in seconds)
readonly property int timestamp: {
return Math.floor(date / 1000)
return `${dayName}, ` + (reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
}
@@ -78,23 +64,34 @@ Singleton {
}
// Format an easy to read approximate duration ex: 4h32m
// Used to display the time remaining on the Battery widget
// Used to display the time remaining on the Battery widget, computer uptime, etc..
function formatVagueHumanReadableDuration(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60)
const seconds = totalSeconds - (hours * 3600) - (minutes * 60)
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
return '0s'
}
var str = ""
if (hours) {
str += hours.toString() + "h"
}
if (minutes) {
str += minutes.toString() + "m"
}
// Floor the input to handle decimal seconds
totalSeconds = Math.floor(totalSeconds)
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const parts = []
if (days)
parts.push(`${days}d`)
if (hours)
parts.push(`${hours}h`)
if (minutes)
parts.push(`${minutes}m`)
// Only show seconds if no hours and no minutes
if (!hours && !minutes) {
str += seconds.toString() + "s"
parts.push(`${seconds}s`)
}
return str
return parts.join('')
}
Timer {

View File

@@ -1,527 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NPanel {
id: root
panelWidth: 380 * scaling
panelHeight: 500 * scaling
panelAnchorRight: true
panelContent: Rectangle {
color: Color.mSurface
radius: Style.radiusL * scaling
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
// Header
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NIcon {
text: "system_update_alt"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
NText {
text: "System Updates"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
// Reset button (only show if update failed)
NIconButton {
visible: ArchUpdaterService.updateFailed
icon: "refresh"
tooltipText: "Reset update state"
sizeRatio: 0.8
colorBg: Color.mError
colorFg: Color.mOnError
onClicked: {
ArchUpdaterService.resetUpdateState()
}
}
NIconButton {
icon: "close"
tooltipText: "Close"
sizeRatio: 0.8
onClicked: root.close()
}
}
NDivider {
Layout.fillWidth: true
}
// Update summary (only show when packages are available and terminal is configured)
NText {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates > 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
text: ArchUpdaterService.totalUpdates + " package" + (ArchUpdaterService.totalUpdates !== 1 ? "s" : "") + " can be updated"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
Layout.fillWidth: true
}
// Package selection info (only show when not updating and have packages and terminal is configured)
NText {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates > 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.totalUpdates + " packages selected"
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
// Update in progress state
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.updateInProgress
spacing: Style.marginM * scaling
Item {
Layout.fillHeight: true
} // Spacer
NIcon {
text: "hourglass_empty"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Update in progress"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Please check your terminal window for update progress and prompts."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
Item {
Layout.fillHeight: true
} // Spacer
}
// Terminal not available state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: !ArchUpdaterService.terminalAvailable && !ArchUpdaterService.updateInProgress
&& !ArchUpdaterService.updateFailed
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "terminal"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Terminal not configured"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "The TERMINAL environment variable is not set. Please set it to your preferred terminal (e.g., kitty, alacritty, foot) in your shell configuration."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// AUR helper not available state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.terminalAvailable && !ArchUpdaterService.aurHelperAvailable
&& !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "package"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "AUR helper not found"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No AUR helper (yay or paru) is installed. Please install either yay or paru to manage AUR packages. yay is recommended."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Check failed state (AUR down, network issues, etc.)
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.checkFailed && !ArchUpdaterService.updateInProgress
&& !ArchUpdaterService.updateFailed && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "error"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Cannot check for updates"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: ArchUpdaterService.lastCheckError
|| "AUR helper is unavailable or network connection failed. This could be due to AUR being down, network issues, or missing AUR helper (yay/paru)."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
// Prominent refresh button
NIconButton {
icon: "refresh"
tooltipText: "Try checking again"
sizeRatio: 1.2
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
onClicked: {
ArchUpdaterService.forceRefresh()
}
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.marginL * scaling
}
}
}
// Update failed state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.updateFailed
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "error_outline"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Update failed"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Check your terminal for error details and try again."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
// Prominent refresh button
NIconButton {
icon: "refresh"
tooltipText: "Refresh and try again"
sizeRatio: 1.2
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
onClicked: {
ArchUpdaterService.resetUpdateState()
}
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.marginL * scaling
}
}
}
// No updates available state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates === 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "check_circle"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "System is up to date"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "All packages are current. Check back later for updates."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Checking for updates state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.aurBusy && !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& ArchUpdaterService.terminalAvailable && ArchUpdaterService.aurHelperAvailable
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NBusyIndicator {
Layout.alignment: Qt.AlignHCenter
size: Style.fontSizeXXXL * scaling
color: Color.mPrimary
}
NText {
text: "Checking for updates"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Scanning package databases for available updates..."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Package list (only show when not in any special state)
NBox {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates > 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
Layout.fillWidth: true
Layout.fillHeight: true
// Combine repo and AUR lists in order: repos first, then AUR
property var items: (ArchUpdaterService.repoPackages || []).concat(ArchUpdaterService.aurPackages || [])
ListView {
id: unifiedList
anchors.fill: parent
anchors.margins: Style.marginM * scaling
cacheBuffer: Math.round(300 * scaling)
clip: true
model: parent.items
delegate: Rectangle {
width: unifiedList.width
height: 44 * scaling
color: Color.transparent
radius: Style.radiusS * scaling
RowLayout {
anchors.fill: parent
spacing: Style.marginS * scaling
// Checkbox for selection
NCheckbox {
id: checkbox
label: ""
description: ""
checked: ArchUpdaterService.isPackageSelected(modelData.name)
baseSize: Math.max(Style.baseWidgetSize * 0.7, 14)
onToggled: function (checked) {
ArchUpdaterService.togglePackageSelection(modelData.name)
// Force refresh of the checked property
checkbox.checked = ArchUpdaterService.isPackageSelected(modelData.name)
}
}
// Package info
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS * scaling
NText {
text: modelData.name
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
NText {
text: modelData.oldVersion + " → " + modelData.newVersion
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
}
// Source tag (AUR vs PAC)
Rectangle {
visible: !!modelData.source
radius: width * 0.5
color: modelData.source === "aur" ? Color.mTertiary : Color.mSecondary
Layout.alignment: Qt.AlignVCenter
implicitHeight: Style.fontSizeS * 1.8 * scaling
// Width based on label content + horizontal padding
implicitWidth: badgeText.implicitWidth + Math.max(12 * scaling, Style.marginS * scaling)
NText {
id: badgeText
anchors.centerIn: parent
text: modelData.source === "aur" ? "AUR" : "PAC"
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightBold
color: modelData.source === "aur" ? Color.mOnTertiary : Color.mOnSecondary
}
}
}
}
}
}
// Action buttons (only show when not updating)
RowLayout {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
Layout.fillWidth: true
spacing: Style.marginL * scaling
NIconButton {
icon: "refresh"
tooltipText: ArchUpdaterService.aurBusy ? "Checking for updates..." : (!ArchUpdaterService.canPoll ? "Refresh available soon" : "Refresh package lists")
onClicked: {
ArchUpdaterService.forceRefresh()
}
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
Layout.fillWidth: true
enabled: !ArchUpdaterService.aurBusy
}
NIconButton {
icon: "system_update_alt"
tooltipText: "Update all packages"
enabled: ArchUpdaterService.totalUpdates > 0
onClicked: {
ArchUpdaterService.runUpdate()
root.close()
}
colorBg: ArchUpdaterService.totalUpdates > 0 ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ArchUpdaterService.totalUpdates > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant
Layout.fillWidth: true
}
NIconButton {
icon: "check_box"
tooltipText: "Update selected packages"
enabled: ArchUpdaterService.selectedPackagesCount > 0
onClicked: {
if (ArchUpdaterService.selectedPackagesCount > 0) {
ArchUpdaterService.runSelectiveUpdate()
root.close()
}
}
colorBg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant
Layout.fillWidth: true
}
}
}
}
}

View File

@@ -3,6 +3,8 @@ import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Modules.SettingsPanel
import qs.Widgets
Variants {
id: backgroundVariants
@@ -41,9 +43,7 @@ Variants {
// Fillmode default is "crop"
property real fillMode: 1.0
property vector4d fillColor: Qt.vector4d(Settings.data.wallpaper.fillColor.r,
Settings.data.wallpaper.fillColor.g,
Settings.data.wallpaper.fillColor.b, 1.0)
property vector4d fillColor: Qt.vector4d(Settings.data.wallpaper.fillColor.r, Settings.data.wallpaper.fillColor.g, Settings.data.wallpaper.fillColor.b, 1.0)
// On startup assign wallpaper immediately
Component.onCompleted: {
@@ -137,7 +137,7 @@ Variants {
property real screenWidth: width
property real screenHeight: height
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_fade.frag.qsb")
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_fade.frag.qsb")
}
// Wipe transition shader
@@ -162,7 +162,7 @@ Variants {
property real screenWidth: width
property real screenHeight: height
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_wipe.frag.qsb")
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_wipe.frag.qsb")
}
// Disc reveal transition shader
@@ -189,7 +189,7 @@ Variants {
property real screenWidth: width
property real screenHeight: height
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_disc.frag.qsb")
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_disc.frag.qsb")
}
// Diagonal stripes transition shader
@@ -216,7 +216,7 @@ Variants {
property real screenWidth: width
property real screenHeight: height
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_stripes.frag.qsb")
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_stripes.frag.qsb")
}
// Animation for the transition progress
@@ -227,8 +227,7 @@ Variants {
from: 0.0
to: 1.0
// The stripes shader feels faster visually, we make it a bit slower here.
duration: transitionType == "stripes" ? Settings.data.wallpaper.transitionDuration
* 1.6 : Settings.data.wallpaper.transitionDuration
duration: transitionType == "stripes" ? Settings.data.wallpaper.transitionDuration * 1.6 : Settings.data.wallpaper.transitionDuration
easing.type: Easing.InOutCubic
onFinished: {
// Swap images after transition completes
@@ -237,7 +236,7 @@ Variants {
transitionProgress = 0.0
Qt.callLater(() => {
currentWallpaper.asynchronous = true
}, 100)
})
}
}

View File

@@ -68,9 +68,7 @@ Variants {
// Make the overview darker
Rectangle {
anchors.fill: parent
color: Settings.data.colorSchemes.darkMode ? Qt.alpha(Color.mSurface,
Style.opacityMedium) : Qt.alpha(Color.mOnSurface,
Style.opacityMedium)
color: Settings.data.colorSchemes.darkMode ? Qt.alpha(Color.mSurface, Style.opacityMedium) : Qt.alpha(Color.mOnSurface, Style.opacityMedium)
}
}
}

View File

@@ -7,7 +7,7 @@ import qs.Services
import qs.Widgets
Loader {
active: Settings.data.general.showScreenCorners
active: Settings.data.general.showScreenCorners && !Settings.data.bar.floating
sourceComponent: Variants {
model: Quickshell.screens
@@ -20,8 +20,8 @@ Loader {
screen: modelData
property color cornerColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
property real cornerRadius: 20 * scaling
property real cornerSize: 20 * scaling
property real cornerRadius: Style.screenRadius * scaling
property real cornerSize: Style.screenRadius * scaling
Connections {
target: ScalingService
@@ -46,12 +46,10 @@ Loader {
}
margins {
top: ((modelData && Settings.data.bar.monitors.includes(modelData.name))
|| (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top"
&& Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
bottom: ((modelData && Settings.data.bar.monitors.includes(modelData.name))
|| (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom"
&& Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
top: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
bottom: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
left: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "left" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
right: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "right" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
}
mask: Region {}

View File

@@ -27,116 +27,219 @@ Variants {
}
}
active: Settings.isLoaded && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name)
|| (Settings.data.bar.monitors.length === 0)) : false
active: Settings.isLoaded && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
sourceComponent: PanelWindow {
screen: modelData || null
WlrLayershell.namespace: "noctalia-bar"
implicitHeight: Math.round(Style.barHeight * scaling)
implicitHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? screen.height : Math.round(Style.barHeight * scaling)
implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Math.round(Style.barHeight * scaling) : screen.width
color: Color.transparent
anchors {
top: Settings.data.bar.position === "top"
bottom: Settings.data.bar.position === "bottom"
left: true
right: true
top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
bottom: Settings.data.bar.position === "bottom" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
left: Settings.data.bar.position === "left" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
}
// Floating bar margins - only apply when floating is enabled
margins {
top: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
bottom: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
left: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
right: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
}
Item {
anchors.fill: parent
clip: true
// Background fill
// Background fill with shadow
Rectangle {
id: bar
anchors.fill: parent
color: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
layer.enabled: true
// Floating bar rounded corners
radius: Settings.data.bar.floating ? Style.radiusL : 0
}
// ------------------------------
// Left Section - Dynamic Widgets
Row {
id: leftSection
objectName: "leftSection"
// For vertical bars, use a single column layout
Loader {
id: verticalBarLayout
anchors.fill: parent
visible: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
sourceComponent: verticalBarComponent
}
height: parent.height
anchors.left: parent.left
anchors.leftMargin: Style.marginS * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
// For horizontal bars, use the original three-section layout
Loader {
id: horizontalBarLayout
anchors.fill: parent
visible: Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
sourceComponent: horizontalBarComponent
}
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"barSection": parent.objectName,
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
// Main layout components
Component {
id: verticalBarComponent
Item {
anchors.fill: parent
// Top section (left widgets)
Column {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Style.marginM * root.scaling
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "left",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
}
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
// Center section (center widgets)
Column {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "center",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
// Bottom section (right widgets)
Column {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.marginM * root.scaling
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "right",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
}
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
// ------------------------------
// Center Section - Dynamic Widgets
Row {
id: centerSection
objectName: "centerSection"
Component {
id: horizontalBarComponent
Item {
anchors.fill: parent
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"barSection": parent.objectName,
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
// Left Section
RowLayout {
id: leftSection
objectName: "leftSection"
anchors.left: parent.left
anchors.leftMargin: Style.marginS * root.scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "left",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
}
}
}
}
}
}
// ------------------------------
// Right Section - Dynamic Widgets
Row {
id: rightSection
objectName: "rightSection"
height: parent.height
anchors.right: bar.right
anchors.rightMargin: Style.marginS * scaling
anchors.verticalCenter: bar.verticalCenter
spacing: Style.marginS * scaling
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"barSection": parent.objectName,
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
}
// Center Section
RowLayout {
id: centerSection
objectName: "centerSection"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "center",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
}
}
}
// Right Section
RowLayout {
id: rightSection
objectName: "rightSection"
anchors.right: parent.right
anchors.rightMargin: Style.marginS * root.scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "right",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
}
}
}
}
}
}

View File

@@ -31,8 +31,7 @@ PopupWindow {
implicitWidth: menuWidth * scaling
// Use the content height of the Flickable for implicit height
implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9,
flickable.contentHeight + (Style.marginS * 2 * scaling))
implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9, flickable.contentHeight + (Style.marginS * 2 * scaling))
visible: false
color: Color.transparent
anchor.item: anchorItem
@@ -159,8 +158,7 @@ PopupWindow {
NText {
id: text
Layout.fillWidth: true
color: (modelData?.enabled
?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
font.pointSize: Style.fontSizeS * scaling
verticalAlignment: Text.AlignVCenter
@@ -176,7 +174,7 @@ PopupWindow {
}
NIcon {
text: modelData?.hasChildren ? "menu" : ""
icon: modelData?.hasChildren ? "menu" : ""
font.pointSize: Style.fontSizeS * scaling
verticalAlignment: Text.AlignVCenter
visible: modelData?.hasChildren ?? false

View File

@@ -2,38 +2,122 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Commons
import qs.Services
import qs.Widgets
Row {
Item {
id: root
property ShellScreen screen
property real scaling: 1.0
readonly property real minWidth: 160
readonly property real maxWidth: 400
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: getTitle() !== ""
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
// 6% of total width
readonly property real minWidth: Math.max(1, screen.width * 0.06)
readonly property real maxWidth: minWidth * 2
readonly property string barPosition: Settings.data.bar.position
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
function getTitle() {
// Use the service's focusedWindowTitle property which is updated immediately
// when WindowOpenedOrChanged events are received
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
try {
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
} catch (e) {
Logger.warn("ActiveWindow", "Error getting title:", e)
return ""
}
}
visible: getTitle() !== ""
function calculatedVerticalHeight() {
// Use standard widget height like other widgets
return Math.round(Style.capsuleHeight * scaling)
}
function calculatedHorizontalWidth() {
let total = Style.marginM * 2 * scaling // internal padding
if (showIcon) {
total += Style.baseWidgetSize * 0.5 * scaling + 2 * scaling // icon + spacing
}
// Calculate actual text width more accurately
const title = getTitle()
if (title !== "") {
// Estimate text width: average character width * number of characters
const avgCharWidth = Style.fontSizeS * scaling * 0.6 // rough estimate
const titleWidth = Math.min(title.length * avgCharWidth, 80 * scaling)
total += titleWidth
}
// Row layout handles spacing between widgets
return Math.max(total, Style.capsuleHeight * scaling) // Minimum width
}
function getAppIcon() {
const focusedWindow = CompositorService.getFocusedWindow()
if (!focusedWindow || !focusedWindow.appId)
return ""
try {
// Try CompositorService first
const focusedWindow = CompositorService.getFocusedWindow()
if (focusedWindow && focusedWindow.appId) {
try {
const idValue = focusedWindow.appId
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue)
const iconResult = AppIcons.iconForAppId(normalizedId.toLowerCase())
if (iconResult && iconResult !== "") {
return iconResult
}
} catch (iconError) {
Logger.warn("ActiveWindow", "Error getting icon from CompositorService:", iconError)
}
}
return Icons.iconForAppId(focusedWindow.appId)
// Fallback to ToplevelManager
if (ToplevelManager && ToplevelManager.activeToplevel) {
try {
const activeToplevel = ToplevelManager.activeToplevel
if (activeToplevel.appId) {
const idValue2 = activeToplevel.appId
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2)
const iconResult2 = AppIcons.iconForAppId(normalizedId2.toLowerCase())
if (iconResult2 && iconResult2 !== "") {
return iconResult2
}
}
} catch (fallbackError) {
Logger.warn("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError)
}
}
return ""
} catch (e) {
Logger.warn("ActiveWindow", "Error in getAppIcon:", e)
return ""
}
}
// A hidden text element to safely measure the full title width
// A hidden text element to safely measure the full title width
NText {
id: fullTitleMetrics
visible: false
@@ -43,33 +127,35 @@ Row {
}
Rectangle {
// Let the Rectangle size itself based on its content (the Row)
id: windowTitleRect
visible: root.visible
width: row.width + Style.marginM * 2 * scaling
height: Math.round(Style.capsuleHeight * scaling)
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
Item {
id: mainContainer
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
clip: true
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
// Horizontal layout for top/bottom bars
RowLayout {
id: horizontalLayout
anchors.centerIn: parent
spacing: 2 * scaling
visible: barPosition === "top" || barPosition === "bottom"
// Window icon
Item {
width: Style.fontSizeL * scaling * 1.2
height: Style.fontSizeL * scaling * 1.2
anchors.verticalCenter: parent.verticalCenter
visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon
Layout.preferredWidth: Style.baseWidgetSize * 0.5 * scaling
Layout.preferredHeight: Style.baseWidgetSize * 0.5 * scaling
Layout.alignment: Qt.AlignVCenter
visible: getTitle() !== "" && showIcon
IconImage {
id: windowIcon
@@ -78,31 +164,41 @@ Row {
asynchronous: true
smooth: true
visible: source !== ""
// Handle loading errors gracefully
onStatusChanged: {
if (status === Image.Error) {
Logger.warn("ActiveWindow", "Failed to load icon:", source)
}
}
}
}
NText {
id: titleText
// For short titles, show full. For long titles, truncate and expand on hover
width: {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
Layout.preferredWidth: {
try {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, 80 * scaling)) // Limited width for horizontal bars
}
} catch (e) {
Logger.warn("ActiveWindow", "Error calculating width:", e)
return 80 * scaling
}
}
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
text: getTitle()
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mSecondary
color: Color.mPrimary
clip: true
Behavior on width {
Behavior on Layout.preferredWidth {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.InOutCubic
@@ -111,12 +207,85 @@ Row {
}
}
// Vertical layout for left/right bars - icon only
Item {
id: verticalLayout
anchors.centerIn: parent
width: parent.width - Style.marginXS * scaling * 2
height: parent.height - Style.marginXS * scaling * 2
visible: barPosition === "left" || barPosition === "right"
// Window icon
Item {
width: Style.baseWidgetSize * 0.5 * scaling
height: Style.baseWidgetSize * 0.5 * scaling
anchors.centerIn: parent
visible: getTitle() !== "" && showIcon
IconImage {
id: windowIconVertical
anchors.fill: parent
source: getAppIcon()
asynchronous: true
smooth: true
visible: source !== ""
// Handle loading errors gracefully
onStatusChanged: {
if (status === Image.Error) {
Logger.warn("ActiveWindow", "Failed to load icon:", source)
}
}
}
}
}
// Mouse area for hover detection
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
if (barPosition === "left" || barPosition === "right") {
tooltip.show()
}
}
onExited: {
if (barPosition === "left" || barPosition === "right") {
tooltip.hide()
}
}
}
// Hover tooltip with full title (only for vertical bars)
NTooltip {
id: tooltip
target: verticalLayout
text: getTitle()
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
delay: 500
}
}
}
Connections {
target: CompositorService
function onActiveWindowChanged() {
try {
windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
} catch (e) {
Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e)
}
}
function onWindowListChanged() {
try {
windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
} catch (e) {
Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e)
}
}
}

View File

@@ -1,80 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
property real scaling: 1.0
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
colorBorder: Color.transparent
colorBorderHover: Color.transparent
colorFg: {
if (!ArchUpdaterService.terminalAvailable || !ArchUpdaterService.aurHelperAvailable) {
return Color.mError
}
return (ArchUpdaterService.totalUpdates === 0) ? Color.mOnSurface : Color.mPrimary
}
// Icon states
icon: {
if (!ArchUpdaterService.terminalAvailable) {
return "terminal"
}
if (!ArchUpdaterService.aurHelperAvailable) {
return "package"
}
if (ArchUpdaterService.aurBusy) {
return "sync"
}
if (ArchUpdaterService.totalUpdates > 0) {
return "system_update_alt"
}
return "task_alt"
}
// Tooltip with repo vs AUR breakdown and sample lists
tooltipText: {
if (!ArchUpdaterService.terminalAvailable) {
return "Terminal not configured\nSet TERMINAL environment variable"
}
if (!ArchUpdaterService.aurHelperAvailable) {
return "AUR helper not found\nInstall yay or paru"
}
if (ArchUpdaterService.aurBusy) {
return "Checking for updates…"
}
const total = ArchUpdaterService.totalUpdates
if (total === 0) {
return "System is up to date ✓"
}
let header = (total === 1) ? "1 package can be updated" : (total + " packages can be updated")
const pacCount = ArchUpdaterService.updates
const aurCount = ArchUpdaterService.aurUpdates
const pacmanTooltip = (pacCount > 0) ? ((pacCount === 1) ? "1 system package" : pacCount + " system packages") : ""
const aurTooltip = (aurCount > 0) ? ((aurCount === 1) ? "1 AUR package" : aurCount + " AUR packages") : ""
let tooltip = header
if (pacmanTooltip !== "") {
tooltip += "\n" + pacmanTooltip
}
if (aurTooltip !== "") {
tooltip += "\n" + aurTooltip
}
return tooltip
}
onClicked: {
// Always allow panel to open, never block
PanelService.getPanel("archUpdaterPanel").toggle(screen, this)
}
}

View File

@@ -11,11 +11,38 @@ Item {
property ShellScreen screen
property real scaling: 1.0
property string barSection: ""
property int sectionWidgetIndex: 0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Track if we've already notified to avoid spam
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property bool alwaysShowPercentage: widgetSettings.alwaysShowPercentage !== undefined ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
readonly property real warningThreshold: widgetSettings.warningThreshold !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold
// Test mode
readonly property bool testMode: false
readonly property int testPercent: 90
readonly property bool testCharging: false
// Main properties
readonly property var battery: UPower.displayDevice
readonly property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
readonly property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
readonly property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
property bool hasNotifiedLowBattery: false
implicitWidth: pill.width
@@ -23,15 +50,12 @@ Item {
// Helper to evaluate and possibly notify
function maybeNotify(percent, charging) {
const p = Math.round(percent)
// Only notify exactly at 15%, not at 0% or any other percentage
if (!charging && p === 15 && !root.hasNotifiedLowBattery) {
Quickshell.execDetached(
["notify-send", "-u", "critical", "-i", "battery-caution", "Low Battery", `Battery is at ${p}%. Please connect charger.`])
// Only notify once we are a below threshold
if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) {
root.hasNotifiedLowBattery = true
}
// Reset when charging starts or when battery recovers above 20%
if (charging || p > 20) {
ToastService.showWarning("Low Battery", `Battery is at ${Math.round(percent)}%. Please connect the charger.`)
} else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) {
// Reset when charging starts or when battery recovers 5% above threshold
root.hasNotifiedLowBattery = false
}
}
@@ -40,99 +64,58 @@ Item {
Connections {
target: UPower.displayDevice
function onPercentageChanged() {
let battery = UPower.displayDevice
let isReady = battery && battery.ready && battery.isLaptopBattery && battery.isPresent
let percent = isReady ? (battery.percentage * 100) : 0
let charging = isReady ? battery.state === UPowerDeviceState.Charging : false
root.maybeNotify(percent, charging)
var currentPercent = UPower.displayDevice.percentage * 100
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging
root.maybeNotify(currentPercent, isCharging)
}
function onStateChanged() {
let battery = UPower.displayDevice
let isReady = battery && battery.ready && battery.isLaptopBattery && battery.isPresent
let charging = isReady ? battery.state === UPowerDeviceState.Charging : false
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging
// Reset notification flag when charging starts
if (charging) {
if (isCharging) {
root.hasNotifiedLowBattery = false
}
// Also re-evaluate maybeNotify, as state might have changed
var currentPercent = UPower.displayDevice.percentage * 100
root.maybeNotify(currentPercent, isCharging)
}
}
NPill {
id: pill
// Test mode
property bool testMode: false
property int testPercent: 20
property bool testCharging: false
property var battery: UPower.displayDevice
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
// Choose icon based on charge and charging state
function batteryIcon() {
if (!isReady || !battery.isLaptopBattery)
return "battery_android_alert"
if (charging)
return "battery_android_bolt"
if (percent >= 95)
return "battery_android_full"
// Hardcoded battery symbols
if (percent >= 85)
return "battery_android_6"
if (percent >= 70)
return "battery_android_5"
if (percent >= 55)
return "battery_android_4"
if (percent >= 40)
return "battery_android_3"
if (percent >= 25)
return "battery_android_2"
if (percent >= 10)
return "battery_android_1"
if (percent >= 0)
return "battery_android_0"
}
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: batteryIcon()
text: ((isReady && battery.isLaptopBattery) || testMode) ? Math.round(percent) + "%" : "-"
textColor: charging ? Color.mPrimary : Color.mOnSurface
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady)
text: (isReady || testMode) ? Math.round(percent) + "%" : "-"
autoHide: false
forceOpen: isReady && (testMode || battery.isLaptopBattery) && Settings.data.bar.alwaysShowBatteryPercentage
forceOpen: isReady && (testMode || battery.isLaptopBattery) && alwaysShowPercentage
disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery))
tooltipText: {
let lines = []
if (testMode) {
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345))
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(12345)}.`)
return lines.join("\n")
}
if (!isReady || !battery.isLaptopBattery) {
return "No battery detected"
return "No battery detected."
}
if (battery.timeToEmpty > 0) {
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty))
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(battery.timeToEmpty)}.`)
}
if (battery.timeToFull > 0) {
lines.push("Time until full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull))
lines.push(`Time until full: ${Time.formatVagueHumanReadableDuration(battery.timeToFull)}.`)
}
if (battery.changeRate !== undefined) {
const rate = battery.changeRate
if (rate > 0) {
lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed(
2) + " W")
lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W." : "Discharging rate: " + rate.toFixed(2) + " W.")
} else if (rate < 0) {
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W")
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W.")
} else {
lines.push("Estimating...")
}
} else {
lines.push(charging ? "Charging" : "Discharging")
lines.push(charging ? "Charging." : "Discharging.")
}
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
lines.push("Health: " + Math.round(battery.healthPercentage) + "%")

View File

@@ -13,14 +13,14 @@ NIconButton {
property ShellScreen screen
property real scaling: 1.0
visible: Settings.data.network.bluetoothEnabled
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: "bluetooth"
tooltipText: "Bluetooth devices"
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen, this)
icon: Settings.data.network.bluetoothEnabled ? "bluetooth" : "bluetooth-off"
tooltipText: "Bluetooth devices."
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
}

View File

@@ -10,10 +10,26 @@ Item {
property ShellScreen screen
property real scaling: 1.0
property string barSection: ""
property int sectionWidgetIndex: 0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool userAlwaysShowPercentage: (widgetSettings.alwaysShowPercentage !== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
// Used to avoid opening the pill on Quickshell startup
property bool firstBrightnessReceived: false
@@ -28,8 +44,7 @@ Item {
function getIcon() {
var monitor = getMonitor()
var brightness = monitor ? monitor.brightness : 0
return brightness <= 0 ? "brightness_1" : brightness < 0.33 ? "brightness_low" : brightness
< 0.66 ? "brightness_medium" : "brightness_high"
return brightness <= 0.5 ? "brightness-low" : "brightness-high"
}
// Connection used to open the pill when brightness changes
@@ -37,46 +52,42 @@ Item {
target: getMonitor()
ignoreUnknownSignals: true
function onBrightnessUpdated() {
Logger.log("Bar-Brightness", "OnBrightnessUpdated")
var monitor = getMonitor()
if (!monitor)
return
var currentBrightness = monitor.brightness
// Ignore if this is the first time or if brightness hasn't actually changed
// Ignore if this is the first time we receive an update.
// Most likely service just kicked off.
if (!firstBrightnessReceived) {
firstBrightnessReceived = true
monitor.lastBrightness = currentBrightness
return
}
// Only show pill if brightness actually changed (not just loaded from settings)
if (Math.abs(currentBrightness - monitor.lastBrightness) > 0.1) {
pill.show()
}
monitor.lastBrightness = currentBrightness
pill.show()
hideTimerAfterChange.restart()
}
}
Timer {
id: hideTimerAfterChange
interval: 2500
running: false
repeat: false
onTriggered: pill.hide()
}
NPill {
id: pill
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: getIcon()
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want
text: {
var monitor = getMonitor()
return monitor ? (Math.round(monitor.brightness * 100) + "%") : ""
}
forceOpen: userAlwaysShowPercentage
tooltipText: {
var monitor = getMonitor()
if (!monitor)
return ""
return "Brightness: " + Math.round(monitor.brightness * 100) + "%\nMethod: " + monitor.method
+ "\nLeft click for advanced settings.\nScroll up/down to change brightness."
return "Brightness: " + Math.round(monitor.brightness * 100) + "%\nMethod: " + monitor.method + "\nLeft click for advanced settings.\nScroll up/down to change brightness."
}
onWheel: function (angle) {
@@ -90,10 +101,10 @@ Item {
}
}
onClicked: {
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Brightness
settingsPanel.open(screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
}
}
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
@@ -10,24 +11,203 @@ Rectangle {
property ShellScreen screen
property real scaling: 1.0
implicitWidth: clock.width + Style.marginM * 2 * scaling
implicitHeight: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property string barPosition: Settings.data.bar.position
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property bool use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock
readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth !== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth
readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat
// Use compact mode for vertical bars
readonly property bool useCompactMode: barPosition === "left" || barPosition === "right"
implicitWidth: useCompactMode ? Math.round(Style.capsuleHeight * scaling) : Math.round(layout.implicitWidth + Style.marginM * 2 * scaling)
implicitHeight: useCompactMode ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusS * scaling)
color: Color.mSurfaceVariant
// Clock Icon with attached calendar
NClock {
id: clock
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
Item {
id: clockContainer
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
NTooltip {
id: tooltip
text: Time.dateString
target: clock
positionAbove: Settings.data.bar.position === "bottom"
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: useCompactMode ? -2 * scaling : -3 * scaling
// Compact mode for vertical bars - Time section (HH, MM)
Repeater {
model: useCompactMode ? 2 : 1
NText {
readonly property bool showSeconds: (displayFormat === "time-seconds")
readonly property bool inlineDate: (displayFormat === "time-date")
readonly property var now: Time.date
text: {
if (useCompactMode) {
// Compact mode: time section (first 2 lines)
switch (index) {
case 0:
// Hours
if (use12h) {
const hours = now.getHours()
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
return displayHours.toString().padStart(2, '0')
} else {
return now.getHours().toString().padStart(2, '0')
}
case 1:
// Minutes
return now.getMinutes().toString().padStart(2, '0')
default:
return ""
}
} else {
// Normal mode: single line with time
let timeStr = ""
if (use12h) {
// 12-hour format with proper padding and consistent spacing
const hours = now.getHours()
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
const paddedHours = displayHours.toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
const ampm = hours < 12 ? 'AM' : 'PM'
if (showSeconds) {
const seconds = now.getSeconds().toString().padStart(2, '0')
timeStr = `${paddedHours}:${minutes}:${seconds} ${ampm}`
} else {
timeStr = `${paddedHours}:${minutes} ${ampm}`
}
} else {
// 24-hour format with padding
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
if (showSeconds) {
const seconds = now.getSeconds().toString().padStart(2, '0')
timeStr = `${hours}:${minutes}:${seconds}`
} else {
timeStr = `${hours}:${minutes}`
}
}
// Add inline date if needed
if (inlineDate) {
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
const day = now.getDate().toString().padStart(2, '0')
let month = now.toLocaleDateString(Qt.locale(), "MMM")
timeStr += " - " + (reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
}
return timeStr
}
}
font.family: Settings.data.ui.fontFixed
font.pointSize: useCompactMode ? Style.fontSizeXXS * scaling : Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
// Separator line for compact mode (between time and date)
Rectangle {
visible: useCompactMode
Layout.preferredWidth: 20 * scaling
Layout.preferredHeight: 2 * scaling
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 3 * scaling
Layout.bottomMargin: 3 * scaling
color: Color.mPrimary
opacity: 0.3
radius: 1 * scaling
}
// Compact mode for vertical bars - Date section (DD, MM)
Repeater {
model: useCompactMode ? 2 : 0
NText {
readonly property var now: Time.date
text: {
if (useCompactMode) {
// Compact mode: date section (last 2 lines)
switch (index) {
case 0:
// Day
return now.getDate().toString().padStart(2, '0')
case 1:
// Month
return (now.getMonth() + 1).toString().padStart(2, '0')
default:
return ""
}
}
return ""
}
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
// Second line for normal mode (date)
NText {
visible: !useCompactMode && (displayFormat === "time-date-short")
text: {
const now = Time.date
const day = now.getDate().toString().padStart(2, '0')
const month = (now.getMonth() + 1).toString().padStart(2, '0')
return reverseDayMonth ? `${month}/${day}` : `${day}/${month}`
}
// Enable fixed-width font for consistent spacing
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightRegular
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
}
NTooltip {
id: tooltip
text: `${Time.formatDate(reverseDayMonth)}.`
target: clockContainer
positionAbove: Settings.data.bar.position === "bottom"
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
if (!PanelService.getPanel("calendarPanel")?.active) {
tooltip.show()
@@ -38,7 +218,7 @@ Rectangle {
}
onClicked: {
tooltip.hide()
PanelService.getPanel("calendarPanel")?.toggle(screen, this)
PanelService.getPanel("calendarPanel")?.toggle(this)
}
}
}

View File

@@ -13,13 +13,14 @@ NIconButton {
property var screen
property real scaling: 1.0
property string barSection: ""
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Get user settings from Settings data
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
@@ -30,59 +31,59 @@ NIconButton {
}
// Use settings or defaults from BarWidgetRegistry
readonly property string userIcon: widgetSettings.icon || BarWidgetRegistry.widgetMetadata["CustomButton"].icon
readonly property string userLeftClickExec: widgetSettings.leftClickExec
|| BarWidgetRegistry.widgetMetadata["CustomButton"].leftClickExec
readonly property string userRightClickExec: widgetSettings.rightClickExec
|| BarWidgetRegistry.widgetMetadata["CustomButton"].rightClickExec
readonly property string userMiddleClickExec: widgetSettings.middleClickExec
|| BarWidgetRegistry.widgetMetadata["CustomButton"].middleClickExec
readonly property bool hasExec: (userLeftClickExec || userRightClickExec || userMiddleClickExec)
readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon
readonly property string leftClickExec: widgetSettings.leftClickExec || widgetMetadata.leftClickExec
readonly property string rightClickExec: widgetSettings.rightClickExec || widgetMetadata.rightClickExec
readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec
readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec)
enabled: hasExec
allowClickWhenDisabled: true // we want to be able to open config with left click when its not setup properly
colorBorder: Color.transparent
colorBorderHover: Color.transparent
sizeRatio: 0.8
icon: userIcon
icon: customIcon
tooltipText: {
if (!hasExec) {
return "Custom Button - Configure in settings"
} else {
var lines = []
if (userLeftClickExec !== "") {
lines.push(`Left click: <i>${userLeftClickExec}</i>`)
if (leftClickExec !== "") {
lines.push(`Left click: <i>${leftClickExec}</i>.`)
}
if (userRightClickExec !== "") {
lines.push(`Right click: <i>${userRightClickExec}</i>`)
if (rightClickExec !== "") {
lines.push(`Right click: <i>${rightClickExec}</i>.`)
}
if (userMiddleClickExec !== "") {
lines.push(`Middle click: <i>${userMiddleClickExec}</i>`)
if (middleClickExec !== "") {
lines.push(`Middle click: <i>${middleClickExec}</i>.`)
}
return lines.join("<br/>")
}
}
opacity: hasExec ? Style.opacityFull : Style.opacityMedium
onClicked: {
if (userLeftClickExec) {
Quickshell.execDetached(["sh", "-c", userLeftClickExec])
Logger.log("CustomButton", `Executing command: ${userLeftClickExec}`)
if (leftClickExec) {
Quickshell.execDetached(["sh", "-c", leftClickExec])
Logger.log("CustomButton", `Executing command: ${leftClickExec}`)
} else if (!hasExec) {
// No script was defined, open settings
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Bar
settingsPanel.open(screen)
settingsPanel.open()
}
}
onRightClicked: {
if (userRightClickExec) {
Quickshell.execDetached(["sh", "-c", userRightClickExec])
Logger.log("CustomButton", `Executing command: ${userRightClickExec}`)
if (rightClickExec) {
Quickshell.execDetached(["sh", "-c", rightClickExec])
Logger.log("CustomButton", `Executing command: ${rightClickExec}`)
}
}
onMiddleClicked: {
if (userMiddleClickExec) {
Quickshell.execDetached(["sh", "-c", userMiddleClickExec])
Logger.log("CustomButton", `Executing command: ${userMiddleClickExec}`)
if (middleClickExec) {
Quickshell.execDetached(["sh", "-c", middleClickExec])
Logger.log("CustomButton", `Executing command: ${middleClickExec}`)
}
}
}

View File

@@ -9,12 +9,12 @@ NIconButton {
property ShellScreen screen
property real scaling: 1.0
icon: "contrast"
icon: "dark-mode"
tooltipText: "Toggle light/dark mode"
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBg: Settings.data.colorSchemes.darkMode ? Color.mSurfaceVariant : Color.mPrimary
colorFg: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mOnPrimary
colorBorder: Color.transparent
colorBorderHover: Color.transparent

View File

@@ -13,10 +13,10 @@ NIconButton {
sizeRatio: 0.8
icon: "coffee"
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake"
colorBg: Color.mSurfaceVariant
colorFg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mOnSurface
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mOnSurface
colorBorder: Color.transparent
onClicked: {
IdleInhibitorService.manualToggle()

View File

@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
@@ -6,31 +7,48 @@ import qs.Commons
import qs.Services
import qs.Widgets
Row {
Item {
id: root
property ShellScreen screen
property real scaling: 1.0
property string barSection: ""
property int sectionWidgetIndex: 0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool forceOpen: (widgetSettings.forceOpen !== undefined) ? widgetSettings.forceOpen : widgetMetadata.forceOpen
// Use the shared service for keyboard layout
property string currentLayout: KeyboardLayoutService.currentLayout
width: pill.width
height: pill.height
implicitWidth: pill.width
implicitHeight: pill.height
NPill {
id: pill
anchors.verticalCenter: parent.verticalCenter
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: "keyboard_alt"
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
icon: "keyboard"
autoHide: false // Important to be false so we can hover as long as we want
text: currentLayout
tooltipText: "Keyboard layout: " + currentLayout
text: currentLayout.toUpperCase()
tooltipText: "Keyboard layout: " + currentLayout.toUpperCase()
forceOpen: root.forceOpen
fontSize: Style.fontSizeS // Use larger font size
onClicked: {

View File

@@ -7,23 +7,64 @@ import qs.Commons
import qs.Services
import qs.Widgets
Row {
Item {
id: root
property ShellScreen screen
property real scaling: 1.0
readonly property real minWidth: 160
readonly property real maxWidth: 400
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: MediaService.currentPlayer !== null && MediaService.canPlay
width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property string barPosition: Settings.data.bar.position
readonly property bool showAlbumArt: (widgetSettings.showAlbumArt !== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt
readonly property bool showVisualizer: (widgetSettings.showVisualizer !== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType !== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType
// 6% of total width
readonly property real minWidth: Math.max(1, screen.width * 0.06)
readonly property real maxWidth: minWidth * 2
function getTitle() {
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
}
function calculatedVerticalHeight() {
return Math.round(Style.baseWidgetSize * 0.8 * scaling)
}
function calculatedHorizontalWidth() {
let total = Style.marginM * 2 * scaling // internal padding
if (showAlbumArt) {
total += 18 * scaling + 2 * scaling // album art + spacing
} else {
total += Style.fontSizeL * scaling + 2 * scaling // icon + spacing
}
total += Math.min(fullTitleMetrics.contentWidth, maxWidth * scaling) // title text
// Row layout handles spacing between widgets
return total
}
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)) : 0
visible: MediaService.currentPlayer !== null && MediaService.canPlay
// A hidden text element to safely measure the full title width
NText {
id: fullTitleMetrics
@@ -34,15 +75,13 @@ Row {
Rectangle {
id: mediaMini
// Let the Rectangle size itself based on its content (the Row)
width: row.width + Style.marginM * 2 * scaling
height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
visible: root.visible
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
// Used to anchor the tooltip, so the tooltip does not move when the content expands
Item {
@@ -54,14 +93,13 @@ Row {
Item {
id: mainContainer
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear"
&& MediaService.isPlaying && MediaService.trackLength > 0
active: showVisualizer && visualizerType == "linear" && MediaService.isPlaying
z: 0
sourceComponent: LinearSpectrum {
@@ -71,68 +109,70 @@ Row {
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
}
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored"
&& MediaService.isPlaying && MediaService.trackLength > 0
z: 0
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: showVisualizer && visualizerType == "mirrored" && MediaService.isPlaying
z: 0
sourceComponent: MirroredSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
}
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave"
&& MediaService.isPlaying && MediaService.trackLength > 0
z: 0
sourceComponent: WaveSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
sourceComponent: MirroredSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
}
Row {
id: row
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: showVisualizer && visualizerType == "wave" && MediaService.isPlaying
z: 0
sourceComponent: WaveSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
}
// Horizontal layout for top/bottom bars
RowLayout {
id: rowLayout
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: barPosition === "top" || barPosition === "bottom"
z: 1 // Above the visualizer
NIcon {
id: windowIcon
text: MediaService.isPlaying ? "pause" : "play_arrow"
icon: MediaService.isPlaying ? "media-pause" : "media-play"
font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible
Layout.alignment: Qt.AlignVCenter
visible: !showAlbumArt && getTitle() !== "" && !trackArt.visible
}
Column {
anchors.verticalCenter: parent.verticalCenter
visible: Settings.data.audio.showMiniplayerAlbumArt
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
visible: showAlbumArt
spacing: 0
Item {
width: Math.round(18 * scaling)
height: Math.round(18 * scaling)
Layout.preferredWidth: Math.round(18 * scaling)
Layout.preferredHeight: Math.round(18 * scaling)
NImageCircled {
id: trackArt
anchors.fill: parent
imagePath: MediaService.trackArtUrl
fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow"
fallbackIcon: MediaService.isPlaying ? "media-pause" : "media-play"
fallbackIconSize: 10 * scaling
borderWidth: 0
border.color: Color.transparent
}
@@ -142,23 +182,23 @@ Row {
NText {
id: titleText
// For short titles, show full. For long titles, truncate and expand on hover
width: {
Layout.preferredWidth: {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
}
}
Layout.alignment: Qt.AlignVCenter
text: getTitle()
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mTertiary
color: Color.mSecondary
Behavior on width {
Behavior on Layout.preferredWidth {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.InOutCubic
@@ -167,6 +207,33 @@ Row {
}
}
// Vertical layout for left/right bars - icon only
Item {
id: verticalLayout
anchors.centerIn: parent
width: parent.width - Style.marginM * scaling * 2
height: parent.height - Style.marginM * scaling * 2
visible: barPosition === "left" || barPosition === "right"
z: 1 // Above the visualizer
// Media icon
Item {
width: Style.baseWidgetSize * 0.5 * scaling
height: Style.baseWidgetSize * 0.5 * scaling
anchors.centerIn: parent
visible: getTitle() !== ""
NIcon {
id: mediaIconVertical
anchors.fill: parent
icon: MediaService.isPlaying ? "media-pause" : "media-play"
font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
}
// Mouse area for hover detection
MouseArea {
id: mouseArea
@@ -189,12 +256,18 @@ Row {
}
onEntered: {
if (tooltip.text !== "") {
if (barPosition === "left" || barPosition === "right") {
tooltip.show()
} else if (tooltip.text !== "") {
tooltip.show()
}
}
onExited: {
tooltip.hide()
if (barPosition === "left" || barPosition === "right") {
tooltip.hide()
} else {
tooltip.hide()
}
}
}
}
@@ -203,16 +276,23 @@ Row {
NTooltip {
id: tooltip
text: {
var str = ""
if (MediaService.canGoNext) {
str += "Right click for next\n"
if (barPosition === "left" || barPosition === "right") {
return getTitle()
} else {
var str = ""
if (MediaService.canGoNext) {
str += "Right click for next.\n"
}
if (MediaService.canGoPrevious) {
str += "Middle click for previous."
}
return str
}
if (MediaService.canGoPrevious) {
str += "Middle click for previous\n"
}
return str
}
target: anchor
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : anchor
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
positionAbove: Settings.data.bar.position === "bottom"
delay: 500
}
}

View File

@@ -12,10 +12,26 @@ Item {
property ShellScreen screen
property real scaling: 1.0
property string barSection: ""
property int sectionWidgetIndex: 0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool alwaysShowPercentage: (widgetSettings.alwaysShowPercentage !== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
// Used to avoid opening the pill on Quickshell startup
property bool firstInputVolumeReceived: false
property int wheelAccumulator: 0
@@ -25,9 +41,9 @@ Item {
function getIcon() {
if (AudioService.inputMuted) {
return "mic_off"
return "microphone-mute"
}
return AudioService.inputVolume <= Number.EPSILON ? "mic_off" : (AudioService.inputVolume < 0.33 ? "mic" : "mic")
return (AudioService.inputVolume <= Number.EPSILON) ? "microphone-mute" : "microphone"
}
// Connection used to open the pill when input volume changes
@@ -74,12 +90,10 @@ Item {
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: getIcon()
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.inputVolume * 100) + "%"
tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100)
+ "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."
forceOpen: alwaysShowPercentage
tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."
onWheel: function (delta) {
wheelAccumulator += delta
@@ -92,18 +106,15 @@ Item {
}
}
onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Audio
settingsPanel.open(screen)
}
onRightClicked: {
AudioService.setInputMuted(!AudioService.inputMuted)
}
}
Process {
id: pwvucontrolProcess
command: ["pwvucontrol"]
running: false
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Audio
settingsPanel.open()
}
onMiddleClicked: {
Quickshell.execDetached(["pwvucontrol"])
}
}
}

View File

@@ -4,6 +4,7 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Services
import qs.Widgets
@@ -14,19 +15,28 @@ NIconButton {
property real scaling: 1.0
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBg: Settings.data.nightLight.forced ? Color.mPrimary : Color.mSurfaceVariant
colorFg: Settings.data.nightLight.forced ? Color.mOnPrimary : Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off"
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled" : "disabled"}\nLeft click to toggle.\nRight click to access settings.`
onClicked: Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled
icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "forced." : "enabled.") : "disabled."}\nLeft click to cycle (disabled normal forced).\nRight click to access settings.`
onClicked: {
if (!Settings.data.nightLight.enabled) {
Settings.data.nightLight.enabled = true
Settings.data.nightLight.forced = false
} else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) {
Settings.data.nightLight.forced = true
} else {
Settings.data.nightLight.enabled = false
Settings.data.nightLight.forced = false
}
}
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open(screen)
settingsPanel.open()
}
}

View File

@@ -13,12 +13,84 @@ NIconButton {
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool showUnreadBadge: (widgetSettings.showUnreadBadge !== undefined) ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge
readonly property bool hideWhenZero: (widgetSettings.hideWhenZero !== undefined) ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero
function lastSeenTs() {
return Settings.data.notifications?.lastSeenTs || 0
}
function computeUnreadCount() {
var since = lastSeenTs()
var count = 0
var model = NotificationService.historyModel
for (var i = 0; i < model.count; i++) {
var item = model.get(i)
var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp
if (ts > since)
count++
}
return count
}
sizeRatio: 0.8
icon: "notifications"
tooltipText: "Notification history"
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'."
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this)
onClicked: {
var panel = PanelService.getPanel("notificationHistoryPanel")
panel?.toggle(this)
Settings.data.notifications.lastSeenTs = Time.timestamp * 1000
}
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
Loader {
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -4 * scaling
anchors.topMargin: -4 * scaling
z: 2
active: showUnreadBadge && (!hideWhenZero || computeUnreadCount() > 0)
sourceComponent: Rectangle {
id: badge
readonly property int count: computeUnreadCount()
readonly property string label: count <= 99 ? String(count) : "99+"
readonly property real pad: 8 * scaling
height: 16 * scaling
width: Math.max(height, textNode.implicitWidth + pad)
radius: height / 2
color: Color.mError
border.color: Color.mSurface
border.width: 1
visible: count > 0 || !hideWhenZero
NText {
id: textNode
anchors.centerIn: parent
text: badge.label
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnError
}
}
}
}

View File

@@ -11,49 +11,43 @@ NIconButton {
property ShellScreen screen
property real scaling: 1.0
property var powerProfiles: PowerProfiles
readonly property bool hasPP: powerProfiles.hasPerformanceProfile
readonly property bool hasPP: PowerProfileService.available
sizeRatio: 0.8
visible: hasPP
function profileIcon() {
if (!hasPP)
return "balance"
if (powerProfiles.profile === PowerProfile.Performance)
return "speed"
if (powerProfiles.profile === PowerProfile.Balanced)
return "balance"
if (powerProfiles.profile === PowerProfile.PowerSaver)
return "eco"
return "balanced"
if (PowerProfileService.profile === PowerProfile.Performance)
return "performance"
if (PowerProfileService.profile === PowerProfile.Balanced)
return "balanced"
if (PowerProfileService.profile === PowerProfile.PowerSaver)
return "powersaver"
}
function profileName() {
if (!hasPP)
return "Unknown"
if (powerProfiles.profile === PowerProfile.Performance)
if (PowerProfileService.profile === PowerProfile.Performance)
return "Performance"
if (powerProfiles.profile === PowerProfile.Balanced)
if (PowerProfileService.profile === PowerProfile.Balanced)
return "Balanced"
if (powerProfiles.profile === PowerProfile.PowerSaver)
if (PowerProfileService.profile === PowerProfile.PowerSaver)
return "Power Saver"
}
function changeProfile() {
if (!hasPP)
return
if (powerProfiles.profile === PowerProfile.Performance)
powerProfiles.profile = PowerProfile.PowerSaver
else if (powerProfiles.profile === PowerProfile.Balanced)
powerProfiles.profile = PowerProfile.Performance
else if (powerProfiles.profile === PowerProfile.PowerSaver)
powerProfiles.profile = PowerProfile.Balanced
PowerProfileService.cycleProfile()
}
icon: root.profileIcon()
tooltipText: root.profileName()
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.mSurfaceVariant : Color.mPrimary
colorFg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnSurface : Color.mOnPrimary
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: root.changeProfile()

View File

@@ -13,11 +13,11 @@ NIconButton {
sizeRatio: 0.8
icon: "power_settings_new"
icon: "power"
tooltipText: "Power Settings"
colorBg: Color.mSurfaceVariant
colorFg: Color.mError
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("powerPanel")?.toggle(screen)
onClicked: PanelService.getPanel("powerPanel")?.toggle()
}

View File

@@ -11,7 +11,7 @@ NIconButton {
property real scaling: 1.0
visible: ScreenRecorderService.isRecording
icon: "videocam"
icon: "camera-video"
tooltipText: "Screen recording is active\nClick to stop recording"
sizeRatio: 0.8
colorBg: Color.mPrimary

View File

@@ -1,3 +1,4 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import QtQuick.Effects
@@ -11,8 +12,27 @@ NIconButton {
property ShellScreen screen
property real scaling: 1.0
icon: Settings.data.bar.useDistroLogo ? "" : "widgets"
tooltipText: "Open side panel"
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool useDistroLogo: (widgetSettings.useDistroLogo !== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo
icon: useDistroLogo ? "" : "noctalia"
tooltipText: "Open side panel."
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
@@ -21,17 +41,16 @@ NIconButton {
colorBorderHover: Color.transparent
anchors.verticalCenter: parent.verticalCenter
onClicked: PanelService.getPanel("sidePanel")?.toggle(screen)
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle(screen)
onClicked: PanelService.getPanel("sidePanel")?.toggle(this)
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle()
// When enabled, draw the distro logo instead of the icon glyph
IconImage {
id: logo
anchors.centerIn: parent
width: root.width * 0.6
height: width
source: Settings.data.bar.useDistroLogo ? DistroLogoService.osLogo : ""
visible: false //Settings.data.bar.useDistroLogo && source !== ""
source: useDistroLogo ? DistroLogoService.osLogo : ""
visible: useDistroLogo && source !== ""
smooth: true
}

View File

@@ -0,0 +1,40 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
Item {
id: root
// Widget properties passed from Bar.qml
property var screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
// Use settings or defaults from BarWidgetRegistry
readonly property int spacerWidth: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width
// Set the width based on user settings
implicitWidth: spacerWidth * scaling
implicitHeight: Style.barHeight * scaling
width: implicitWidth
height: implicitHeight
}

View File

@@ -1,145 +1,426 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
Row {
Rectangle {
id: root
property ShellScreen screen
property real scaling: 1.0
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
Rectangle {
// Let the Rectangle size itself based on its content (the Row)
width: row.width + Style.marginM * scaling * 2
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
readonly property string barPosition: Settings.data.bar.position
anchors.verticalCenter: parent.verticalCenter
readonly property bool showCpuUsage: (widgetSettings.showCpuUsage !== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage
readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp
readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage
readonly property bool showMemoryAsPercent: (widgetSettings.showMemoryAsPercent !== undefined) ? widgetSettings.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats
readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage
anchors.centerIn: parent
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : Math.round(horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
implicitHeight: (barPosition === "left" || barPosition === "right") ? Math.round(verticalLayout.implicitHeight + Style.marginM * 2 * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
// Horizontal layout for top/bottom bars
RowLayout {
id: horizontalLayout
anchors.centerIn: parent
anchors.leftMargin: Style.marginM * scaling
anchors.rightMargin: Style.marginM * scaling
spacing: Style.marginXS * scaling
visible: barPosition === "top" || barPosition === "bottom"
// CPU Usage Component
Item {
id: mainContainer
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
Layout.preferredWidth: cpuUsageRow.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
visible: showCpuUsage
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
Row {
id: cpuUsageLayout
spacing: Style.marginXS * scaling
RowLayout {
id: cpuUsageRow
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NIcon {
id: cpuUsageIcon
text: "speed"
anchors.verticalCenter: parent.verticalCenter
}
NText {
id: cpuUsageText
text: `${SystemStatService.cpuUsage}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
NIcon {
icon: "cpu-usage"
font.pointSize: Style.fontSizeM * scaling
Layout.alignment: Qt.AlignVCenter
}
// CPU Temperature Component
Row {
id: cpuTempLayout
// spacing is thin here to compensate for the vertical thermometer icon
spacing: Style.marginXXS * scaling
NText {
text: `${SystemStatService.cpuUsage}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
}
NIcon {
text: "thermometer"
anchors.verticalCenter: parent.verticalCenter
}
// CPU Temperature Component
Item {
Layout.preferredWidth: cpuTempRow.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
visible: showCpuTemp
NText {
text: `${SystemStatService.cpuTemp}°C`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
RowLayout {
id: cpuTempRow
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NIcon {
icon: "cpu-temperature"
// Fire is so tall, we need to make it smaller
font.pointSize: Style.fontSizeS * scaling
Layout.alignment: Qt.AlignVCenter
}
// Memory Usage Component
Row {
id: memoryUsageLayout
spacing: Style.marginXS * scaling
NText {
text: `${SystemStatService.cpuTemp}°C`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
}
NIcon {
text: "memory"
anchors.verticalCenter: parent.verticalCenter
}
// Memory Usage Component
Item {
Layout.preferredWidth: memoryUsageRow.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
visible: showMemoryUsage
NText {
text: `${SystemStatService.memoryUsageGb}G`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
RowLayout {
id: memoryUsageRow
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NIcon {
icon: "memory"
font.pointSize: Style.fontSizeM * scaling
Layout.alignment: Qt.AlignVCenter
}
// Network Download Speed Component
Row {
id: networkDownloadLayout
spacing: Style.marginXS * scaling
visible: Settings.data.bar.showNetworkStats
NText {
text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${SystemStatService.memGb}G`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
}
NIcon {
text: "download"
anchors.verticalCenter: parent.verticalCenter
}
// Network Download Speed Component
Item {
Layout.preferredWidth: networkDownloadRow.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
visible: showNetworkStats
NText {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
RowLayout {
id: networkDownloadRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NIcon {
icon: "download-speed"
font.pointSize: Style.fontSizeM * scaling
Layout.alignment: Qt.AlignVCenter
}
// Network Upload Speed Component
Row {
id: networkUploadLayout
spacing: Style.marginXS * scaling
visible: Settings.data.bar.showNetworkStats
NText {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
}
NIcon {
text: "upload"
anchors.verticalCenter: parent.verticalCenter
}
// Network Upload Speed Component
Item {
Layout.preferredWidth: networkUploadRow.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
visible: showNetworkStats
NText {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
RowLayout {
id: networkUploadRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NIcon {
icon: "upload-speed"
font.pointSize: Style.fontSizeM * scaling
Layout.alignment: Qt.AlignVCenter
}
NText {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
}
// Disk Usage Component (primary drive)
Item {
Layout.preferredWidth: diskUsageRow.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
visible: showDiskUsage
RowLayout {
id: diskUsageRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NIcon {
icon: "storage"
font.pointSize: Style.fontSizeM * scaling
Layout.alignment: Qt.AlignVCenter
}
NText {
text: `${SystemStatService.diskPercent}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
}
}
// Vertical layout for left/right bars
ColumnLayout {
id: verticalLayout
anchors.centerIn: parent
anchors.topMargin: Style.marginS * scaling
anchors.bottomMargin: Style.marginS * scaling
width: Math.round(28 * scaling)
spacing: Style.marginS * scaling
visible: barPosition === "left" || barPosition === "right"
// CPU Usage Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(28 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showCpuUsage
Column {
id: cpuUsageRowVertical
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NText {
text: `${Math.round(SystemStatService.cpuUsage)}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
NIcon {
icon: "cpu-usage"
font.pointSize: Style.fontSizeS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
// CPU Temperature Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(28 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showCpuTemp
Column {
id: cpuTempRowVertical
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NText {
text: `${SystemStatService.cpuTemp}°`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
NIcon {
icon: "cpu-temperature"
// Fire is so tall, we need to make it smaller
font.pointSize: Style.fontSizeXS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
// Memory Usage Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(28 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showMemoryUsage
Column {
id: memoryUsageRowVertical
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NText {
text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${Math.round(SystemStatService.memGb)}G`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
NIcon {
icon: "memory"
font.pointSize: Style.fontSizeS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
// Network Download Speed Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(28 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showNetworkStats
Column {
id: networkDownloadRowVertical
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NIcon {
icon: "download-speed"
font.pointSize: Style.fontSizeS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
NText {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
// Network Upload Speed Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(28 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showNetworkStats
Column {
id: networkUploadRowVertical
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NIcon {
icon: "upload-speed"
font.pointSize: Style.fontSizeS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
NText {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
// Disk Usage Component (primary drive)
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(28 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showDiskUsage
ColumnLayout {
id: diskUsageRowVertical
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NIcon {
icon: "storage"
font.pointSize: Style.fontSizeS * scaling
Layout.alignment: Qt.AlignHCenter
}
NText {
text: `${SystemStatService.diskPercent}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}

View File

@@ -2,6 +2,7 @@ pragma ComponentBehavior
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
@@ -17,15 +18,14 @@ Rectangle {
readonly property real itemSize: Style.baseWidgetSize * 0.8 * scaling
// Always visible when there are toplevels
implicitWidth: taskbarRow.width + Style.marginM * scaling * 2
implicitWidth: taskbarLayout.implicitWidth + Style.marginM * scaling * 2
implicitHeight: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
Row {
id: taskbarRow
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
RowLayout {
id: taskbarLayout
anchors.centerIn: parent
spacing: Style.marginXXS * root.scaling
Repeater {
@@ -35,8 +35,10 @@ Rectangle {
required property Toplevel modelData
property Toplevel toplevel: modelData
property bool isActive: ToplevelManager.activeToplevel === modelData
width: root.itemSize
height: root.itemSize
Layout.preferredWidth: root.itemSize
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignCenter
Rectangle {
id: iconBackground
@@ -54,7 +56,7 @@ Rectangle {
anchors.centerIn: parent
width: Style.marginL * root.scaling
height: Style.marginL * root.scaling
source: Icons.iconForAppId(taskbarItem.modelData.appId)
source: AppIcons.iconForAppId(taskbarItem.modelData.appId)
smooth: true
}
}
@@ -89,7 +91,7 @@ Rectangle {
NTooltip {
id: taskbarTooltip
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App"
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App."
target: taskbarItem
positionAbove: Settings.data.bar.position === "bottom"
}

View File

@@ -15,34 +15,36 @@ Rectangle {
property ShellScreen screen
property real scaling: 1.0
readonly property real itemSize: 24 * scaling
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
function onLoaded() {
// When the widget is fully initialized with its props
// set the screen for the trayMenu
// When the widget is fully initialized with its props set the screen for the trayMenu
if (trayMenu.item) {
trayMenu.item.screen = screen
}
}
visible: SystemTray.items.values.length > 0
implicitWidth: tray.width + Style.marginM * scaling * 2
implicitHeight: Math.round(Style.capsuleHeight * scaling)
implicitWidth: isVertical ? Math.round(Style.capsuleHeight * scaling) : (trayFlow.implicitWidth + Style.marginS * scaling * 2)
implicitHeight: isVertical ? (trayFlow.implicitHeight + Style.marginS * scaling * 2) : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
Layout.alignment: Qt.AlignVCenter
Row {
id: tray
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
Flow {
id: trayFlow
anchors.centerIn: parent
spacing: Style.marginS * scaling
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
Repeater {
id: repeater
model: SystemTray.items
delegate: Item {
width: itemSize
height: itemSize
@@ -110,9 +112,21 @@ Rectangle {
if (modelData.hasMenu && modelData.menu && trayMenu.item) {
trayPanel.open()
// Anchor the menu to the tray icon item (parent) and position it below the icon
const menuX = (width / 2) - (trayMenu.item.width / 2)
const menuY = Math.round(Style.barHeight * scaling)
// Position menu based on bar position
let menuX, menuY
if (barPosition === "left") {
// For left bar: position menu to the right of the bar
menuX = width + Style.marginM * scaling
menuY = 0
} else if (barPosition === "right") {
// For right bar: position menu to the left of the bar
menuX = -trayMenu.item.width - Style.marginM * scaling
menuY = 0
} else {
// For horizontal bars: center horizontally and position below
menuX = (width / 2) - (trayMenu.item.width / 2)
menuY = Math.round(Style.barHeight * scaling)
}
trayMenu.item.menu = modelData.menu
trayMenu.item.showAt(parent, menuX, menuY)
} else {
@@ -146,13 +160,14 @@ Rectangle {
function open() {
visible = true
PanelService.willOpenPanel(trayPanel)
}
function close() {
visible = false
trayMenu.item.hideMenu()
if (trayMenu.item) {
trayMenu.item.hideMenu()
}
}
// Clicking outside of the rectangle to close

View File

@@ -12,10 +12,26 @@ Item {
property ShellScreen screen
property real scaling: 1.0
property string barSection: ""
property int sectionWidgetIndex: 0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool alwaysShowPercentage: (widgetSettings.alwaysShowPercentage !== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
// Used to avoid opening the pill on Quickshell startup
property bool firstVolumeReceived: false
property int wheelAccumulator: 0
@@ -25,9 +41,9 @@ Item {
function getIcon() {
if (AudioService.muted) {
return "volume_off"
return "volume-mute"
}
return AudioService.volume <= Number.EPSILON ? "volume_off" : (AudioService.volume < 0.33 ? "volume_down" : "volume_up")
return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"
}
// Connection used to open the pill when volume changes
@@ -59,12 +75,10 @@ Item {
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: getIcon()
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.volume * 100) + "%"
tooltipText: "Volume: " + Math.round(
AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume."
forceOpen: alwaysShowPercentage
tooltipText: "Volume: " + Math.round(AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."
onWheel: function (delta) {
wheelAccumulator += delta
@@ -77,18 +91,15 @@ Item {
}
}
onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Audio
settingsPanel.open(screen)
AudioService.setOutputMuted(!AudioService.muted)
}
onRightClicked: {
pwvucontrolProcess.running = true
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Audio
settingsPanel.open()
}
onMiddleClicked: {
Quickshell.execDetached(["pwvucontrol"])
}
}
Process {
id: pwvucontrolProcess
command: ["pwvucontrol"]
running: false
}
}

View File

@@ -13,18 +13,8 @@ NIconButton {
property ShellScreen screen
property real scaling: 1.0
visible: Settings.data.network.wifiEnabled
sizeRatio: 0.8
Component.onCompleted: {
Logger.log("WiFi", "Widget component completed")
Logger.log("WiFi", "NetworkService available:", !!NetworkService)
if (NetworkService) {
Logger.log("WiFi", "NetworkService.networks available:", !!NetworkService.networks)
}
}
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
@@ -32,8 +22,8 @@ NIconButton {
icon: {
try {
if (NetworkService.ethernet) {
return "lan"
if (NetworkService.ethernetConnected) {
return "ethernet"
}
let connected = false
let signalStrength = 0
@@ -44,12 +34,13 @@ NIconButton {
break
}
}
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off"
} catch (error) {
Logger.error("WiFi", "Error getting icon:", error)
Logger.error("Wi-Fi", "Error getting icon:", error)
return "signal_wifi_bad"
}
}
tooltipText: "Network / Wi-Fi"
onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this)
tooltipText: "Manage Wi-Fi."
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
}

View File

@@ -11,9 +11,31 @@ import qs.Services
Item {
id: root
property ShellScreen screen: null
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property string barPosition: Settings.data.bar.position
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied
property bool isDestroying: false
property bool hovered: false
@@ -22,22 +44,13 @@ Item {
property bool effectsActive: false
property color effectColor: Color.mPrimary
property int horizontalPadding: Math.round(16 * scaling)
property int spacingBetweenPills: Math.round(8 * scaling)
property int horizontalPadding: Math.round(Style.marginS * scaling)
property int spacingBetweenPills: Math.round(Style.marginXS * scaling)
signal workspaceChanged(int workspaceId, color accentColor)
implicitHeight: Math.round(Style.barHeight * scaling)
implicitWidth: {
let total = 0
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
total += calculatedWsWidth(ws)
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return total
}
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.barHeight * scaling) : calculatedHorizontalWidth()
function calculatedWsWidth(ws) {
if (ws.isFocused)
@@ -48,6 +61,37 @@ Item {
return Math.round(20 * scaling)
}
function calculatedWsHeight(ws) {
if (ws.isFocused)
return Math.round(44 * scaling)
else if (ws.isActive)
return Math.round(28 * scaling)
else
return Math.round(20 * scaling)
}
function calculatedVerticalHeight() {
let total = 0
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
total += calculatedWsHeight(ws)
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return total
}
function calculatedHorizontalWidth() {
let total = 0
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
total += calculatedWsWidth(ws)
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return total
}
Component.onCompleted: {
refreshWorkspaces()
}
@@ -57,6 +101,7 @@ Item {
}
onScreenChanged: refreshWorkspaces()
onHideUnoccupiedChanged: refreshWorkspaces()
Connections {
target: WorkspaceService
@@ -71,11 +116,15 @@ Item {
for (var i = 0; i < WorkspaceService.workspaces.count; i++) {
const ws = WorkspaceService.workspaces.get(i)
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) {
continue
}
localWorkspaces.append(ws)
}
}
}
workspaceRepeater.model = localWorkspaces
workspaceRepeaterHorizontal.model = localWorkspaces
workspaceRepeaterVertical.model = localWorkspaces
updateWorkspaceFocus()
}
@@ -124,35 +173,30 @@ Item {
Rectangle {
id: workspaceBackground
width: parent.width - Style.marginS * scaling * 2
height: Math.round(Style.capsuleHeight * scaling)
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : parent.width
height: (barPosition === "left" || barPosition === "right") ? parent.height : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
layer.enabled: true
layer.effect: MultiEffect {
shadowColor: Color.mShadow
shadowVerticalOffset: 0
shadowHorizontalOffset: 0
shadowOpacity: 0.10
}
}
// Horizontal layout for top/bottom bars
Row {
id: pillRow
spacing: spacingBetweenPills
anchors.verticalCenter: workspaceBackground.verticalCenter
width: root.width - horizontalPadding * 2
x: horizontalPadding
visible: barPosition === "top" || barPosition === "bottom"
Repeater {
id: workspaceRepeater
id: workspaceRepeaterHorizontal
model: localWorkspaces
Item {
id: workspacePillContainer
height: (Settings.data.bar.showWorkspaceLabel !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling)
height: (labelMode !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling)
width: root.calculatedWsWidth(model)
Rectangle {
@@ -160,15 +204,13 @@ Item {
anchors.fill: parent
Loader {
active: (Settings.data.bar.showWorkspaceLabel !== "none")
active: (labelMode !== "none")
sourceComponent: Component {
Text {
// Center horizontally
x: (pill.width - width) / 2
// Center vertically accounting for font metrics
y: (pill.height - height) / 2 + (height - contentHeight) / 2
text: {
if (Settings.data.bar.showWorkspaceLabel === "name" && model.name && model.name.length > 0) {
if (labelMode === "name" && model.name && model.name.length > 0) {
return model.name.substring(0, 2)
} else {
return model.idx.toString()
@@ -186,8 +228,6 @@ Item {
return Color.mOnError
if (model.isActive || model.isOccupied)
return Color.mOnSecondary
if (model.isUrgent)
return Color.mOnError
return Color.mOnSurface
}
@@ -203,8 +243,6 @@ Item {
return Color.mError
if (model.isActive || model.isOccupied)
return Color.mSecondary
if (model.isUrgent)
return Color.mError
return Color.mOutline
}
@@ -288,4 +326,149 @@ Item {
}
}
}
// Vertical layout for left/right bars
Column {
id: pillColumn
spacing: spacingBetweenPills
anchors.horizontalCenter: workspaceBackground.horizontalCenter
height: root.height - horizontalPadding * 2
y: horizontalPadding
visible: barPosition === "left" || barPosition === "right"
Repeater {
id: workspaceRepeaterVertical
model: localWorkspaces
Item {
id: workspacePillContainerVertical
width: (labelMode !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling)
height: root.calculatedWsHeight(model)
Rectangle {
id: pillVertical
anchors.fill: parent
Loader {
active: (labelMode !== "none")
sourceComponent: Component {
Text {
x: (pillVertical.width - width) / 2
y: (pillVertical.height - height) / 2 + (height - contentHeight) / 2
text: {
if (labelMode === "name" && model.name && model.name.length > 0) {
return model.name.substring(0, 2)
} else {
return model.idx.toString()
}
}
font.pointSize: model.isFocused ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
font.capitalization: Font.AllUppercase
font.family: Settings.data.ui.fontFixed
font.weight: Style.fontWeightBold
wrapMode: Text.Wrap
color: {
if (model.isFocused)
return Color.mOnPrimary
if (model.isUrgent)
return Color.mOnError
if (model.isActive || model.isOccupied)
return Color.mOnSecondary
return Color.mOnSurface
}
}
}
}
radius: width * 0.5
color: {
if (model.isFocused)
return Color.mPrimary
if (model.isUrgent)
return Color.mError
if (model.isActive || model.isOccupied)
return Color.mSecondary
return Color.mOutline
}
scale: model.isFocused ? 1.0 : 0.9
z: 0
MouseArea {
id: pillMouseAreaVertical
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
WorkspaceService.switchToWorkspace(model.idx)
}
hoverEnabled: true
}
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
Behavior on radius {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
}
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
// Burst effect overlay for focused pill (smaller outline)
Rectangle {
id: pillBurstVertical
anchors.centerIn: workspacePillContainerVertical
width: workspacePillContainerVertical.width + 18 * root.masterProgress * scale
height: workspacePillContainerVertical.height + 18 * root.masterProgress * scale
radius: width / 2
color: Color.transparent
border.color: root.effectColor
border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * scaling))
opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0
visible: root.effectsActive && model.isFocused
z: 1
}
}
}
}
}

View File

@@ -36,65 +36,39 @@ ColumnLayout {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Rectangle {
id: bluetoothDeviceRectangle
property bool canConnect: BluetoothService.canConnect(modelData)
property bool canDisconnect: BluetoothService.canDisconnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
id: device
Layout.fillWidth: true
Layout.preferredHeight: 64 * scaling + (10 * scaling * modelData.batteryAvailable)
radius: Style.radiusM * scaling
color: {
if (availableDeviceArea.containsMouse) {
if (canDisconnect && !isBusy)
return Color.mError
if (!isBusy)
return Color.mTertiary
return Color.mPrimary
}
readonly property bool canConnect: BluetoothService.canConnect(modelData)
readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData)
readonly property bool isBusy: BluetoothService.isDeviceBusy(modelData)
function getContentColor(defaultColor = Color.mOnSurface) {
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary
if (modelData.blocked)
return Color.mError
return Color.mSurfaceVariant
return defaultColor
}
border.color: Color.mOutline
Layout.fillWidth: true
Layout.preferredHeight: deviceLayout.implicitHeight + (Style.marginM * scaling * 2)
radius: Style.radiusM * scaling
color: Color.mSurface
border.width: Math.max(1, Style.borderS * scaling)
NTooltip {
id: tooltip
target: bluetoothDeviceRectangle
positionAbove: Settings.data.bar.position === "bottom"
text: root.tooltipText
}
border.color: getContentColor(Color.mOutline)
RowLayout {
id: deviceLayout
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginS * scaling
spacing: Style.marginM * scaling
Layout.alignment: Qt.AlignVCenter
// One device BT icon
NIcon {
text: BluetoothService.getDeviceIcon(modelData)
icon: BluetoothService.getDeviceIcon(modelData)
font.pointSize: Style.fontSizeXXL * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
color: getContentColor(Color.mOnSurface)
Layout.alignment: Qt.AlignVCenter
}
@@ -106,25 +80,23 @@ ColumnLayout {
NText {
text: modelData.name || modelData.deviceName
font.pointSize: Style.fontSizeM * scaling
elide: Text.ElideRight
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
font.weight: Style.fontWeightMedium
elide: Text.ElideRight
color: getContentColor(Color.mOnSurface)
Layout.fillWidth: true
}
// Status
NText {
text: BluetoothService.getStatusString(modelData)
visible: text !== ""
font.pointSize: Style.fontSizeXS * scaling
color: getContentColor(Color.mOnSurfaceVariant)
}
// Signal Strength
RowLayout {
visible: modelData.signalStrength !== undefined
Layout.fillWidth: true
spacing: Style.marginXS * scaling
@@ -132,76 +104,30 @@ ColumnLayout {
NText {
text: BluetoothService.getSignalStrength(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurfaceVariant
}
color: getContentColor(Color.mOnSurfaceVariant)
}
NIcon {
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
text: BluetoothService.getSignalIcon(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing
&& !modelData.blocked
color: getContentColor(Color.mOnSurface)
}
NText {
text: (modelData.signalStrength !== undefined
&& modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing
&& !modelData.blocked
color: getContentColor(Color.mOnSurface)
}
}
// Battery
NText {
visible: modelData.batteryAvailable
text: BluetoothService.getBattery(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurfaceVariant
}
color: getContentColor(Color.mOnSurfaceVariant)
}
}
@@ -211,96 +137,85 @@ ColumnLayout {
}
// Call to action
Rectangle {
Layout.preferredWidth: 80 * scaling
Layout.preferredHeight: 28 * scaling
radius: Style.radiusM * scaling
NButton {
id: button
visible: (modelData.state !== BluetoothDeviceState.Connecting)
color: Color.transparent
border.color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
}
if (bluetoothDeviceRectangle.canDisconnect && !isBusy) {
enabled: (canConnect || canDisconnect) && !isBusy
outlined: !button.hovered
fontSize: Style.fontSizeXS * scaling
fontWeight: Style.fontWeightMedium
backgroundColor: {
if (device.canDisconnect && !isBusy) {
return Color.mError
}
return Color.mPrimary
}
border.width: Math.max(1, Style.borderS * scaling)
opacity: canConnect || isBusy || canDisconnect ? 1 : 0.5
NText {
anchors.centerIn: parent
text: {
if (modelData.pairing) {
return "Pairing..."
}
if (modelData.blocked) {
return "Blocked"
}
if (modelData.connected) {
return "Disconnect"
}
return "Connect"
tooltipText: root.tooltipText
text: {
if (modelData.pairing) {
return "Pairing..."
}
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
}
if (bluetoothDeviceRectangle.canDisconnect && !isBusy) {
return Color.mError
} else {
return Color.mPrimary
}
if (modelData.blocked) {
return "Blocked"
}
if (modelData.connected) {
return "Disconnect"
}
return "Connect"
}
}
}
MouseArea {
id: availableDeviceArea
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
hoverEnabled: true
cursorShape: (canConnect || canDisconnect)
&& !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
onEntered: {
if (root.tooltipText && !isBusy) {
tooltip.show()
}
}
onExited: {
if (root.tooltipText && !isBusy) {
tooltip.hide()
}
}
onClicked: function (mouse) {
if (!modelData || modelData.pairing) {
return
}
if (root.tooltipText && !isBusy) {
tooltip.hide()
}
if (mouse.button === Qt.LeftButton) {
icon: (isBusy ? "hourglass-split" : null)
onClicked: {
if (modelData.connected) {
BluetoothService.disconnectDevice(modelData)
} else {
BluetoothService.connectDeviceWithTrust(modelData)
}
} else if (mouse.button === Qt.RightButton) {
}
onRightClicked: {
BluetoothService.forgetDevice(modelData)
}
}
}
// MouseArea {
// id: availableDeviceArea
// acceptedButtons: Qt.LeftButton | Qt.RightButton
// anchors.fill: parent
// hoverEnabled: true
// cursorShape: (canConnect || canDisconnect)
// && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
// onEntered: {
// if (root.tooltipText && !isBusy) {
// tooltip.show()
// }
// }
// onExited: {
// if (root.tooltipText && !isBusy) {
// tooltip.hide()
// }
// }
// onClicked: function (mouse) {
// if (!modelData || modelData.pairing) {
// return
// }
// if (root.tooltipText && !isBusy) {
// tooltip.hide()
// }
// if (mouse.button === Qt.LeftButton) {
// if (modelData.connected) {
// BluetoothService.disconnectDevice(modelData)
// } else {
// BluetoothService.connectDeviceWithTrust(modelData)
// }
// } else if (mouse.button === Qt.RightButton) {
// BluetoothService.forgetDevice(modelData)
// }
// }
// }
}
}
}

View File

@@ -11,9 +11,9 @@ import qs.Widgets
NPanel {
id: root
panelWidth: 380 * scaling
panelHeight: 500 * scaling
panelAnchorRight: true
preferredWidth: 380
preferredHeight: 500
panelKeyboardFocus: true
panelContent: Rectangle {
color: Color.transparent
@@ -29,7 +29,7 @@ NPanel {
spacing: Style.marginM * scaling
NIcon {
text: "bluetooth"
icon: "bluetooth"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
@@ -42,8 +42,16 @@ NPanel {
Layout.fillWidth: true
}
NToggle {
id: bluetoothSwitch
checked: Settings.data.network.bluetoothEnabled
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
baseSize: Style.baseWidgetSize * 0.65 * scaling
}
NIconButton {
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop_circle" : "refresh"
enabled: Settings.data.network.bluetoothEnabled
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "refresh"
tooltipText: "Refresh Devices"
sizeRatio: 0.8
onClicked: {
@@ -55,7 +63,7 @@ NPanel {
NIconButton {
icon: "close"
tooltipText: "Close"
tooltipText: "Close."
sizeRatio: 0.8
onClicked: {
root.close()
@@ -67,16 +75,50 @@ NPanel {
Layout.fillWidth: true
}
ScrollView {
Rectangle {
visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled)
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
color: Color.transparent
// Center the content within this rectangle
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
icon: "bluetooth-off"
font.pointSize: 64 * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Bluetooth is disabled"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Enable Bluetooth to see available devices."
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
}
NScrollView {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
ColumnLayout {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
width: parent.width
spacing: Style.marginM * scaling
@@ -97,12 +139,11 @@ NPanel {
// Known devices
BluetoothDevicesList {
label: "Known devices"
tooltipText: "Left click to connect, right click to forget"
tooltipText: "Left click to connect.\nRight click to forget."
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected
&& (dev.paired || dev.trusted))
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted))
return BluetoothService.sortDevices(filtered)
}
model: items
@@ -124,9 +165,9 @@ NPanel {
Layout.fillWidth: true
}
// Fallback
// Fallback - No devices, scanning
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM * scaling
visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) {
@@ -134,21 +175,18 @@ NPanel {
}
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing
&& !dev.blocked
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0)
}).length
return (availableCount === 0)
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM * scaling
spacing: Style.marginXS * scaling
NIcon {
text: "sync"
font.pointSize: Style.fontSizeXLL * 1.5 * scaling
icon: "refresh"
font.pointSize: Style.fontSizeXXL * 1.5 * scaling
color: Color.mPrimary
RotationAnimation on rotation {
@@ -164,12 +202,11 @@ NPanel {
text: "Scanning for devices..."
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
font.weight: Style.fontWeightMedium
}
}
NText {
text: "Make sure your device is in pairing mode"
text: "Make sure your device is in pairing mode."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter

View File

@@ -10,9 +10,9 @@ import qs.Widgets
NPanel {
id: root
panelWidth: 340 * scaling
panelHeight: 320 * scaling
panelAnchorRight: true
preferredWidth: 340
preferredHeight: 320
panelAnchorRight: Settings.data.bar.position === "right"
// Main Column
panelContent: ColumnLayout {
@@ -28,7 +28,7 @@ NPanel {
spacing: Style.marginS * scaling
NIconButton {
icon: "chevron_left"
icon: "chevron-left"
tooltipText: "Previous month"
onClicked: {
let newDate = new Date(grid.year, grid.month - 1, 1)
@@ -47,7 +47,7 @@ NPanel {
}
NIconButton {
icon: "chevron_right"
icon: "chevron-right"
tooltipText: "Next month"
onClicked: {
let newDate = new Date(grid.year, grid.month + 1, 1)

View File

@@ -12,10 +12,10 @@ import qs.Widgets
Variants {
model: Quickshell.screens
delegate: Loader {
delegate: Item {
required property ShellScreen modelData
property real scaling: ScalingService.getScreenScale(modelData)
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
@@ -25,312 +25,370 @@ Variants {
}
}
active: Settings.isLoaded && modelData ? Settings.data.dock.monitors.includes(modelData.name) : false
// Shared properties between peek and dock windows
readonly property bool autoHide: Settings.data.dock.autoHide
readonly property int hideDelay: 500
readonly property int showDelay: 100
readonly property int hideAnimationDuration: Style.animationFast
readonly property int showAnimationDuration: Style.animationFast
readonly property int peekHeight: 1 // no scaling for peek
readonly property int iconSize: 36 * scaling
readonly property int floatingMargin: Settings.data.dock.floatingRatio * Style.marginL * scaling
sourceComponent: PanelWindow {
id: dockWindow
// Bar detection and positioning properties
readonly property bool hasBar: modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
readonly property bool barAtBottom: hasBar && Settings.data.bar.position === "bottom"
readonly property int barHeight: Style.barHeight * scaling
screen: modelData
// Shared state between windows
property bool dockHovered: false
property bool anyAppHovered: false
property bool hidden: autoHide
property bool peekHovered: false
property bool autoHide: Settings.data.dock.autoHide
property bool hidden: autoHide
property int hideDelay: 500
property int showDelay: 100
property int hideAnimationDuration: Style.animationFast
property int showAnimationDuration: Style.animationFast
property int peekHeight: 7 * scaling
property int fullHeight: dockContainer.height
property int iconSize: 36
// Separate property to control Loader - stays true during animations
property bool dockLoaded: !autoHide // Start loaded if autoHide is off
// Bar positioning properties
property bool barAtBottom: Settings.data.bar.position === "bottom"
property int barHeight: barAtBottom ? (Settings.data.bar.height || 30) * scaling : 0
property int dockSpacing: 4 * scaling // Space between dock and bar
// Timer to unload dock after hide animation completes
Timer {
id: unloadTimer
interval: hideAnimationDuration + 50 // Add small buffer
onTriggered: {
if (hidden && autoHide) {
dockLoaded = false
}
}
}
// Track hover state
property bool dockHovered: false
property bool anyAppHovered: false
// Dock is positioned at the bottom
anchors.bottom: true
// Dock should be above bar but not create its own exclusion zone
exclusionMode: ExclusionMode.Ignore
focusable: false
// Make the window transparent
color: Color.transparent
// Set the window size - always include space for peek area when auto-hide is enabled
implicitWidth: dockContainer.width
implicitHeight: fullHeight + (barAtBottom ? barHeight + dockSpacing : 0)
// Position the entire window above the bar when bar is at bottom
margins.bottom: barAtBottom ? barHeight : 0
// Watch for autoHide setting changes
onAutoHideChanged: {
if (!autoHide) {
// If auto-hide is disabled, show the dock
hidden = false
hideTimer.stop()
showTimer.stop()
} else {
// If auto-hide is enabled, start hidden
// Timer for auto-hide delay
Timer {
id: hideTimer
interval: hideDelay
onTriggered: {
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
hidden = true
unloadTimer.restart() // Start unload timer when hiding
}
}
}
// Timer for auto-hide delay
Timer {
id: hideTimer
interval: hideDelay
onTriggered: {
if (autoHide && !dockHovered && !anyAppHovered && !peekArea.containsMouse) {
hidden = true
// Timer for show delay
Timer {
id: showTimer
interval: showDelay
onTriggered: {
if (autoHide) {
dockLoaded = true // Load dock immediately
hidden = false // Then trigger show animation
unloadTimer.stop() // Cancel any pending unload
}
}
}
// Watch for autoHide setting changes
onAutoHideChanged: {
if (!autoHide) {
hidden = false
dockLoaded = true
hideTimer.stop()
showTimer.stop()
unloadTimer.stop()
} else {
hidden = true
unloadTimer.restart() // Schedule unload after animation
}
}
// PEEK WINDOW - Always visible when auto-hide is enabled
Loader {
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && autoHide
sourceComponent: PanelWindow {
id: peekWindow
screen: modelData
anchors.bottom: true
anchors.left: true
anchors.right: true
focusable: false
color: Color.transparent
WlrLayershell.namespace: "noctalia-dock-peek"
WlrLayershell.exclusionMode: ExclusionMode.Auto // Always exclusive
implicitHeight: peekHeight
Rectangle {
anchors.fill: parent
color: barAtBottom ? Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) : Color.transparent
}
MouseArea {
id: peekArea
anchors.fill: parent
hoverEnabled: true
onEntered: {
peekHovered = true
if (hidden) {
showTimer.start()
}
}
onExited: {
peekHovered = false
if (!hidden && !dockHovered && !anyAppHovered) {
hideTimer.restart()
}
}
}
}
}
// Timer for show delay
Timer {
id: showTimer
interval: showDelay
onTriggered: {
if (autoHide) {
hidden = false
}
}
}
// DOCK WINDOW
Loader {
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (ToplevelManager.toplevels.values.length > 0)
// Peek area that remains visible when dock is hidden
MouseArea {
id: peekArea
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: peekHeight + dockSpacing
hoverEnabled: autoHide
visible: autoHide
sourceComponent: PanelWindow {
id: dockWindow
onEntered: {
if (autoHide && hidden) {
showTimer.start()
screen: modelData
focusable: false
color: Color.transparent
WlrLayershell.namespace: "noctalia-dock-main"
WlrLayershell.exclusionMode: Settings.data.dock.exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore
// Size to fit the dock container exactly
implicitWidth: dockContainerWrapper.width
implicitHeight: dockContainerWrapper.height
// Position above the bar if it's at bottom
anchors.bottom: true
margins.bottom: {
switch (Settings.data.bar.position) {
case "bottom":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling + floatingMargin : floatingMargin)
default:
return floatingMargin
}
}
onExited: {
if (autoHide && !hidden && !dockHovered && !anyAppHovered) {
hideTimer.restart()
}
}
}
// Rectangle {
// anchors.fill: parent
// color: "#000FF0"
// z: -1
// }
Rectangle {
id: dockContainer
width: dock.width + 48 * scaling
height: iconSize * 1.4 * scaling
color: Color.mSurface
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: dockSpacing
topLeftRadius: Style.radiusL * scaling
topRightRadius: Style.radiusL * scaling
// Wrapper item for scale/opacity animations
Item {
id: dockContainerWrapper
width: dockContainer.width
height: dockContainer.height
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
// Animate the dock sliding up and down
transform: Translate {
y: hidden ? (fullHeight - peekHeight) : 0
// Apply animations to this wrapper
opacity: hidden ? 0 : 1
scale: hidden ? 0.85 : 1
Behavior on y {
Behavior on opacity {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: Easing.InOutQuad
}
}
}
// Drop shadow for better visibility when bar is transparent
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowBlur: 0.5
shadowVerticalOffset: 2
shadowHorizontalOffset: 0
}
Behavior on scale {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: hidden ? Easing.InQuad : Easing.OutBack
easing.overshoot: hidden ? 0 : 1.05
}
}
MouseArea {
id: dockMouseArea
anchors.fill: parent
hoverEnabled: true
Rectangle {
id: dockContainer
width: dockLayout.implicitWidth + Style.marginM * scaling * 2
height: Math.round(iconSize * 1.5)
color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
anchors.centerIn: parent
radius: Style.radiusL * scaling
border.width: Math.max(1, Style.borderS * scaling)
border.color: Qt.alpha(Color.mOutline, Settings.data.dock.backgroundOpacity)
onEntered: {
dockHovered = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
if (hidden) {
hidden = false
MouseArea {
id: dockMouseArea
anchors.fill: parent
hoverEnabled: true
onEntered: {
dockHovered = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
unloadTimer.stop() // Cancel unload if hovering
}
}
onExited: {
dockHovered = false
if (autoHide && !anyAppHovered && !peekHovered) {
hideTimer.restart()
}
}
}
}
onExited: {
dockHovered = false
// Only start hide timer if we're not hovering over any app or the peek area
if (autoHide && !anyAppHovered && !peekArea.containsMouse) {
hideTimer.restart()
}
}
}
Item {
id: dock
width: dockLayout.implicitWidth
height: parent.height - (Style.marginM * 2 * scaling)
anchors.centerIn: parent
Item {
id: dock
width: runningAppsRow.width
height: parent.height - (20 * scaling)
anchors.centerIn: parent
function getAppIcon(toplevel: Toplevel): string {
if (!toplevel)
return ""
return AppIcons.iconForAppId(toplevel.appId?.toLowerCase())
}
NTooltip {
id: appTooltip
visible: false
positionAbove: true
}
RowLayout {
id: dockLayout
spacing: Style.marginM * scaling
Layout.preferredHeight: parent.height
anchors.centerIn: parent
function getAppIcon(toplevel: Toplevel): string {
if (!toplevel)
return ""
return Icons.iconForAppId(toplevel.appId?.toLowerCase())
}
Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null
Row {
id: runningAppsRow
spacing: Style.marginL * scaling
height: parent.height
anchors.centerIn: parent
delegate: Item {
id: appButton
Layout.preferredWidth: iconSize
Layout.preferredHeight: iconSize
Layout.alignment: Qt.AlignCenter
Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
property bool hovered: appMouseArea.containsMouse
property string appId: modelData ? modelData.appId : ""
property string appTitle: modelData ? modelData.title : ""
delegate: Rectangle {
id: appButton
width: iconSize * scaling
height: iconSize * scaling
color: Color.transparent
radius: Style.radiusM * scaling
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
property bool hovered: appMouseArea.containsMouse
property string appId: modelData ? modelData.appId : ""
property string appTitle: modelData ? modelData.title : ""
// Hover background
Rectangle {
id: hoverBackground
anchors.fill: parent
color: appButton.hovered ? Color.mSurfaceVariant : Color.transparent
radius: parent.radius
opacity: appButton.hovered ? 0.8 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
// Individual tooltip for this app
NTooltip {
id: appTooltip
target: appButton
positionAbove: true
visible: false
}
}
}
// The icon
Image {
id: appIcon
width: iconSize * scaling
height: iconSize * scaling
anchors.centerIn: parent
source: dock.getAppIcon(modelData)
visible: source.toString() !== ""
smooth: true
mipmap: false
antialiasing: false
fillMode: Image.PreserveAspectFit
Image {
id: appIcon
width: iconSize
height: iconSize
anchors.centerIn: parent
source: dock.getAppIcon(modelData)
visible: source.toString() !== ""
sourceSize.width: iconSize * 2
sourceSize.height: iconSize * 2
smooth: true
mipmap: true
antialiasing: true
fillMode: Image.PreserveAspectFit
cache: true
scale: appButton.hovered ? 1.1 : 1.0
scale: appButton.hovered ? 1.15 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutBack
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
easing.overshoot: 1.2
}
}
}
}
}
// Fall back if no icon
NText {
anchors.centerIn: parent
visible: !appIcon.visible
text: "question_mark"
font.family: "Material Symbols Rounded"
font.pointSize: iconSize * 0.7 * scaling
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
// Fall back if no icon
NIcon {
anchors.centerIn: parent
visible: !appIcon.visible
icon: "question-mark"
font.pointSize: iconSize * 0.7
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
scale: appButton.hovered ? 1.15 : 1.0
scale: appButton.hovered ? 1.1 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutBack
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutBack
easing.overshoot: 1.2
}
}
}
}
}
MouseArea {
id: appMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
MouseArea {
id: appMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
onEntered: {
anyAppHovered = true
const appName = appButton.appTitle || appButton.appId || "Unknown"
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
appTooltip.target = appButton
appTooltip.isVisible = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
if (hidden) {
hidden = false
onEntered: {
anyAppHovered = true
const appName = appButton.appTitle || appButton.appId || "Unknown"
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
appTooltip.isVisible = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
unloadTimer.stop() // Cancel unload if hovering app
}
}
onExited: {
anyAppHovered = false
appTooltip.hide()
if (autoHide && !dockHovered && !peekHovered) {
hideTimer.restart()
}
}
onClicked: function (mouse) {
if (mouse.button === Qt.MiddleButton && modelData?.close) {
modelData.close()
}
if (mouse.button === Qt.LeftButton && modelData?.activate) {
modelData.activate()
}
}
}
// Active indicator
Rectangle {
visible: isActive
width: iconSize * 0.2
height: iconSize * 0.1
color: Color.mPrimary
radius: Style.radiusXS * scaling
anchors.top: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
// Pulse animation for active indicator
SequentialAnimation on opacity {
running: isActive
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: Style.animationSlowest
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: Style.animationSlowest
easing.type: Easing.InOutQuad
}
}
}
}
onExited: {
anyAppHovered = false
appTooltip.hide()
// Only start hide timer if we're not hovering over the dock or peek area
if (autoHide && !dockHovered && !peekArea.containsMouse) {
hideTimer.restart()
}
}
onClicked: function (mouse) {
if (mouse.button === Qt.MiddleButton && modelData?.close) {
modelData.close()
}
if (mouse.button === Qt.LeftButton && modelData?.activate) {
modelData.activate()
}
}
}
Rectangle {
visible: isActive
width: iconSize * 0.75
height: 4 * scaling
color: Color.mPrimary
radius: Style.radiusXS
anchors.top: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Style.marginXXS * scaling
}
}
}

View File

@@ -8,37 +8,29 @@ import qs.Services
Item {
id: root
// Using Wayland protocols to get focused window then determine which screen it's on.
function getActiveScreen() {
const activeWindow = ToplevelManager.activeToplevel
if (activeWindow && activeWindow.screens.length > 0) {
return activeWindow.screens[0]
}
// Fall back to the primary screen
return Quickshell.screens[0]
}
IpcHandler {
target: "screenRecorder"
function toggle() {
ScreenRecorderService.toggleRecording()
if (ScreenRecorderService.isAvailable) {
ScreenRecorderService.toggleRecording()
}
}
}
IpcHandler {
target: "settings"
function toggle() {
settingsPanel.toggle(getActiveScreen())
settingsPanel.toggle()
}
}
IpcHandler {
target: "notifications"
function toggleHistory() {
notificationHistoryPanel.toggle(getActiveScreen())
notificationHistoryPanel.toggle()
}
function toggleDoNotDisturb() {// TODO
function toggleDND() {
Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
}
}
@@ -52,15 +44,15 @@ Item {
IpcHandler {
target: "launcher"
function toggle() {
launcherPanel.toggle(getActiveScreen())
launcherPanel.toggle()
}
function clipboard() {
launcherPanel.setSearchText(">clip ")
launcherPanel.toggle(getActiveScreen())
launcherPanel.toggle()
}
function calculator() {
launcherPanel.setSearchText(">calc ")
launcherPanel.toggle(getActiveScreen())
launcherPanel.toggle()
}
}
@@ -107,7 +99,7 @@ Item {
AudioService.decreaseVolume()
}
function muteOutput() {
AudioService.setMuted(!AudioService.muted)
AudioService.setOutputMuted(!AudioService.muted)
}
function muteInput() {
if (AudioService.source?.ready && AudioService.source?.audio) {
@@ -119,14 +111,14 @@ Item {
IpcHandler {
target: "powerPanel"
function toggle() {
powerPanel.toggle(getActiveScreen())
powerPanel.toggle()
}
}
IpcHandler {
target: "sidePanel"
function toggle() {
sidePanel.toggle(getActiveScreen())
sidePanel.toggle()
}
}
@@ -138,5 +130,12 @@ Item {
WallpaperService.setRandomWallpaper()
}
}
function set(path: string, screen: string) {
if (screen === "all" || screen === "") {
screen = undefined
}
WallpaperService.changeWallpaper(path, screen)
}
}
}

View File

@@ -11,16 +11,10 @@ NPanel {
id: root
// Panel configuration
panelWidth: {
var w = Math.round(Math.max(screen?.width * 0.3, 500) * scaling)
w = Math.min(w, screen?.width - Style.marginL * 2)
return w
}
panelHeight: {
var h = Math.round(Math.max(screen?.height * 0.5, 600) * scaling)
h = Math.min(h, screen?.height - Style.barHeight * scaling - Style.marginL * 2)
return h
}
preferredWidth: 500
preferredWidthRatio: 0.3
preferredHeight: 600
preferredHeightRatio: 0.5
panelKeyboardFocus: true
panelBackgroundColor: Qt.alpha(Color.mSurface, Settings.data.appLauncher.backgroundOpacity)
@@ -202,73 +196,97 @@ NPanel {
}
}
Shortcut {
sequence: "Ctrl+K"
onActivated: ui.selectPrevious()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "Ctrl+J"
onActivated: ui.selectNext()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "PgDown" // or "PageDown"
onActivated: ui.selectNextPage()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "PgUp" // or "PageUp"
onActivated: ui.selectPreviousPage()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "Home"
onActivated: ui.selectFirst()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "End"
onActivated: ui.selectLast()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
Item {
id: searchInputWrap
NTextInput {
id: searchInput
Layout.fillWidth: true
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
NTextInput {
id: searchInput
anchors.fill: parent
inputMaxWidth: Number.MAX_SAFE_INTEGER
fontSize: Style.fontSizeL * scaling
fontWeight: Style.fontWeightSemiBold
fontSize: Style.fontSizeL * scaling
fontWeight: Style.fontWeightSemiBold
text: searchText
placeholderText: "Search entries... or use > for commands"
text: searchText
placeholderText: "Search entries... or use > for commands"
onTextChanged: searchText = text
Component.onCompleted: {
if (searchInput.inputItem && searchInput.inputItem.visible) {
searchInput.inputItem.forceActiveFocus()
}
Component.onCompleted: {
if (searchInput.inputItem && searchInput.inputItem.visible) {
searchInput.inputItem.forceActiveFocus()
// Override the TextField's default Home/End behavior
searchInput.inputItem.Keys.priority = Keys.BeforeItem
searchInput.inputItem.Keys.onPressed.connect(function (event) {
// Intercept Home and End BEFORE the TextField handles them
if (event.key === Qt.Key_Home) {
ui.selectFirst()
event.accepted = true
return
} else if (event.key === Qt.Key_End) {
ui.selectLast()
event.accepted = true
return
}
})
searchInput.inputItem.Keys.onDownPressed.connect(function (event) {
ui.selectNext()
})
searchInput.inputItem.Keys.onUpPressed.connect(function (event) {
ui.selectPrevious()
})
searchInput.inputItem.Keys.onReturnPressed.connect(function (event) {
ui.activate()
})
}
onTextChanged: searchText = text
Keys.onEscapePressed: root.close()
Keys.onReturnPressed: ui.activate()
Keys.onDownPressed: ui.selectNext()
Keys.onUpPressed: ui.selectPrevious()
Keys.onPressed: event => {
if (event.key === Qt.Key_PageDown) {
ui.selectNextPage()
event.accepted = true
} else if (event.key === Qt.Key_PageUp) {
ui.selectPreviousPage()
event.accepted = true
} else if (event.key === Qt.Key_Home) {
ui.selectFirst()
event.accepted = true
} else if (event.key === Qt.Key_End) {
ui.selectLast()
event.accepted = true
}
if (event.modifiers & Qt.ControlModifier) {
switch (event.key) {
case Qt.Key_K:
ui.selectPrevious()
event.accepted = true
break
case Qt.Key_J:
ui.selectNext()
event.accepted = true
break
}
}
}
}
}
// Results list
ListView {
NListView {
id: resultsList
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
Layout.fillWidth: true
Layout.fillHeight: true
spacing: Style.marginXXS * scaling
@@ -285,10 +303,6 @@ NPanel {
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
delegate: Rectangle {
id: entry
@@ -383,7 +397,7 @@ NPanel {
sourceComponent: Component {
IconImage {
anchors.fill: parent
source: modelData.icon ? Icons.iconFromName(modelData.icon, "application-x-executable") : ""
source: modelData.icon ? AppIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== ""
asynchronous: true
}
@@ -459,7 +473,7 @@ NPanel {
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedIndex = index
root.activate()
ui.activate()
}
}
}

View File

@@ -37,8 +37,7 @@ Item {
if (!query || query.trim() === "") {
// Return all apps alphabetically
return entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(
app => createResultEntry(app))
return entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(app => createResultEntry(app))
}
// Use fuzzy search if available, fallback to simple search
@@ -57,8 +56,7 @@ Item {
const name = (app.name || "").toLowerCase()
const comment = (app.comment || "").toLowerCase()
const generic = (app.genericName || "").toLowerCase()
return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(
searchTerm)
return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm)
}).sort((a, b) => {
// Prioritize name matches
const aName = a.name.toLowerCase()
@@ -82,7 +80,14 @@ Item {
"isImage": false,
"onActivate": function () {
Logger.log("ApplicationsPlugin", `Launching: ${app.name}`)
if (app.execute) {
if (Settings.data.appLauncher.useApp2Unit && app.id) {
Logger.log("ApplicationsPlugin", `Using app2unit for: ${app.id}`)
if (app.runInTerminal)
Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"])
else
Quickshell.execDetached(["app2unit", "--"].concat(app.command))
} else if (app.execute) {
app.execute()
} else if (app.exec) {
// Fallback to manual execution

View File

@@ -8,8 +8,7 @@ Item {
function handleCommand(query) {
// Handle >calc command or direct math expressions after >
return query.startsWith(">calc") || (query.startsWith(">") && query.length > 1 && isMathExpression(
query.substring(1)))
return query.startsWith(">calc") || (query.startsWith(">") && query.length > 1 && isMathExpression(query.substring(1)))
}
function commands() {

View File

@@ -136,7 +136,7 @@ Item {
const items = ClipboardService.items || []
// If no items and we haven't tried loading yet, trigger a load
if (items.length === 0 && !ClipboardService.loading) {
if (items.count === 0 && !ClipboardService.loading) {
isWaitingForData = true
ClipboardService.list(100)
return [{

View File

@@ -48,8 +48,7 @@ Scope {
user: Quickshell.env("USER")
onPamMessage: {
Logger.log("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:",
responseRequired)
Logger.log("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired)
if (messageIsError) {
errorMessage = message

View File

@@ -58,35 +58,11 @@ Loader {
property real percent: isReady ? (battery.percentage * 100) : 0
property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false
property bool batteryVisible: isReady && percent > 0
function getIcon() {
if (!batteryVisible)
return ""
if (charging)
return "battery_android_bolt"
if (percent >= 95)
return "battery_android_full"
if (percent >= 85)
return "battery_android_6"
if (percent >= 70)
return "battery_android_5"
if (percent >= 55)
return "battery_android_4"
if (percent >= 40)
return "battery_android_3"
if (percent >= 25)
return "battery_android_2"
if (percent >= 10)
return "battery_android_1"
if (percent >= 0)
return "battery_android_0"
}
}
Item {
id: keyboardLayout
property string currentLayout: (typeof KeyboardLayoutService !== 'undefined'
&& KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown"
property string currentLayout: (typeof KeyboardLayoutService !== 'undefined' && KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown"
}
Image {
@@ -99,14 +75,6 @@ Loader {
mipmap: false
}
Rectangle {
anchors.fill: parent
color: Color.transparent
layer.enabled: true
layer.smooth: true
layer.samples: 4
}
Rectangle {
anchors.fill: parent
gradient: Gradient {
@@ -163,7 +131,7 @@ Loader {
anchors.topMargin: 80 * scaling
spacing: 40 * scaling
Column {
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignHCenter
@@ -176,6 +144,7 @@ Loader {
font.letterSpacing: -2 * scaling
color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
SequentialAnimation on scale {
loops: Animation.Infinite
@@ -200,22 +169,23 @@ Loader {
font.weight: Font.Light
color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter
width: timeText.width
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: timeText.implicitWidth
}
}
Column {
ColumnLayout {
spacing: Style.marginM * scaling
Layout.alignment: Qt.AlignHCenter
Rectangle {
width: 108 * scaling
height: 108 * scaling
Layout.preferredWidth: 108 * scaling
Layout.preferredHeight: 108 * scaling
Layout.alignment: Qt.AlignHCenter
radius: width * 0.5
color: Color.transparent
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderL * scaling)
anchors.horizontalCenter: parent.horizontalCenter
z: 10
Loader {
@@ -256,12 +226,10 @@ Loader {
Repeater {
model: CavaService.values.length * 2
Rectangle {
property int mirroredValueIndex: index < CavaService.values.length ? index : (CavaService.values.length
* 2 - 1 - index)
property int mirroredValueIndex: index < CavaService.values.length ? index : (CavaService.values.length * 2 - 1 - index)
property real mirroredAngle: (index / (CavaService.values.length * 2)) * 2 * Math.PI
property real mirroredRadius: 70 * scaling
property real mirroredBarLength: Math.max(
2, CavaService.values[mirroredValueIndex] * 30 * scaling)
property real mirroredBarLength: Math.max(2, CavaService.values[mirroredValueIndex] * 30 * scaling)
property real mirroredBarWidth: 3 * scaling
width: mirroredBarWidth
height: mirroredBarLength
@@ -383,389 +351,410 @@ Loader {
anchors.centerIn: parent
anchors.verticalCenterOffset: 50 * scaling
Item {
width: parent.width
height: 280 * scaling
Layout.fillWidth: true
Rectangle {
id: terminalBackground
anchors.fill: parent
radius: Style.radiusM * scaling
color: Qt.alpha(Color.mSurface, 0.9)
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling)
Repeater {
model: 20
Rectangle {
width: parent.width
height: 1
color: Qt.alpha(Color.mPrimary, 0.1)
y: index * 10 * scaling
opacity: Style.opacityMedium
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: 2000 + Math.random() * 1000
}
NumberAnimation {
to: 0.1
duration: 2000 + Math.random() * 1000
}
}
}
}
Rectangle {
id: terminalBackground
anchors.fill: parent
radius: Style.radiusM * scaling
color: Qt.alpha(Color.mSurface, 0.9)
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling)
Repeater {
model: 20
Rectangle {
width: parent.width
height: 40 * scaling
color: Qt.alpha(Color.mPrimary, 0.2)
topLeftRadius: Style.radiusS * scaling
topRightRadius: Style.radiusS * scaling
RowLayout {
anchors.fill: parent
anchors.topMargin: Style.marginM * scaling
anchors.bottomMargin: Style.marginM * scaling
anchors.leftMargin: Style.marginL * scaling
anchors.rightMargin: Style.marginL * scaling
spacing: Style.marginM * scaling
NText {
text: "SECURE TERMINAL"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
Row {
spacing: Style.marginS * scaling
visible: batteryIndicator.batteryVisible
NIcon {
text: batteryIndicator.getIcon()
font.pointSize: Style.fontSizeM * scaling
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
}
NText {
text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
}
Row {
spacing: Style.marginS * scaling
NText {
text: keyboardLayout.currentLayout
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
NIcon {
text: "keyboard_alt"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
}
}
}
}
ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Style.marginL * scaling
anchors.topMargin: 70 * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: Quickshell.env("USER") + "@noctalia:~$"
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
NText {
id: welcomeText
text: ""
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
property int currentIndex: 0
property string fullText: "Welcome back, " + Quickshell.env("USER") + "!"
Timer {
interval: Style.animationFast
running: true
repeat: true
onTriggered: {
if (parent.currentIndex < parent.fullText.length) {
parent.text = parent.fullText.substring(0, parent.currentIndex + 1)
parent.currentIndex++
} else {
running = false
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: Quickshell.env("USER") + "@noctalia:~$"
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
NText {
text: "sudo unlock-session"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
}
TextInput {
id: passwordInput
width: 0
height: 0
visible: false
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
echoMode: TextInput.Password
passwordCharacter: "*"
passwordMaskDelay: 0
text: lockContext.currentText
onTextChanged: {
lockContext.currentText = text
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
lockContext.tryUnlock()
}
}
Component.onCompleted: {
forceActiveFocus()
}
}
NText {
id: asterisksText
text: "*".repeat(passwordInput.text.length)
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
visible: passwordInput.activeFocus
SequentialAnimation {
id: typingEffect
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.01
duration: 50
}
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.0
duration: 50
}
}
}
Rectangle {
width: 8 * scaling
height: 20 * scaling
color: Color.mPrimary
visible: passwordInput.activeFocus
Layout.leftMargin: -Style.marginS * scaling
Layout.alignment: Qt.AlignVCenter
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 500
}
NumberAnimation {
to: 0.0
duration: 500
}
}
}
}
NText {
text: {
if (lockContext.unlockInProgress)
return "Authenticating..."
if (lockContext.showFailure && lockContext.errorMessage)
return lockContext.errorMessage
if (lockContext.showFailure)
return "Authentication failed."
return ""
}
color: {
if (lockContext.unlockInProgress)
return Color.mPrimary
if (lockContext.showFailure)
return Color.mError
return Color.transparent
}
font.family: "DejaVu Sans Mono"
font.pointSize: Style.fontSizeL * scaling
Layout.fillWidth: true
SequentialAnimation on opacity {
running: lockContext.unlockInProgress
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 800
}
NumberAnimation {
to: 0.5
duration: 800
}
}
}
Row {
Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -10 * scaling
Rectangle {
width: 120 * scaling
height: 40 * scaling
radius: Style.radiusS * scaling
color: executeButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.2)
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderS * scaling)
enabled: !lockContext.unlockInProgress
NText {
anchors.centerIn: parent
text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE"
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
MouseArea {
id: executeButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
lockContext.tryUnlock()
}
SequentialAnimation on scale {
running: executeButtonArea.containsMouse
NumberAnimation {
to: 1.05
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
SequentialAnimation on scale {
running: !executeButtonArea.containsMouse
NumberAnimation {
to: 1.0
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
SequentialAnimation on scale {
loops: Animation.Infinite
running: lockContext.unlockInProgress
NumberAnimation {
to: 1.02
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 600
easing.type: Easing.InOutQuad
}
}
}
}
}
Rectangle {
anchors.fill: parent
radius: parent.radius
color: Color.transparent
border.color: Qt.alpha(Color.mPrimary, 0.3)
border.width: Math.max(1, Style.borderS * scaling)
z: -1
height: 1
color: Qt.alpha(Color.mPrimary, 0.1)
y: index * 10 * scaling
opacity: Style.opacityMedium
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: 2000
easing.type: Easing.InOutQuad
duration: 2000 + Math.random() * 1000
}
NumberAnimation {
to: 0.2
duration: 2000
easing.type: Easing.InOutQuad
to: 0.1
duration: 2000 + Math.random() * 1000
}
}
}
}
Rectangle {
width: parent.width
height: 40 * scaling
color: Qt.alpha(Color.mPrimary, 0.2)
topLeftRadius: Style.radiusS * scaling
topRightRadius: Style.radiusS * scaling
RowLayout {
anchors.fill: parent
anchors.topMargin: Style.marginM * scaling
anchors.bottomMargin: Style.marginM * scaling
anchors.leftMargin: Style.marginL * scaling
anchors.rightMargin: Style.marginL * scaling
spacing: Style.marginL * scaling
NText {
text: "SECURE TERMINAL"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginS * scaling
NText {
text: keyboardLayout.currentLayout
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
NIcon {
icon: "keyboard"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
}
}
RowLayout {
spacing: Style.marginS * scaling
visible: batteryIndicator.batteryVisible
NIcon {
icon: BatteryService.getIcon(batteryIndicator.percent, batteryIndicator.charging, batteryIndicator.isReady)
font.pointSize: Style.fontSizeM * scaling
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
rotation: -90
}
NText {
text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
}
}
}
ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Style.marginL * scaling
anchors.topMargin: 70 * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: Quickshell.env("USER") + "@noctalia:~$"
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
NText {
id: welcomeText
text: ""
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
property int currentIndex: 0
property string fullText: "Welcome back, " + Quickshell.env("USER") + "!"
Timer {
interval: Style.animationFast
running: true
repeat: true
onTriggered: {
if (parent.currentIndex < parent.fullText.length) {
parent.text = parent.fullText.substring(0, parent.currentIndex + 1)
parent.currentIndex++
} else {
running = false
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: Quickshell.env("USER") + "@noctalia:~$"
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
NText {
text: "sudo unlock-session"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
}
TextInput {
id: passwordInput
width: 0
height: 0
visible: false
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
echoMode: TextInput.Password
passwordCharacter: "*"
passwordMaskDelay: 0
text: lockContext.currentText
onTextChanged: {
lockContext.currentText = text
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
lockContext.tryUnlock()
}
}
Component.onCompleted: {
forceActiveFocus()
}
}
NText {
id: asterisksText
text: "*".repeat(passwordInput.text.length)
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
visible: passwordInput.activeFocus
SequentialAnimation {
id: typingEffect
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.01
duration: 50
}
NumberAnimation {
target: passwordInput
property: "scale"
to: 1.0
duration: 50
}
}
}
Rectangle {
width: 8 * scaling
height: 20 * scaling
color: Color.mPrimary
visible: passwordInput.activeFocus
Layout.leftMargin: -Style.marginS * scaling
Layout.alignment: Qt.AlignVCenter
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 500
}
NumberAnimation {
to: 0.0
duration: 500
}
}
}
}
NText {
text: {
if (lockContext.unlockInProgress)
return "Authenticating..."
if (lockContext.showFailure && lockContext.errorMessage)
return lockContext.errorMessage
if (lockContext.showFailure)
return "Authentication failed."
return ""
}
color: {
if (lockContext.unlockInProgress)
return Color.mPrimary
if (lockContext.showFailure)
return Color.mError
return Color.transparent
}
font.family: "DejaVu Sans Mono"
font.pointSize: Style.fontSizeL * scaling
Layout.fillWidth: true
SequentialAnimation on opacity {
running: lockContext.unlockInProgress
loops: Animation.Infinite
NumberAnimation {
to: 1.0
duration: 800
}
NumberAnimation {
to: 0.5
duration: 800
}
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -10 * scaling
Rectangle {
Layout.preferredWidth: 120 * scaling
Layout.preferredHeight: 40 * scaling
radius: Style.radiusS * scaling
color: executeButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.2)
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderS * scaling)
enabled: !lockContext.unlockInProgress
NText {
anchors.centerIn: parent
text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE"
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
MouseArea {
id: executeButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
lockContext.tryUnlock()
}
SequentialAnimation on scale {
running: executeButtonArea.containsMouse
NumberAnimation {
to: 1.05
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
SequentialAnimation on scale {
running: !executeButtonArea.containsMouse
NumberAnimation {
to: 1.0
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
SequentialAnimation on scale {
loops: Animation.Infinite
running: lockContext.unlockInProgress
NumberAnimation {
to: 1.02
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 600
easing.type: Easing.InOutQuad
}
}
}
}
}
Rectangle {
anchors.fill: parent
radius: parent.radius
color: Color.transparent
border.color: Qt.alpha(Color.mPrimary, 0.3)
border.width: Math.max(1, Style.borderS * scaling)
z: -1
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: 2000
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 0.2
duration: 2000
easing.type: Easing.InOutQuad
}
}
}
}
}
// Power buttons at bottom
Row {
// Power buttons at bottom right
RowLayout {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 50 * scaling
spacing: 20 * scaling
// Shutdown
Rectangle {
width: 60 * scaling
height: 60 * scaling
Layout.preferredWidth: iconPower.implicitWidth + Style.marginXL * scaling
Layout.preferredHeight: Layout.preferredWidth
radius: width * 0.5
color: powerButtonArea.containsMouse ? Color.mError : Qt.alpha(Color.mError, 0.2)
border.color: Color.mError
border.width: Math.max(1, Style.borderM * scaling)
NIcon {
id: iconPower
anchors.centerIn: parent
text: "power_settings_new"
font.pointSize: Style.fontSizeXL * scaling
icon: "shutdown"
font.pointSize: Style.fontSizeXXXL * scaling
color: powerButtonArea.containsMouse ? Color.mOnError : Color.mError
}
// Tooltip (inline rectangle to avoid separate Window during lock)
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: powerButtonArea.containsMouse
z: 1
NText {
id: shutdownTooltipText
anchors.margins: Style.marginM * scaling
anchors.fill: parent
text: "Shut down."
font.pointSize: Style.fontSizeM * scaling
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
implicitWidth: shutdownTooltipText.implicitWidth + Style.marginM * 2 * scaling
implicitHeight: shutdownTooltipText.implicitHeight + Style.marginM * 2 * scaling
}
MouseArea {
id: powerButtonArea
anchors.fill: parent
@@ -776,21 +765,47 @@ Loader {
}
}
// Reboot
Rectangle {
width: 60 * scaling
height: 60 * scaling
Layout.preferredWidth: iconReboot.implicitWidth + Style.marginXL * scaling
Layout.preferredHeight: Layout.preferredWidth
radius: width * 0.5
color: restartButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, Style.opacityLight)
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling)
NIcon {
id: iconReboot
anchors.centerIn: parent
text: "restart_alt"
font.pointSize: Style.fontSizeXL * scaling
icon: "reboot"
font.pointSize: Style.fontSizeXXXL * scaling
color: restartButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
}
// Tooltip
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: restartButtonArea.containsMouse
z: 1
NText {
id: restartTooltipText
anchors.margins: Style.marginM * scaling
anchors.fill: parent
text: "Restart."
font.pointSize: Style.fontSizeM * scaling
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
implicitWidth: restartTooltipText.implicitWidth + Style.marginM * 2 * scaling
implicitHeight: restartTooltipText.implicitHeight + Style.marginM * 2 * scaling
}
MouseArea {
id: restartButtonArea
anchors.fill: parent
@@ -798,24 +813,51 @@ Loader {
onClicked: {
CompositorService.reboot()
}
// Tooltip handled via inline rectangle visibility
}
}
// Suspend
Rectangle {
width: 60 * scaling
height: 60 * scaling
Layout.preferredWidth: iconSuspend.implicitWidth + Style.marginXL * scaling
Layout.preferredHeight: Layout.preferredWidth
radius: width * 0.5
color: suspendButtonArea.containsMouse ? Color.mSecondary : Qt.alpha(Color.mSecondary, 0.2)
border.color: Color.mSecondary
border.width: Math.max(1, Style.borderM * scaling)
NIcon {
id: iconSuspend
anchors.centerIn: parent
text: "bedtime"
font.pointSize: Style.fontSizeXL * scaling
icon: "suspend"
font.pointSize: Style.fontSizeXXXL * scaling
color: suspendButtonArea.containsMouse ? Color.mOnSecondary : Color.mSecondary
}
// Tooltip
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: suspendButtonArea.containsMouse
z: 1
NText {
id: suspendTooltipText
anchors.margins: Style.marginM * scaling
anchors.fill: parent
text: "Suspend."
font.pointSize: Style.fontSizeM * scaling
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
implicitWidth: suspendTooltipText.implicitWidth + Style.marginM * 2 * scaling
implicitHeight: suspendTooltipText.implicitHeight + Style.marginM * 2 * scaling
}
MouseArea {
id: suspendButtonArea
anchors.fill: parent
@@ -823,6 +865,7 @@ Loader {
onClicked: {
CompositorService.suspend()
}
// Tooltip handled via inline rectangle visibility
}
}
}

View File

@@ -25,10 +25,7 @@ Variants {
property var removingNotifications: ({})
// If no notification display activated in settings, then show them all
active: Settings.isLoaded && modelData
&& (NotificationService.notificationModel.count > 0) ? (Settings.data.notifications.monitors.includes(
modelData.name)
|| (Settings.data.notifications.monitors.length === 0)) : false
active: Settings.isLoaded && modelData && (NotificationService.notificationModel.count > 0) ? (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0)) : false
visible: (NotificationService.notificationModel.count > 0)
@@ -36,13 +33,50 @@ Variants {
screen: modelData
color: Color.transparent
// Position based on bar location
anchors.top: Settings.data.bar.position === "top"
anchors.bottom: Settings.data.bar.position === "bottom"
anchors.right: true
margins.top: Settings.data.bar.position === "top" ? (Style.barHeight + Style.marginM) * scaling : 0
margins.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginM) * scaling : 0
margins.right: Style.marginM * scaling
// Position based on bar location - always at top
anchors.top: true
anchors.right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
anchors.left: Settings.data.bar.position === "left"
margins.top: {
switch (Settings.data.bar.position) {
case "top":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
return Style.marginM * scaling
}
}
margins.bottom: {
switch (Settings.data.bar.position) {
case "bottom":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
return 0
}
}
margins.left: {
switch (Settings.data.bar.position) {
case "left":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
default:
return 0
}
}
margins.right: {
switch (Settings.data.bar.position) {
case "right":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
case "top":
case "bottom":
return Style.marginM * scaling
default:
return 0
}
}
implicitWidth: 360 * scaling
implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling)
//WlrLayershell.layer: WlrLayer.Overlay
@@ -78,12 +112,12 @@ Variants {
}
// Main notification container
Column {
ColumnLayout {
id: notificationStack
// Position based on bar location
anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined
anchors.bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined
anchors.right: parent.right
// Position based on bar location - always at top
anchors.top: parent.top
anchors.right: (Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom") ? parent.right : undefined
anchors.left: Settings.data.bar.position === "left" ? parent.left : undefined
spacing: Style.marginS * scaling
width: 360 * scaling
visible: true
@@ -92,8 +126,9 @@ Variants {
Repeater {
model: notificationModel
delegate: Rectangle {
width: 360 * scaling
height: Math.max(80 * scaling, contentRow.implicitHeight + (Style.marginL * 2 * scaling))
Layout.preferredWidth: 360 * scaling
Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling)
Layout.maximumHeight: Layout.preferredHeight
clip: true
radius: Style.radiusL * scaling
border.color: Color.mOutline
@@ -105,6 +140,17 @@ Variants {
property real opacityValue: 0.0
property bool isRemoving: false
// Right-click to dismiss
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (mouse.button === Qt.RightButton) {
animateOut()
}
}
}
// Scale and fade-in animation
scale: scaleValue
opacity: opacityValue
@@ -156,104 +202,135 @@ Variants {
}
}
RowLayout {
id: contentRow
ColumnLayout {
id: notificationLayout
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginL * scaling
anchors.margins: Style.marginM * scaling
anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button
spacing: Style.marginM * scaling
// Right: header on top, then avatar + texts
ColumnLayout {
id: textColumn
spacing: Style.marginS * scaling
// Header section with app name and timestamp
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
RowLayout {
spacing: Style.marginS * scaling
id: appHeaderRow
NText {
text: `${(model.appName || model.desktopEntry)
|| "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
color: Color.mSecondary
font.pointSize: Style.fontSizeXS * scaling
}
Rectangle {
width: 6 * scaling
height: 6 * scaling
radius: Style.radiusXS * scaling
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
NText {
text: `${(model.appName || model.desktopEntry) || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
color: Color.mSecondary
font.pointSize: Style.fontSizeXS * scaling
}
RowLayout {
id: bodyRow
spacing: Style.marginM * scaling
Rectangle {
Layout.preferredWidth: 6 * scaling
Layout.preferredHeight: 6 * scaling
radius: Style.radiusXS * scaling
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
Layout.alignment: Qt.AlignVCenter
}
NImageCircled {
id: appAvatar
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling
Layout.alignment: Qt.AlignTop
// Start avatar aligned with body (below the summary)
anchors.topMargin: textContent.childrenRect.y
// Prefer notification-provided image (e.g., user avatar) then fall back to app icon
imagePath: (model.image && model.image !== "") ? model.image : Icons.iconFromName(
model.appIcon, "application-x-executable")
fallbackIcon: "apps"
borderColor: Color.transparent
borderWidth: 0
visible: (imagePath && imagePath !== "")
Item {
Layout.fillWidth: true
}
}
// Main content section
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Image
NImageCircled {
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling
Layout.alignment: Qt.AlignTop
imagePath: model.image && model.image !== "" ? model.image : ""
borderColor: Color.transparent
borderWidth: 0
visible: (model.image && model.image !== "")
}
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
NText {
text: model.summary || "No summary"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
maximumLineCount: 3
elide: Text.ElideRight
}
Column {
id: textContent
spacing: Style.marginS * scaling
NText {
text: model.body || ""
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
// Ensure a concrete width so text wraps
width: (textColumn.width - (appAvatar.visible ? (appAvatar.width + Style.marginM * scaling) : 0))
NText {
text: model.summary || "No summary"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
width: parent.width
maximumLineCount: 3
elide: Text.ElideRight
}
NText {
text: model.body || ""
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
width: parent.width
maximumLineCount: 5
elide: Text.ElideRight
}
maximumLineCount: 5
elide: Text.ElideRight
visible: text.length > 0
}
}
}
// Actions removed
// Notification actions
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
visible: model.rawNotification && model.rawNotification.actions && model.rawNotification.actions.length > 0
property var notificationActions: model.rawNotification ? model.rawNotification.actions : []
Repeater {
model: parent.notificationActions
delegate: NButton {
text: {
var actionText = modelData.text || "Open"
// If text contains comma, take the part after the comma (the display text)
if (actionText.includes(",")) {
return actionText.split(",")[1] || actionText
}
return actionText
}
fontSize: Style.fontSizeS * scaling
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
hoverColor: Color.mSecondary
pressColor: Color.mTertiary
outlined: false
customHeight: 32 * scaling
Layout.preferredHeight: 32 * scaling
onClicked: {
if (modelData && modelData.invoke) {
modelData.invoke()
}
}
}
}
// Spacer to push buttons to the left if needed
Item {
Layout.fillWidth: true
}
}
}
// Close button positioned absolutely
NIconButton {
icon: "close"
tooltipText: "Close"
// Compact target (~24dp) and glyph (~16dp)
sizeRatio: 0.75
fontPointSize: 16
tooltipText: "Close."
sizeRatio: 0.6
anchors.top: parent.top
anchors.topMargin: Style.marginM * scaling
anchors.right: parent.right
anchors.margins: Style.marginS * scaling
anchors.rightMargin: Style.marginM * scaling
onClicked: {
animateOut()

View File

@@ -12,9 +12,10 @@ import qs.Widgets
NPanel {
id: root
panelWidth: 380 * scaling
panelHeight: 500 * scaling
panelAnchorRight: true
preferredWidth: 380
preferredHeight: 500
panelAnchorRight: Settings.data.bar.position === "right"
panelKeyboardFocus: true
panelContent: Rectangle {
id: notificationRect
@@ -25,12 +26,13 @@ NPanel {
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
// Header section
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NIcon {
text: "notifications"
icon: "bell"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
@@ -44,15 +46,26 @@ NPanel {
}
NIconButton {
icon: "delete"
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
tooltipText: Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' is enabled." : "'Do Not Disturb' is disabled."
sizeRatio: 0.8
onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
}
NIconButton {
icon: "trash"
tooltipText: "Clear history"
sizeRatio: 0.8
onClicked: NotificationService.clearHistory()
onClicked: {
NotificationService.clearHistory()
root.close()
}
}
NIconButton {
icon: "close"
tooltipText: "Close"
tooltipText: "Close."
sizeRatio: 0.8
onClicked: {
root.close()
@@ -65,42 +78,54 @@ NPanel {
}
// Empty state when no notifications
Item {
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
visible: NotificationService.historyModel.count === 0
spacing: Style.marginL * scaling
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
Item {
Layout.fillHeight: true
}
NIcon {
text: "notifications_off"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NIcon {
icon: "bell-off"
font.pointSize: 64 * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No notifications"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No notifications"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Your notifications will show up here as they arrive."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Your notifications will show up here as they arrive."
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
ListView {
// Notification list
NListView {
id: notificationList
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
model: NotificationService.historyModel
spacing: Style.marginM * scaling
clip: true
@@ -108,32 +133,45 @@ NPanel {
visible: NotificationService.historyModel.count > 0
delegate: Rectangle {
width: notificationList ? notificationList.width : 380 * scaling
height: Math.max(80, notificationContent.height + 30)
width: notificationList.width
height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2)
radius: Style.radiusM * scaling
color: notificationMouseArea.containsMouse ? Color.mSecondary : Color.mSurfaceVariant
color: notificationMouseArea.containsMouse ? Color.mTertiary : Color.mSurfaceVariant
border.color: Qt.alpha(Color.mOutline, Style.opacityMedium)
border.width: Math.max(1, Style.borderS * scaling)
RowLayout {
anchors {
fill: parent
margins: Style.marginM * scaling
}
id: notificationLayout
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
// Notification content
Column {
id: notificationContent
// App icon (same style as popup)
NImageCircled {
Layout.preferredWidth: 28 * scaling
Layout.preferredHeight: 28 * scaling
Layout.alignment: Qt.AlignVCenter
// Prefer stable themed icons over transient image paths
imagePath: (appIcon && appIcon !== "") ? (AppIcons.iconFromName(appIcon, "application-x-executable") || appIcon) : ((AppIcons.iconForAppId(desktopEntry || appName, "application-x-executable") || (image && image !== "" ? image : AppIcons.iconFromName("application-x-executable", "application-x-executable"))))
borderColor: Color.transparent
borderWidth: 0
visible: true
}
// Notification content column
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: notificationList.width - (Style.marginM * scaling * 4) // Account for margins and delete button
spacing: Style.marginXXS * scaling
NText {
text: (summary || "No summary").substring(0, 100)
font.pointSize: Style.fontSizeM * scaling
font.weight: Font.Medium
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary
color: notificationMouseArea.containsMouse ? Color.mOnTertiary : Color.mPrimary
wrapMode: Text.Wrap
width: parent.width - 60
Layout.fillWidth: true
maximumLineCount: 2
elide: Text.ElideRight
}
@@ -141,25 +179,28 @@ NPanel {
NText {
text: (body || "").substring(0, 150)
font.pointSize: Style.fontSizeXS * scaling
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
color: notificationMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
wrapMode: Text.Wrap
width: parent.width - 60
Layout.fillWidth: true
maximumLineCount: 3
elide: Text.ElideRight
visible: text.length > 0
}
NText {
text: NotificationService.formatTimestamp(timestamp)
font.pointSize: Style.fontSizeXS * scaling
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
color: notificationMouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
Layout.fillWidth: true
}
}
// Trash icon button
// Delete button
NIconButton {
icon: "delete"
icon: "trash"
tooltipText: "Delete notification"
sizeRatio: 0.7
Layout.alignment: Qt.AlignTop
onClicked: {
Logger.log("NotificationHistory", "Removing notification:", summary)
@@ -172,7 +213,7 @@ NPanel {
MouseArea {
id: notificationMouseArea
anchors.fill: parent
anchors.rightMargin: Style.marginL * 3 * scaling
anchors.rightMargin: Style.marginXL * scaling
hoverEnabled: true
}
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
@@ -12,10 +13,11 @@ import qs.Widgets
NPanel {
id: root
panelWidth: 440 * scaling
panelHeight: 380 * scaling
preferredWidth: 440
preferredHeight: 410
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
panelKeyboardFocus: true
// Timer properties
property int timerDuration: 9000 // 9 seconds
@@ -23,9 +25,44 @@ NPanel {
property bool timerActive: false
property int timeRemaining: 0
// Cancel timer when panel is closing
// Navigation properties
property int selectedIndex: 0
readonly property var powerOptions: [{
"action": "lock",
"icon": "lock",
"title": "Lock",
"subtitle": "Lock your session"
}, {
"action": "suspend",
"icon": "suspend",
"title": "Suspend",
"subtitle": "Put the system to sleep"
}, {
"action": "reboot",
"icon": "reboot",
"title": "Reboot",
"subtitle": "Restart the system"
}, {
"action": "logout",
"icon": "logout",
"title": "Logout",
"subtitle": "End your session"
}, {
"action": "shutdown",
"icon": "shutdown",
"title": "Shutdown",
"subtitle": "Turn off the system",
"isShutdown": true
}]
// Lifecycle handlers
onOpened: {
selectedIndex = 0
}
onClosed: {
cancelTimer()
selectedIndex = 0
}
// Timer management
@@ -79,6 +116,38 @@ NPanel {
root.close()
}
// Navigation functions
function selectNext() {
if (powerOptions.length > 0) {
selectedIndex = Math.min(selectedIndex + 1, powerOptions.length - 1)
}
}
function selectPrevious() {
if (powerOptions.length > 0) {
selectedIndex = Math.max(selectedIndex - 1, 0)
}
}
function selectFirst() {
selectedIndex = 0
}
function selectLast() {
if (powerOptions.length > 0) {
selectedIndex = powerOptions.length - 1
} else {
selectedIndex = 0
}
}
function activate() {
if (powerOptions.length > 0 && powerOptions[selectedIndex]) {
const option = powerOptions[selectedIndex]
startTimer(option.action)
}
}
// Countdown timer
Timer {
id: countdownTimer
@@ -93,8 +162,93 @@ NPanel {
}
panelContent: Rectangle {
id: ui
color: Color.transparent
// Keyboard shortcuts
Shortcut {
sequence: "Ctrl+K"
onActivated: ui.selectPrevious()
enabled: root.opened
}
Shortcut {
sequence: "Ctrl+J"
onActivated: ui.selectNext()
enabled: root.opened
}
Shortcut {
sequence: "Up"
onActivated: ui.selectPrevious()
enabled: root.opened
}
Shortcut {
sequence: "Down"
onActivated: ui.selectNext()
enabled: root.opened
}
Shortcut {
sequence: "Home"
onActivated: ui.selectFirst()
enabled: root.opened
}
Shortcut {
sequence: "End"
onActivated: ui.selectLast()
enabled: root.opened
}
Shortcut {
sequence: "Return"
onActivated: ui.activate()
enabled: root.opened
}
Shortcut {
sequence: "Enter"
onActivated: ui.activate()
enabled: root.opened
}
Shortcut {
sequence: "Escape"
onActivated: {
if (timerActive) {
cancelTimer()
} else {
cancelTimer()
root.close()
}
}
context: Qt.WidgetShortcut
enabled: root.opened
}
// Navigation functions
function selectNext() {
root.selectNext()
}
function selectPrevious() {
root.selectPrevious()
}
function selectFirst() {
root.selectFirst()
}
function selectLast() {
root.selectLast()
}
function activate() {
root.activate()
}
ColumnLayout {
anchors.fill: parent
anchors.topMargin: Style.marginL * scaling
@@ -109,8 +263,7 @@ NPanel {
Layout.preferredHeight: Style.baseWidgetSize * 0.8 * scaling
NText {
text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(
timeRemaining / 1000)} seconds...` : "Power Options"
text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(timeRemaining / 1000)} seconds...` : "Power Menu"
font.weight: Style.fontWeightBold
font.pointSize: Style.fontSizeL * scaling
color: timerActive ? Color.mPrimary : Color.mOnSurface
@@ -123,7 +276,7 @@ NPanel {
}
NIconButton {
icon: timerActive ? "back_hand" : "close"
icon: timerActive ? "stop" : "close"
tooltipText: timerActive ? "Cancel Timer" : "Close"
Layout.alignment: Qt.AlignVCenter
colorBg: timerActive ? Qt.alpha(Color.mError, 0.08) : Color.transparent
@@ -139,60 +292,30 @@ NPanel {
}
}
NDivider {
Layout.fillWidth: true
}
// Power options
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Lock Screen
PowerButton {
Layout.fillWidth: true
icon: "lock_outline"
title: "Lock"
subtitle: "Lock your session"
onClicked: startTimer("lock")
pending: timerActive && pendingAction === "lock"
}
// Suspend
PowerButton {
Layout.fillWidth: true
icon: "bedtime"
title: "Suspend"
subtitle: "Put the system to sleep"
onClicked: startTimer("suspend")
pending: timerActive && pendingAction === "suspend"
}
// Reboot
PowerButton {
Layout.fillWidth: true
icon: "refresh"
title: "Reboot"
subtitle: "Restart the system"
onClicked: startTimer("reboot")
pending: timerActive && pendingAction === "reboot"
}
// Logout
PowerButton {
Layout.fillWidth: true
icon: "exit_to_app"
title: "Logout"
subtitle: "End your session"
onClicked: startTimer("logout")
pending: timerActive && pendingAction === "logout"
}
// Shutdown
PowerButton {
Layout.fillWidth: true
icon: "power_settings_new"
title: "Shutdown"
subtitle: "Turn off the system"
onClicked: startTimer("shutdown")
pending: timerActive && pendingAction === "shutdown"
isShutdown: true
Repeater {
model: powerOptions
delegate: PowerButton {
Layout.fillWidth: true
icon: modelData.icon
title: modelData.title
subtitle: modelData.subtitle
isShutdown: modelData.isShutdown || false
isSelected: index === selectedIndex
onClicked: {
selectedIndex = index
startTimer(modelData.action)
}
pending: timerActive && pendingAction === modelData.action
}
}
}
}
@@ -207,6 +330,7 @@ NPanel {
property string subtitle: ""
property bool pending: false
property bool isShutdown: false
property bool isSelected: false
signal clicked
@@ -216,8 +340,8 @@ NPanel {
if (pending) {
return Qt.alpha(Color.mPrimary, 0.08)
}
if (mouseArea.containsMouse) {
return Color.mSecondary
if (isSelected || mouseArea.containsMouse) {
return Color.mTertiary
}
return Color.transparent
}
@@ -240,14 +364,13 @@ NPanel {
id: iconElement
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: buttonRoot.icon
icon: buttonRoot.icon
color: {
if (buttonRoot.pending)
return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError
if (mouseArea.containsMouse)
if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnTertiary
return Color.mOnSurface
}
@@ -264,7 +387,7 @@ NPanel {
}
// Text content in the middle
Column {
ColumnLayout {
anchors.left: iconElement.right
anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -279,9 +402,9 @@ NPanel {
color: {
if (buttonRoot.pending)
return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError
if (mouseArea.containsMouse)
if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnTertiary
return Color.mOnSurface
}
@@ -304,9 +427,9 @@ NPanel {
color: {
if (buttonRoot.pending)
return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError
if (mouseArea.containsMouse)
if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnTertiary
return Color.mOnSurfaceVariant
}

View File

@@ -0,0 +1,525 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
NBox {
id: root
property string sectionName: ""
property string sectionId: ""
property var widgetModel: []
property var availableWidgets: []
signal addWidget(string widgetId, string section)
signal removeWidget(string section, int index)
signal reorderWidget(string section, int fromIndex, int toIndex)
signal updateWidgetSettings(string section, int index, var settings)
signal dragPotentialStarted
signal dragPotentialEnded
color: Color.mSurface
Layout.fillWidth: true
Layout.minimumHeight: {
var widgetCount = widgetModel.length
if (widgetCount === 0)
return 140 * scaling
var availableWidth = parent.width
var avgWidgetWidth = 150 * scaling
var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth))
var rows = Math.ceil(widgetCount / widgetsPerRow)
return (50 + 20 + (rows * 48) + ((rows - 1) * Style.marginS) + 20) * scaling
}
// Generate widget color from name checksum
function getWidgetColor(widget) {
const totalSum = JSON.stringify(widget).split('').reduce((acc, character) => {
return acc + character.charCodeAt(0)
}, 0)
switch (totalSum % 6) {
case 0:
return [Color.mPrimary, Color.mOnPrimary]
case 1:
return [Color.mSecondary, Color.mOnSecondary]
case 2:
return [Color.mTertiary, Color.mOnTertiary]
case 3:
return [Color.mError, Color.mOnError]
case 4:
return [Color.mOnSurface, Color.mSurface]
case 5:
return [Color.mOnSurfaceVariant, Color.mSurfaceVariant]
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
NText {
text: sectionName + " Section"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
NComboBox {
id: comboBox
model: availableWidgets
label: ""
description: ""
placeholder: "Select a widget to add..."
onSelected: key => comboBox.currentKey = key
popupHeight: 340 * scaling
Layout.alignment: Qt.AlignVCenter
}
NIconButton {
icon: "add"
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
colorBgHover: Color.mSecondary
colorFgHover: Color.mOnSecondary
enabled: comboBox.currentKey !== ""
tooltipText: "Add widget to section"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
onClicked: {
if (comboBox.currentKey !== "") {
addWidget(comboBox.currentKey, sectionId)
comboBox.currentKey = ""
}
}
}
}
// Drag and Drop Widget Area
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 65 * scaling
clip: false // Don't clip children so ghost can move freely
Flow {
id: widgetFlow
anchors.fill: parent
spacing: Style.marginS * scaling
flow: Flow.LeftToRight
Repeater {
model: widgetModel
delegate: Rectangle {
id: widgetItem
required property int index
required property var modelData
width: widgetContent.implicitWidth + Style.marginL * scaling
height: Style.baseWidgetSize * 1.15 * scaling
radius: Style.radiusL * scaling
color: root.getWidgetColor(modelData)[0]
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
// Store the widget index for drag operations
property int widgetIndex: index
readonly property int buttonsWidth: Math.round(20 * scaling)
readonly property int buttonsCount: 1 + BarWidgetRegistry.widgetHasUserSettings(modelData.id)
// Visual feedback during drag
opacity: flowDragArea.draggedIndex === index ? 0.5 : 1.0
scale: flowDragArea.draggedIndex === index ? 0.95 : 1.0
z: flowDragArea.draggedIndex === index ? 1000 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
}
}
RowLayout {
id: widgetContent
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NText {
text: modelData.id
font.pointSize: Style.fontSizeS * scaling
color: root.getWidgetColor(modelData)[1]
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
Layout.preferredWidth: 80 * scaling
}
RowLayout {
spacing: 0
Layout.preferredWidth: buttonsCount * buttonsWidth
Loader {
active: BarWidgetRegistry.widgetHasUserSettings(modelData.id)
sourceComponent: NIconButton {
icon: "settings"
sizeRatio: 0.6
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
colorBg: Color.mOnSurface
colorFg: Color.mOnPrimary
colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight)
colorFgHover: Color.mOnPrimary
onClicked: {
var component = Qt.createComponent(Qt.resolvedUrl("BarWidgetSettingsDialog.qml"))
function instantiateAndOpen() {
var dialog = component.createObject(root, {
"widgetIndex": index,
"widgetData": modelData,
"widgetId": modelData.id,
"parent": Overlay.overlay
})
if (dialog) {
dialog.open()
} else {
Logger.error("BarSectionEditor", "Failed to create settings dialog instance")
}
}
if (component.status === Component.Ready) {
instantiateAndOpen()
} else if (component.status === Component.Error) {
Logger.error("BarSectionEditor", component.errorString())
} else {
component.statusChanged.connect(function () {
if (component.status === Component.Ready) {
instantiateAndOpen()
} else if (component.status === Component.Error) {
Logger.error("BarSectionEditor", component.errorString())
}
})
}
}
}
}
NIconButton {
icon: "close"
sizeRatio: 0.6
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
colorBg: Color.mOnSurface
colorFg: Color.mOnPrimary
colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight)
colorFgHover: Color.mOnPrimary
onClicked: {
removeWidget(sectionId, index)
}
}
}
}
}
}
}
// Ghost/Clone widget for dragging
Rectangle {
id: dragGhost
width: 0
height: Style.baseWidgetSize * 1.15 * scaling
radius: Style.radiusL * scaling
color: Color.transparent
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
opacity: 0.7
visible: flowDragArea.dragStarted
z: 2000
clip: false // Ensure ghost isn't clipped
Text {
id: ghostText
anchors.centerIn: parent
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnPrimary
}
}
// Drop indicator - visual feedback for where the widget will be inserted
Rectangle {
id: dropIndicator
width: 3 * scaling
height: Style.baseWidgetSize * 1.15 * scaling
radius: width / 2
color: Color.mPrimary
opacity: 0
visible: opacity > 0
z: 1999
SequentialAnimation on opacity {
id: pulseAnimation
running: false
loops: Animation.Infinite
NumberAnimation {
to: 1
duration: 400
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 0.6
duration: 400
easing.type: Easing.InOutQuad
}
}
Behavior on x {
NumberAnimation {
duration: 100
easing.type: Easing.OutCubic
}
}
Behavior on y {
NumberAnimation {
duration: 100
easing.type: Easing.OutCubic
}
}
}
// MouseArea for drag and drop
MouseArea {
id: flowDragArea
anchors.fill: parent
z: -1
acceptedButtons: Qt.LeftButton
preventStealing: false
propagateComposedEvents: false
hoverEnabled: true // Always track mouse for drag operations
property point startPos: Qt.point(0, 0)
property bool dragStarted: false
property bool potentialDrag: false // Track if we're in a potential drag interaction
property int draggedIndex: -1
property real dragThreshold: 15 * scaling
property Item draggedWidget: null
property int dropTargetIndex: -1
property var draggedModelData: null
// Drop position calculation
function updateDropIndicator(mouseX, mouseY) {
if (!dragStarted || draggedIndex === -1) {
dropIndicator.opacity = 0
pulseAnimation.running = false
return
}
let bestIndex = -1
let bestPosition = null
let minDistance = Infinity
// Check position relative to each widget
for (var i = 0; i < widgetModel.length; i++) {
if (i === draggedIndex)
continue
const widget = widgetFlow.children[i]
if (!widget || widget.widgetIndex === undefined)
continue
// Check distance to left edge (insert before)
const leftDist = Math.sqrt(Math.pow(mouseX - widget.x, 2) + Math.pow(mouseY - (widget.y + widget.height / 2), 2))
// Check distance to right edge (insert after)
const rightDist = Math.sqrt(Math.pow(mouseX - (widget.x + widget.width), 2) + Math.pow(mouseY - (widget.y + widget.height / 2), 2))
if (leftDist < minDistance) {
minDistance = leftDist
bestIndex = i
bestPosition = Qt.point(widget.x - dropIndicator.width / 2 - Style.marginXS * scaling, widget.y)
}
if (rightDist < minDistance) {
minDistance = rightDist
bestIndex = i + 1
bestPosition = Qt.point(widget.x + widget.width + Style.marginXS * scaling - dropIndicator.width / 2, widget.y)
}
}
// Check if we should insert at position 0 (very beginning)
if (widgetModel.length > 0 && draggedIndex !== 0) {
const firstWidget = widgetFlow.children[0]
if (firstWidget) {
const dist = Math.sqrt(Math.pow(mouseX, 2) + Math.pow(mouseY - firstWidget.y, 2))
if (dist < minDistance && mouseX < firstWidget.x + firstWidget.width / 2) {
minDistance = dist
bestIndex = 0
bestPosition = Qt.point(Math.max(0, firstWidget.x - dropIndicator.width - Style.marginS * scaling), firstWidget.y)
}
}
}
// Only show indicator if we're close enough and it's a different position
if (minDistance < 80 * scaling && bestIndex !== -1) {
// Adjust index if we're moving forward
let adjustedIndex = bestIndex
if (bestIndex > draggedIndex) {
adjustedIndex = bestIndex - 1
}
// Don't show if it's the same position
if (adjustedIndex === draggedIndex) {
dropIndicator.opacity = 0
pulseAnimation.running = false
dropTargetIndex = -1
return
}
dropTargetIndex = adjustedIndex
if (bestPosition) {
dropIndicator.x = bestPosition.x
dropIndicator.y = bestPosition.y
dropIndicator.opacity = 1
if (!pulseAnimation.running) {
pulseAnimation.running = true
}
}
} else {
dropIndicator.opacity = 0
pulseAnimation.running = false
dropTargetIndex = -1
}
}
onPressed: mouse => {
startPos = Qt.point(mouse.x, mouse.y)
dragStarted = false
potentialDrag = false
draggedIndex = -1
draggedWidget = null
dropTargetIndex = -1
draggedModelData = null
// Find which widget was clicked
for (var i = 0; i < widgetModel.length; i++) {
const widget = widgetFlow.children[i]
if (widget && widget.widgetIndex !== undefined) {
if (mouse.x >= widget.x && mouse.x <= widget.x + widget.width && mouse.y >= widget.y && mouse.y <= widget.y + widget.height) {
const localX = mouse.x - widget.x
const buttonsStartX = widget.width - (widget.buttonsCount * widget.buttonsWidth)
if (localX < buttonsStartX) {
// This is a draggable area - prevent panel close immediately
draggedIndex = widget.widgetIndex
draggedWidget = widget
draggedModelData = widget.modelData
potentialDrag = true
preventStealing = true
// Signal that interaction started (prevents panel close)
root.dragPotentialStarted()
break
} else {
// This is a button area - let the click through
mouse.accepted = false
return
}
}
}
}
}
onPositionChanged: mouse => {
if (draggedIndex !== -1 && potentialDrag) {
const deltaX = mouse.x - startPos.x
const deltaY = mouse.y - startPos.y
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
if (!dragStarted && distance > dragThreshold) {
dragStarted = true
// Setup ghost widget
if (draggedWidget) {
dragGhost.width = draggedWidget.width
dragGhost.color = root.getWidgetColor(draggedModelData)[0]
ghostText.text = draggedModelData.id
}
}
if (dragStarted) {
// Move ghost widget
dragGhost.x = mouse.x - dragGhost.width / 2
dragGhost.y = mouse.y - dragGhost.height / 2
// Update drop indicator
updateDropIndicator(mouse.x, mouse.y)
}
}
}
onReleased: mouse => {
if (dragStarted && dropTargetIndex !== -1 && dropTargetIndex !== draggedIndex) {
// Perform the reorder
reorderWidget(sectionId, draggedIndex, dropTargetIndex)
}
// Always signal end of interaction if we started one
if (potentialDrag) {
root.dragPotentialEnded()
}
// Reset everything
dragStarted = false
potentialDrag = false
draggedIndex = -1
draggedWidget = null
dropTargetIndex = -1
draggedModelData = null
preventStealing = false
dropIndicator.opacity = 0
pulseAnimation.running = false
dragGhost.width = 0
}
onExited: {
if (dragStarted) {
// Hide drop indicator when mouse leaves, but keep ghost visible
dropIndicator.opacity = 0
pulseAnimation.running = false
}
}
onCanceled: {
// Handle cancel (e.g., ESC key pressed during drag)
if (potentialDrag) {
root.dragPotentialEnded()
}
// Reset everything
dragStarted = false
potentialDrag = false
draggedIndex = -1
draggedWidget = null
dropTargetIndex = -1
draggedModelData = null
preventStealing = false
dropIndicator.opacity = 0
pulseAnimation.running = false
dragGhost.width = 0
}
}
}
}
}

View File

@@ -0,0 +1,136 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
import "./WidgetSettings" as WidgetSettings
// Widget Settings Dialog Component
Popup {
id: settingsPopup
property int widgetIndex: -1
property var widgetData: null
property string widgetId: ""
// Center popup in parent
x: (parent.width - width) * 0.5
y: (parent.height - height) * 0.5
width: 420 * scaling
height: content.implicitHeight + padding * 2
padding: Style.marginXL * scaling
modal: true
background: Rectangle {
id: bgRect
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Style.borderM * scaling
}
// Load settings when popup opens with data
onOpened: {
if (widgetData && widgetId) {
loadWidgetSettings()
}
}
function loadWidgetSettings() {
const widgetSettingsMap = {
"ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml",
"Battery": "WidgetSettings/BatterySettings.qml",
"Brightness": "WidgetSettings/BrightnessSettings.qml",
"Clock": "WidgetSettings/ClockSettings.qml",
"CustomButton": "WidgetSettings/CustomButtonSettings.qml",
"KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml",
"MediaMini": "WidgetSettings/MediaMiniSettings.qml",
"Microphone": "WidgetSettings/MicrophoneSettings.qml",
"NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml",
"Workspace": "WidgetSettings/WorkspaceSettings.qml",
"SidePanelToggle": "WidgetSettings/SidePanelToggleSettings.qml",
"Spacer": "WidgetSettings/SpacerSettings.qml",
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
"Volume": "WidgetSettings/VolumeSettings.qml"
}
const source = widgetSettingsMap[widgetId]
if (source) {
// Use setSource to pass properties at creation time
settingsLoader.setSource(source, {
"widgetData": widgetData,
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
})
}
}
ColumnLayout {
id: content
width: parent.width
spacing: Style.marginM * scaling
// Title
RowLayout {
Layout.fillWidth: true
NText {
text: `${settingsPopup.widgetId} Settings`
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: settingsPopup.close()
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
// Settings based on widget type
// Will be triggered via settingsLoader.setSource()
Loader {
id: settingsLoader
Layout.fillWidth: true
}
// Action buttons
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
spacing: Style.marginM * scaling
Item {
Layout.fillWidth: true
}
NButton {
text: "Cancel"
outlined: true
onClicked: settingsPopup.close()
}
NButton {
text: "Apply"
icon: "check"
onClicked: {
if (settingsLoader.item && settingsLoader.item.saveSettings) {
var newSettings = settingsLoader.item.saveSettings()
root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings)
settingsPopup.close()
}
}
}
}
}
}

View File

@@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showIcon = valueShowIcon
return settings
}
NToggle {
id: showIcon
Layout.fillWidth: true
label: "Show app icon"
checked: root.valueShowIcon
onToggled: checked => root.valueShowIcon = checked
}
}

View File

@@ -0,0 +1,42 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage !== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
property int valueWarningThreshold: widgetData.warningThreshold !== undefined ? widgetData.warningThreshold : widgetMetadata.warningThreshold
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage
settings.warningThreshold = valueWarningThreshold
return settings
}
NToggle {
label: "Always show percentage"
checked: root.valueAlwaysShowPercentage
onToggled: checked => root.valueAlwaysShowPercentage = checked
}
NSpinBox {
label: "Low battery warning threshold"
description: "Show a warning when battery falls below this percentage."
value: valueWarningThreshold
suffix: "%"
minimum: 5
maximum: 50
onValueChanged: valueWarningThreshold = value
}
}

View File

@@ -0,0 +1,30 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage !== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage
return settings
}
NToggle {
label: "Always show percentage"
checked: valueAlwaysShowPercentage
onToggled: checked => valueAlwaysShowPercentage = checked
}
}

View File

@@ -0,0 +1,65 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property string valueDisplayFormat: widgetData.displayFormat !== undefined ? widgetData.displayFormat : widgetMetadata.displayFormat
property bool valueUse12h: widgetData.use12HourClock !== undefined ? widgetData.use12HourClock : widgetMetadata.use12HourClock
property bool valueReverseDayMonth: widgetData.reverseDayMonth !== undefined ? widgetData.reverseDayMonth : widgetMetadata.reverseDayMonth
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.displayFormat = valueDisplayFormat
settings.use12HourClock = valueUse12h
settings.reverseDayMonth = valueReverseDayMonth
return settings
}
NComboBox {
label: "Display Format"
model: ListModel {
ListElement {
key: "time"
name: "HH:mm"
}
ListElement {
key: "time-seconds"
name: "HH:mm:ss"
}
ListElement {
key: "time-date"
name: "HH:mm - Date"
}
ListElement {
key: "time-date-short"
name: "HH:mm - Short Date"
}
}
currentKey: valueDisplayFormat
onSelected: key => valueDisplayFormat = key
minimumWidth: 230 * scaling
}
NToggle {
label: "Use 12-hour clock"
checked: valueUse12h
onToggled: checked => valueUse12h = checked
}
NToggle {
label: "Reverse day and month"
checked: valueReverseDayMonth
onToggled: checked => valueReverseDayMonth = checked
}
}

View File

@@ -0,0 +1,231 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
property var widgetData: null
property var widgetMetadata: null
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.icon = iconInput.text
settings.leftClickExec = leftClickExecInput.text
settings.rightClickExec = rightClickExecInput.text
settings.middleClickExec = middleClickExecInput.text
return settings
}
NTextInput {
id: iconInput
Layout.fillWidth: true
label: "Icon Name"
description: "Select an icon from the library."
placeholderText: "Enter icon name (e.g., cat, gear, house, ...)"
text: widgetData?.icon || widgetMetadata.icon
}
RowLayout {
spacing: Style.marginS * scaling
Layout.alignment: Qt.AlignLeft
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: iconInput.text
visible: iconInput.text !== ""
}
NButton {
text: "Browse"
onClicked: iconPicker.open()
}
}
Popup {
id: iconPicker
modal: true
width: {
var w = Math.round(Math.max(Screen.width * 0.35, 900) * scaling)
w = Math.min(w, Screen.width - Style.marginL * 2)
return w
}
height: {
var h = Math.round(Math.max(Screen.height * 0.65, 700) * scaling)
h = Math.min(h, Screen.height - Style.barHeight * scaling - Style.marginL * 2)
return h
}
anchors.centerIn: Overlay.overlay
padding: Style.marginXL * scaling
property string query: ""
property string selectedIcon: ""
property var allIcons: Object.keys(Icons.icons)
property var filteredIcons: allIcons.filter(function (name) {
return query === "" || name.toLowerCase().indexOf(query.toLowerCase()) !== -1
})
readonly property int columns: 6
readonly property int cellW: Math.floor(grid.width / columns)
readonly property int cellH: Math.round(cellW * 0.7 + 36 * scaling)
background: Rectangle {
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Style.borderM * scaling
}
ColumnLayout {
anchors.fill: parent
spacing: Style.marginM * scaling
// Title row
RowLayout {
Layout.fillWidth: true
NText {
text: "Icon Picker"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: iconPicker.close()
}
}
NDivider {
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
NTextInput {
Layout.fillWidth: true
label: "Search"
placeholderText: "Search (e.g., arrow, battery, cloud)"
text: iconPicker.query
onTextChanged: iconPicker.query = text.trim().toLowerCase()
}
}
// Icon grid
NScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AlwaysOn
GridView {
id: grid
anchors.fill: parent
anchors.margins: Style.marginM * scaling
cellWidth: iconPicker.cellW
cellHeight: iconPicker.cellH
model: iconPicker.filteredIcons
delegate: Rectangle {
width: grid.cellWidth
height: grid.cellHeight
radius: Style.radiusS * scaling
clip: true
color: (iconPicker.selectedIcon === modelData) ? Qt.alpha(Color.mPrimary, 0.15) : "transparent"
border.color: (iconPicker.selectedIcon === modelData) ? Color.mPrimary : Qt.rgba(0, 0, 0, 0)
border.width: (iconPicker.selectedIcon === modelData) ? Style.borderS * scaling : 0
MouseArea {
anchors.fill: parent
onClicked: iconPicker.selectedIcon = modelData
onDoubleClicked: {
iconPicker.selectedIcon = modelData
iconInput.text = iconPicker.selectedIcon
iconPicker.close()
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
Item {
Layout.fillHeight: true
Layout.preferredHeight: 4 * scaling
}
NIcon {
Layout.alignment: Qt.AlignHCenter
icon: modelData
font.pointSize: 42 * scaling
}
NText {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.topMargin: Style.marginXS * scaling
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
horizontalAlignment: Text.AlignHCenter
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeXS * scaling
text: modelData
}
Item {
Layout.fillHeight: true
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
Item {
Layout.fillWidth: true
}
NButton {
text: "Cancel"
outlined: true
onClicked: iconPicker.close()
}
NButton {
text: "Apply"
icon: "check"
enabled: iconPicker.selectedIcon !== ""
onClicked: {
iconInput.text = iconPicker.selectedIcon
iconPicker.close()
}
}
}
}
}
NTextInput {
id: leftClickExecInput
Layout.fillWidth: true
label: "Left Click Command"
placeholderText: "Enter command to execute (app or custom script)"
text: widgetData?.leftClickExec || widgetMetadata.leftClickExec
}
NTextInput {
id: rightClickExecInput
Layout.fillWidth: true
label: "Right Click Command"
placeholderText: "Enter command to execute (app or custom script)"
text: widgetData?.rightClickExec || widgetMetadata.rightClickExec
}
NTextInput {
id: middleClickExecInput
Layout.fillWidth: true
label: "Middle Click Command"
placeholderText: "Enter command to execute (app or custom script)"
text: widgetData.middleClickExec || widgetMetadata.middleClickExec
}
}

View File

@@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueForceOpen: widgetData.forceOpen !== undefined ? widgetData.forceOpen : widgetMetadata.forceOpen
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.forceOpen = valueForceOpen
return settings
}
NToggle {
label: "Force open"
description: "Keep the keyboard layout widget always expanded."
checked: valueForceOpen
onToggled: checked => valueForceOpen = checked
}
}

View File

@@ -0,0 +1,62 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt
property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer
property string valueVisualizerType: widgetData.visualizerType || widgetMetadata.visualizerType
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showAlbumArt = valueShowAlbumArt
settings.showVisualizer = valueShowVisualizer
settings.visualizerType = valueVisualizerType
return settings
}
NToggle {
label: "Show album art"
checked: valueShowAlbumArt
onToggled: checked => valueShowAlbumArt = checked
}
NToggle {
label: "Show visualizer"
checked: valueShowVisualizer
onToggled: checked => valueShowVisualizer = checked
}
NComboBox {
visible: valueShowVisualizer
label: "Visualizer type"
model: ListModel {
ListElement {
key: "linear"
name: "Linear"
}
ListElement {
key: "mirrored"
name: "Mirrored"
}
ListElement {
key: "wave"
name: "Wave"
}
}
currentKey: valueVisualizerType
onSelected: key => valueVisualizerType = key
minimumWidth: 200 * scaling
}
}

View File

@@ -0,0 +1,30 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage !== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage
return settings
}
NToggle {
label: "Always show percentage"
checked: valueAlwaysShowPercentage
onToggled: checked => valueAlwaysShowPercentage = checked
}
}

View File

@@ -0,0 +1,38 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueShowUnreadBadge: widgetData.showUnreadBadge !== undefined ? widgetData.showUnreadBadge : widgetMetadata.showUnreadBadge
property bool valueHideWhenZero: widgetData.hideWhenZero !== undefined ? widgetData.hideWhenZero : widgetMetadata.hideWhenZero
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showUnreadBadge = valueShowUnreadBadge
settings.hideWhenZero = valueHideWhenZero
return settings
}
NToggle {
label: "Show unread badge"
checked: valueShowUnreadBadge
onToggled: checked => valueShowUnreadBadge = checked
}
NToggle {
label: "Hide badge when zero"
checked: valueHideWhenZero
onToggled: checked => valueHideWhenZero = checked
}
}

View File

@@ -0,0 +1,30 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.useDistroLogo = valueUseDistroLogo
return settings
}
NToggle {
label: "Use distro logo instead of icon"
checked: valueUseDistroLogo
onToggled: checked => valueUseDistroLogo = checked
}
}

View File

@@ -0,0 +1,30 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.width = parseInt(widthInput.text) || widgetMetadata.width
return settings
}
NTextInput {
id: widthInput
Layout.fillWidth: true
label: "Width"
description: "Spacing width in pixels"
text: widgetData.width || widgetMetadata.width
placeholderText: "Enter width in pixels"
}
}

View File

@@ -0,0 +1,82 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local, editable state for checkboxes
property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage
property bool valueShowCpuTemp: widgetData.showCpuTemp !== undefined ? widgetData.showCpuTemp : widgetMetadata.showCpuTemp
property bool valueShowMemoryUsage: widgetData.showMemoryUsage !== undefined ? widgetData.showMemoryUsage : widgetMetadata.showMemoryUsage
property bool valueShowMemoryAsPercent: widgetData.showMemoryAsPercent !== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
property bool valueShowNetworkStats: widgetData.showNetworkStats !== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats
property bool valueShowDiskUsage: widgetData.showDiskUsage !== undefined ? widgetData.showDiskUsage : widgetMetadata.showDiskUsage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showCpuUsage = valueShowCpuUsage
settings.showCpuTemp = valueShowCpuTemp
settings.showMemoryUsage = valueShowMemoryUsage
settings.showMemoryAsPercent = valueShowMemoryAsPercent
settings.showNetworkStats = valueShowNetworkStats
settings.showDiskUsage = valueShowDiskUsage
return settings
}
NToggle {
id: showCpuUsage
Layout.fillWidth: true
label: "CPU usage"
checked: valueShowCpuUsage
onToggled: checked => valueShowCpuUsage = checked
}
NToggle {
id: showCpuTemp
Layout.fillWidth: true
label: "CPU temperature"
checked: valueShowCpuTemp
onToggled: checked => valueShowCpuTemp = checked
}
NToggle {
id: showMemoryUsage
Layout.fillWidth: true
label: "Memory usage"
checked: valueShowMemoryUsage
onToggled: checked => valueShowMemoryUsage = checked
}
NToggle {
id: showMemoryAsPercent
Layout.fillWidth: true
label: "Memory as percentage"
checked: valueShowMemoryAsPercent
onToggled: checked => valueShowMemoryAsPercent = checked
}
NToggle {
id: showNetworkStats
Layout.fillWidth: true
label: "Network traffic"
checked: valueShowNetworkStats
onToggled: checked => valueShowNetworkStats = checked
}
NToggle {
id: showDiskUsage
Layout.fillWidth: true
label: "Storage usage"
checked: valueShowDiskUsage
onToggled: checked => valueShowDiskUsage = checked
}
}

View File

@@ -0,0 +1,30 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage !== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage
return settings
}
NToggle {
label: "Always show percentage"
checked: valueAlwaysShowPercentage
onToggled: checked => valueAlwaysShowPercentage = checked
}
}

View File

@@ -0,0 +1,53 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.labelMode = labelModeCombo.currentKey
settings.hideUnoccupied = hideUnoccupiedToggle.checked
return settings
}
NComboBox {
id: labelModeCombo
label: "Label Mode"
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "index"
name: "Index"
}
ListElement {
key: "name"
name: "Name"
}
}
currentKey: widgetData.labelMode || widgetMetadata.labelMode
onSelected: key => labelModeCombo.currentKey = key
minimumWidth: 200 * scaling
}
NToggle {
id: hideUnoccupiedToggle
label: "Hide unoccupied"
description: "Don't display workspaces without windows."
checked: widgetData.hideUnoccupied
onToggled: checked => hideUnoccupiedToggle.checked = checked
}
}

View File

@@ -1,433 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
NBox {
id: root
property string sectionName: ""
property string sectionId: ""
property var widgetModel: []
property var availableWidgets: []
signal addWidget(string widgetId, string section)
signal removeWidget(string section, int index)
signal reorderWidget(string section, int fromIndex, int toIndex)
signal updateWidgetSettings(string section, int index, var settings)
color: Color.mSurface
Layout.fillWidth: true
Layout.minimumHeight: {
var widgetCount = widgetModel.length
if (widgetCount === 0)
return 140 * scaling
var availableWidth = parent.width
var avgWidgetWidth = 150 * scaling
var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth))
var rows = Math.ceil(widgetCount / widgetsPerRow)
return (50 + 20 + (rows * 48) + ((rows - 1) * Style.marginS) + 20) * scaling
}
// Generate widget color from name checksum
function getWidgetColor(widget) {
const totalSum = JSON.stringify(widget).split('').reduce((acc, character) => {
return acc + character.charCodeAt(0)
}, 0)
switch (totalSum % 10) {
case 0:
return Color.mPrimary
case 1:
return Color.mSecondary
case 2:
return Color.mTertiary
case 3:
return Color.mError
case 4:
return Color.mOnSurface
case 5:
return Qt.darker(Color.mPrimary, 1.3)
case 6:
return Qt.darker(Color.mSecondary, 1.3)
case 7:
return Qt.darker(Color.mTertiary, 1.3)
case 8:
return Qt.darker(Color.mError, 1.3)
case 9:
return Qt.darker(Color.mOnSurface, 1.3)
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
NText {
text: sectionName + " Section"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
NComboBox {
id: comboBox
model: availableWidgets
label: ""
description: ""
placeholder: "Select a widget to add..."
onSelected: key => comboBox.currentKey = key
popupHeight: 240 * scaling
Layout.alignment: Qt.AlignVCenter
}
NIconButton {
icon: "add"
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
colorBgHover: Color.mSecondary
colorFgHover: Color.mOnSecondary
enabled: comboBox.currentKey !== ""
tooltipText: "Add widget to section"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
onClicked: {
if (comboBox.currentKey !== "") {
addWidget(comboBox.currentKey, sectionId)
comboBox.currentKey = ""
}
}
}
}
// Drag and Drop Widget Area
// Replace your Flow section with this:
// Drag and Drop Widget Area - use Item container
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 65 * scaling
Flow {
id: widgetFlow
anchors.fill: parent
spacing: Style.marginS * scaling
flow: Flow.LeftToRight
Repeater {
model: widgetModel
delegate: Rectangle {
id: widgetItem
required property int index
required property var modelData
width: widgetContent.implicitWidth + Style.marginL * scaling
height: Style.baseWidgetSize * 1.15 * scaling
radius: Style.radiusL * scaling
color: root.getWidgetColor(modelData)
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
// Store the widget index for drag operations
property int widgetIndex: index
readonly property int buttonsWidth: Math.round(20 * scaling)
readonly property int buttonsCount: 1 + BarWidgetRegistry.widgetHasUserSettings(modelData.id)
// Visual feedback during drag
states: State {
when: flowDragArea.draggedIndex === index
PropertyChanges {
target: widgetItem
scale: 1.1
opacity: 0.9
z: 1000
}
}
RowLayout {
id: widgetContent
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NText {
text: modelData.id
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnPrimary
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
Layout.preferredWidth: 80 * scaling
}
RowLayout {
spacing: 0
Layout.preferredWidth: buttonsCount * buttonsWidth
Loader {
active: BarWidgetRegistry.widgetHasUserSettings(modelData.id)
sourceComponent: NIconButton {
icon: "settings"
sizeRatio: 0.6
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
colorBg: Color.mOnSurface
colorFg: Color.mOnPrimary
colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight)
colorFgHover: Color.mOnPrimary
onClicked: {
var dialog = Qt.createComponent("BarWidgetSettingsDialog.qml").createObject(root, {
"widgetIndex": index,
"widgetData": modelData,
"widgetId": modelData.id,
"parent": Overlay.overlay
})
dialog.open()
}
}
}
NIconButton {
icon: "close"
sizeRatio: 0.6
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
colorBg: Color.mOnSurface
colorFg: Color.mOnPrimary
colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight)
colorFgHover: Color.mOnPrimary
onClicked: {
removeWidget(sectionId, index)
}
}
}
}
}
}
}
// MouseArea outside Flow, covering the same area
MouseArea {
id: flowDragArea
anchors.fill: parent
z: 999 // Above all widgets to ensure it gets events first
// Critical properties for proper event handling
acceptedButtons: Qt.LeftButton
preventStealing: false // Prevent child items from stealing events
propagateComposedEvents: draggedIndex != -1 // Don't propagate to children during drag
hoverEnabled: draggedIndex != -1
property point startPos: Qt.point(0, 0)
property bool dragStarted: false
property int draggedIndex: -1
property real dragThreshold: 15 * scaling
property Item draggedWidget: null
property point clickOffsetInWidget: Qt.point(0, 0)
property point originalWidgetPos: Qt.point(0, 0) // ADD THIS: Store original position
onPressed: mouse => {
startPos = Qt.point(mouse.x, mouse.y)
dragStarted = false
draggedIndex = -1
draggedWidget = null
// Find which widget was clicked
for (var i = 0; i < widgetModel.length; i++) {
const widget = widgetFlow.children[i]
if (widget && widget.widgetIndex !== undefined) {
if (mouse.x >= widget.x && mouse.x <= widget.x + widget.width && mouse.y >= widget.y
&& mouse.y <= widget.y + widget.height) {
const localX = mouse.x - widget.x
const buttonsStartX = widget.width - (widget.buttonsCount * widget.buttonsWidth)
if (localX < buttonsStartX) {
draggedIndex = widget.widgetIndex
draggedWidget = widget
// Calculate and store where within the widget the user clicked
const clickOffsetX = mouse.x - widget.x
const clickOffsetY = mouse.y - widget.y
clickOffsetInWidget = Qt.point(clickOffsetX, clickOffsetY)
// STORE ORIGINAL POSITION
originalWidgetPos = Qt.point(widget.x, widget.y)
// Immediately set prevent stealing to true when drag candidate is found
preventStealing = true
break
} else {
// Click was on buttons - allow event propagation
mouse.accepted = false
return
}
}
}
}
}
onPositionChanged: mouse => {
if (draggedIndex !== -1) {
const deltaX = mouse.x - startPos.x
const deltaY = mouse.y - startPos.y
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
if (!dragStarted && distance > dragThreshold) {
dragStarted = true
//Logger.log("BarSectionEditor", "Drag started")
// Enable visual feedback
if (draggedWidget) {
draggedWidget.z = 1000
}
}
if (dragStarted && draggedWidget) {
// Adjust position to account for where within the widget the user clicked
draggedWidget.x = mouse.x - clickOffsetInWidget.x
draggedWidget.y = mouse.y - clickOffsetInWidget.y
}
}
}
onReleased: mouse => {
if (dragStarted && draggedWidget) {
// Find drop target using improved logic
let targetIndex = -1
let minDistance = Infinity
const mouseX = mouse.x
const mouseY = mouse.y
// Check if we should insert at the beginning
let insertAtBeginning = true
let insertAtEnd = true
// Check if the dragged item is already the last item
let isLastItem = true
for (var k = 0; k < widgetModel.length; k++) {
if (k !== draggedIndex && k > draggedIndex) {
isLastItem = false
break
}
}
for (var i = 0; i < widgetModel.length; i++) {
if (i !== draggedIndex) {
const widget = widgetFlow.children[i]
if (widget && widget.widgetIndex !== undefined) {
const centerX = widget.x + widget.width / 2
const centerY = widget.y + widget.height / 2
const distance = Math.sqrt(Math.pow(mouseX - centerX, 2) + Math.pow(mouseY - centerY, 2))
// Check if mouse is to the right of this widget
if (mouseX > widget.x + widget.width / 2) {
insertAtBeginning = false
}
// Check if mouse is to the left of this widget
if (mouseX < widget.x + widget.width / 2) {
insertAtEnd = false
}
if (distance < minDistance) {
minDistance = distance
targetIndex = widget.widgetIndex
}
}
}
}
// If dragging the last item to the right, don't reorder
if (isLastItem && insertAtEnd) {
insertAtEnd = false
targetIndex = -1
//Logger.log("BarSectionEditor", "Last item dropped to right - no reordering needed")
}
// Determine final target index based on position
let finalTargetIndex = targetIndex
if (insertAtBeginning && widgetModel.length > 1) {
// Insert at the very beginning (position 0)
finalTargetIndex = 0
//Logger.log("BarSectionEditor", "Inserting at beginning")
} else if (insertAtEnd && widgetModel.length > 1) {
// Insert at the very end
let maxIndex = -1
for (var j = 0; j < widgetModel.length; j++) {
if (j !== draggedIndex) {
maxIndex = Math.max(maxIndex, j)
}
}
finalTargetIndex = maxIndex
//Logger.log("BarSectionEditor", "Inserting at end, target:", finalTargetIndex)
} else if (targetIndex !== -1) {
// Normal case - determine if we should insert before or after the target
const targetWidget = widgetFlow.children[targetIndex]
if (targetWidget) {
const targetCenterX = targetWidget.x + targetWidget.width / 2
if (mouseX > targetCenterX) {
// Mouse is to the right of target center, insert after
//Logger.log("BarSectionEditor", "Inserting after widget at index:", targetIndex)
} else {
// Mouse is to the left of target center, insert before
finalTargetIndex = targetIndex
//Logger.log("BarSectionEditor", "Inserting before widget at index:", targetIndex)
}
}
}
//Logger.log("BarSectionEditor", "Final drop target index:", finalTargetIndex)
// Check if reordering is needed
if (finalTargetIndex !== -1 && finalTargetIndex !== draggedIndex) {
// Reordering will happen - reset position for the Flow to handle
draggedWidget.x = 0
draggedWidget.y = 0
draggedWidget.z = 0
reorderWidget(sectionId, draggedIndex, finalTargetIndex)
} else {
// No reordering - restore original position
draggedWidget.x = originalWidgetPos.x
draggedWidget.y = originalWidgetPos.y
draggedWidget.z = 0
//Logger.log("BarSectionEditor", "No reordering - restoring original position")
}
} else if (draggedIndex !== -1 && !dragStarted) {
// This was a click without drag - could add click handling here if needed
}
// Reset everything
dragStarted = false
draggedIndex = -1
draggedWidget = null
preventStealing = false // Allow normal event propagation again
originalWidgetPos = Qt.point(0, 0) // Reset stored position
}
// Handle case where mouse leaves the area during drag
onExited: {
if (dragStarted && draggedWidget) {
// Restore original position when mouse leaves area
draggedWidget.x = originalWidgetPos.x
draggedWidget.y = originalWidgetPos.y
draggedWidget.z = 0
}
}
}
}
}
}

View File

@@ -1,160 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
// Widget Settings Dialog Component
Popup {
id: settingsPopup
property int widgetIndex: -1
property var widgetData: null
property string widgetId: ""
// Center popup in parent
x: (parent.width - width) * 0.5
y: (parent.height - height) * 0.5
width: 420 * scaling
height: content.implicitHeight + padding * 2
padding: Style.marginXL * scaling
modal: true
background: Rectangle {
id: bgRect
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Style.borderM * scaling
}
ColumnLayout {
id: content
width: parent.width
spacing: Style.marginM * scaling
// Title
RowLayout {
Layout.fillWidth: true
NText {
text: "Widget Settings: " + settingsPopup.widgetId
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: settingsPopup.close()
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
// Settings based on widget type
Loader {
id: settingsLoader
Layout.fillWidth: true
sourceComponent: {
if (settingsPopup.widgetId === "CustomButton") {
return customButtonSettings
}
// Add more widget settings components here as needed
return null
}
}
// Action buttons
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
Item {
Layout.fillWidth: true
}
NButton {
text: "Cancel"
outlined: true
onClicked: settingsPopup.close()
}
NButton {
text: "Save"
onClicked: {
if (settingsLoader.item && settingsLoader.item.saveSettings) {
var newSettings = settingsLoader.item.saveSettings()
root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings)
settingsPopup.close()
}
}
}
}
}
// CustomButton settings component
Component {
id: customButtonSettings
ColumnLayout {
spacing: Style.marginM * scaling
function saveSettings() {
var settings = Object.assign({}, settingsPopup.widgetData)
settings.icon = iconInput.text
settings.leftClickExec = leftClickExecInput.text
settings.rightClickExec = rightClickExecInput.text
settings.middleClickExec = middleClickExecInput.text
return settings
}
// Icon setting
NTextInput {
id: iconInput
Layout.fillWidth: true
Layout.bottomMargin: Style.marginXL * scaling
label: "Icon Name"
description: "Use Material Icon names from the icon set."
text: settingsPopup.widgetData.icon || ""
placeholderText: "Enter icon name (e.g., favorite, home, settings)"
}
NTextInput {
id: leftClickExecInput
Layout.fillWidth: true
label: "Left Click Command"
description: "Command or application to run when left clicked."
text: settingsPopup.widgetData.leftClickExec || ""
placeholderText: "Enter command to execute (app or custom script)"
}
NTextInput {
id: rightClickExecInput
Layout.fillWidth: true
label: "Right Click Command"
description: "Command or application to run when right clicked."
text: settingsPopup.widgetData.rightClickExec || ""
placeholderText: "Enter command to execute (app or custom script)"
}
NTextInput {
id: middleClickExecInput
Layout.fillWidth: true
label: "Middle Click Command"
description: "Command or application to run when middle clicked."
text: settingsPopup.widgetData.middleClickExec || ""
placeholderText: "Enter command to execute (app or custom script)"
}
}
}
}

View File

@@ -11,16 +11,11 @@ import qs.Widgets
NPanel {
id: root
panelWidth: {
var w = Math.round(Math.max(screen?.width * 0.4, 1000) * scaling)
w = Math.min(w, screen?.width - Style.marginL * 2)
return w
}
panelHeight: {
var h = Math.round(Math.max(screen?.height * 0.75, 800) * scaling)
h = Math.min(h, screen?.height - Style.barHeight * scaling - Style.marginL * 2)
return h
}
preferredWidth: 1000
preferredHeight: 1000
preferredWidthRatio: 0.4
preferredHeightRatio: 0.75
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
@@ -31,15 +26,16 @@ NPanel {
About,
Audio,
Bar,
Dock,
Hooks,
Launcher,
Brightness,
ColorScheme,
Display,
General,
Network,
Notification,
ScreenRecorder,
TimeWeather,
Weather,
Wallpaper,
WallpaperSelector
}
@@ -47,6 +43,7 @@ NPanel {
property int requestedTab: SettingsPanel.Tab.General
property int currentTabIndex: 0
property var tabsModel: []
property var activeScrollView: null
Connections {
target: Settings.data.wallpaper
@@ -71,15 +68,10 @@ NPanel {
id: barTab
Tabs.BarTab {}
}
Component {
id: audioTab
Tabs.AudioTab {}
}
Component {
id: brightnessTab
Tabs.BrightnessTab {}
}
Component {
id: displayTab
Tabs.DisplayTab {}
@@ -89,8 +81,8 @@ NPanel {
Tabs.NetworkTab {}
}
Component {
id: timeWeatherTab
Tabs.TimeWeatherTab {}
id: weatherTab
Tabs.WeatherTab {}
}
Component {
id: colorSchemeTab
@@ -116,58 +108,71 @@ NPanel {
id: hooksTab
Tabs.HooksTab {}
}
Component {
id: dockTab
Tabs.DockTab {}
}
Component {
id: notificationTab
Tabs.NotificationTab {}
}
// Order *DOES* matter
function updateTabsModel() {
let newTabs = [{
"id": SettingsPanel.Tab.General,
"label": "General",
"icon": "tune",
"icon": "settings-general",
"source": generalTab
}, {
"id": SettingsPanel.Tab.Bar,
"label": "Bar",
"icon": "web_asset",
"icon": "settings-bar",
"source": barTab
}, {
"id": SettingsPanel.Tab.Dock,
"label": "Dock",
"icon": "settings-dock",
"source": dockTab
}, {
"id": SettingsPanel.Tab.Launcher,
"label": "Launcher",
"icon": "apps",
"icon": "settings-launcher",
"source": launcherTab
}, {
"id": SettingsPanel.Tab.Audio,
"label": "Audio",
"icon": "volume_up",
"icon": "settings-audio",
"source": audioTab
}, {
"id": SettingsPanel.Tab.Display,
"label": "Display",
"icon": "monitor",
"icon": "settings-display",
"source": displayTab
}, {
"id": SettingsPanel.Tab.Notification,
"label": "Notification",
"icon": "settings-notification",
"source": notificationTab
}, {
"id": SettingsPanel.Tab.Network,
"label": "Network",
"icon": "lan",
"icon": "settings-network",
"source": networkTab
}, {
"id": SettingsPanel.Tab.Brightness,
"label": "Brightness",
"icon": "brightness_6",
"source": brightnessTab
}, {
"id": SettingsPanel.Tab.TimeWeather,
"label": "Time & Weather",
"icon": "schedule",
"source": timeWeatherTab
"id": SettingsPanel.Tab.Weather,
"label": "Weather",
"icon": "settings-weather",
"source": weatherTab
}, {
"id": SettingsPanel.Tab.ColorScheme,
"label": "Color Scheme",
"icon": "palette",
"icon": "settings-color-scheme",
"source": colorSchemeTab
}, {
"id": SettingsPanel.Tab.Wallpaper,
"label": "Wallpaper",
"icon": "image",
"icon": "settings-wallpaper",
"source": wallpaperTab
}]
@@ -176,7 +181,7 @@ NPanel {
newTabs.push({
"id": SettingsPanel.Tab.WallpaperSelector,
"label": "Wallpaper Selector",
"icon": "wallpaper_slideshow",
"icon": "settings-wallpaper-selector",
"source": wallpaperSelectorTab
})
}
@@ -184,17 +189,17 @@ NPanel {
newTabs.push({
"id": SettingsPanel.Tab.ScreenRecorder,
"label": "Screen Recorder",
"icon": "videocam",
"icon": "settings-screen-recorder",
"source": screenRecorderTab
}, {
"id": SettingsPanel.Tab.Hooks,
"label": "Hooks",
"icon": "cable",
"icon": "settings-hooks",
"source": hooksTab
}, {
"id": SettingsPanel.Tab.About,
"label": "About",
"icon": "info",
"icon": "settings-about",
"source": aboutTab
})
@@ -217,153 +222,325 @@ NPanel {
root.currentTabIndex = initialIndex
}
// Add scroll functions
function scrollDown() {
if (activeScrollView && activeScrollView.ScrollBar.vertical) {
const scrollBar = activeScrollView.ScrollBar.vertical
const stepSize = activeScrollView.height * 0.1 // Scroll 10% of viewport
scrollBar.position = Math.min(scrollBar.position + stepSize / activeScrollView.contentHeight, 1.0 - scrollBar.size)
}
}
function scrollUp() {
if (activeScrollView && activeScrollView.ScrollBar.vertical) {
const scrollBar = activeScrollView.ScrollBar.vertical
const stepSize = activeScrollView.height * 0.1 // Scroll 10% of viewport
scrollBar.position = Math.max(scrollBar.position - stepSize / activeScrollView.contentHeight, 0)
}
}
function scrollPageDown() {
if (activeScrollView && activeScrollView.ScrollBar.vertical) {
const scrollBar = activeScrollView.ScrollBar.vertical
const pageSize = activeScrollView.height * 0.9 // Scroll 90% of viewport
scrollBar.position = Math.min(scrollBar.position + pageSize / activeScrollView.contentHeight, 1.0 - scrollBar.size)
}
}
function scrollPageUp() {
if (activeScrollView && activeScrollView.ScrollBar.vertical) {
const scrollBar = activeScrollView.ScrollBar.vertical
const pageSize = activeScrollView.height * 0.9 // Scroll 90% of viewport
scrollBar.position = Math.max(scrollBar.position - pageSize / activeScrollView.contentHeight, 0)
}
}
// Add navigation functions
function selectNextTab() {
if (tabsModel.length > 0) {
currentTabIndex = (currentTabIndex + 1) % tabsModel.length
}
}
function selectPreviousTab() {
if (tabsModel.length > 0) {
currentTabIndex = (currentTabIndex - 1 + tabsModel.length) % tabsModel.length
}
}
panelContent: Rectangle {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
color: Color.transparent
RowLayout {
// Main layout container that fills the panel
ColumnLayout {
anchors.fill: parent
spacing: Style.marginM * scaling
anchors.margins: Style.marginL * scaling
spacing: 0
Rectangle {
id: sidebar
Layout.preferredWidth: 220 * scaling
Layout.fillHeight: true
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
// Keyboard shortcuts container
Item {
Layout.preferredWidth: 0
Layout.preferredHeight: 0
Column {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginXS * 1.5 * scaling
// Scrolling via keyboard
Shortcut {
sequence: "Down"
onActivated: root.scrollDown()
enabled: root.opened
}
Repeater {
id: sections
model: root.tabsModel
delegate: Rectangle {
id: tabItem
width: parent.width
height: 32 * scaling
radius: Style.radiusS * scaling
color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent)
readonly property bool selected: index === currentTabIndex
property bool hovering: false
property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface)
Shortcut {
sequence: "Up"
onActivated: root.scrollUp()
enabled: root.opened
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Shortcut {
sequence: "Ctrl+J"
onActivated: root.scrollDown()
enabled: root.opened
}
Behavior on tabTextColor {
ColorAnimation {
duration: Style.animationFast
}
}
Shortcut {
sequence: "Ctrl+K"
onActivated: root.scrollUp()
enabled: root.opened
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginS * scaling
// Tab icon on the left side
NIcon {
text: modelData.icon
color: tabTextColor
font.pointSize: Style.fontSizeL * scaling
}
// Tab label on the left side
NText {
text: modelData.label
color: tabTextColor
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onEntered: tabItem.hovering = true
onExited: tabItem.hovering = false
onCanceled: tabItem.hovering = false
onClicked: currentTabIndex = index
}
}
}
Shortcut {
sequence: "PgDown"
onActivated: root.scrollPageDown()
enabled: root.opened
}
Shortcut {
sequence: "PgUp"
onActivated: root.scrollPageUp()
enabled: root.opened
}
// Changing tab via keyboard
Shortcut {
sequence: "Tab"
onActivated: root.selectNextTab()
enabled: root.opened
}
Shortcut {
sequence: "Shift+Tab"
onActivated: root.selectPreviousTab()
enabled: root.opened
}
}
// Content
Rectangle {
id: contentPane
// Main content area
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
clip: true
spacing: Style.marginM * scaling
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginS * scaling
// Sidebar
Rectangle {
id: sidebar
Layout.preferredWidth: 220 * scaling
Layout.fillHeight: true
Layout.alignment: Qt.AlignTop
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
RowLayout {
id: headerRow
Layout.fillWidth: true
spacing: Style.marginS * scaling
// Tab label on the main right side
NText {
text: root.tabsModel[currentTabIndex].label
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
tooltipText: "Close"
Layout.alignment: Qt.AlignVCenter
onClicked: root.close()
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton // Don't interfere with clicks
property int wheelAccumulator: 0
onWheel: wheel => {
wheelAccumulator += wheel.angleDelta.y
if (wheelAccumulator >= 120) {
root.selectPreviousTab()
wheelAccumulator = 0
} else if (wheelAccumulator <= -120) {
root.selectNextTab()
wheelAccumulator = 0
}
wheel.accepted = true
}
}
NDivider {
Layout.fillWidth: true
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginXS * scaling
Repeater {
id: sections
model: root.tabsModel
delegate: Loader {
anchors.fill: parent
active: index === root.currentTabIndex
sourceComponent: ColumnLayout {
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
padding: Style.marginL * scaling
clip: true
delegate: Rectangle {
id: tabItem
Layout.fillWidth: true
Layout.preferredHeight: tabEntryRow.implicitHeight + Style.marginS * scaling * 2
radius: Style.radiusS * scaling
color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent)
readonly property bool selected: index === currentTabIndex
property bool hovering: false
property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface)
Loader {
active: true
sourceComponent: root.tabsModel[index].source
width: scrollView.availableWidth
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on tabTextColor {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
id: tabEntryRow
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginM * scaling
// Tab icon
NIcon {
icon: modelData.icon
color: tabTextColor
font.pointSize: Style.fontSizeXL * scaling
}
// Tab label
NText {
text: modelData.label
color: tabTextColor
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onEntered: tabItem.hovering = true
onExited: tabItem.hovering = false
onCanceled: tabItem.hovering = false
onClicked: currentTabIndex = index
}
}
}
Item {
Layout.fillHeight: true
}
}
}
// Content pane
Rectangle {
id: contentPane
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignTop
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
clip: true
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginS * scaling
// Header row
RowLayout {
id: headerRow
Layout.fillWidth: true
spacing: Style.marginS * scaling
// Main icon
NIcon {
icon: root.tabsModel[currentTabIndex]?.icon
color: Color.mPrimary
font.pointSize: Style.fontSizeXXL * scaling
}
// Main title
NText {
text: root.tabsModel[currentTabIndex]?.label || ""
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
// Close button
NIconButton {
icon: "close"
tooltipText: "Close."
Layout.alignment: Qt.AlignVCenter
onClicked: root.close()
}
}
// Divider
NDivider {
Layout.fillWidth: true
}
// Tab content area
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.transparent
Repeater {
model: root.tabsModel
delegate: Loader {
anchors.fill: parent
active: index === root.currentTabIndex
onStatusChanged: {
if (status === Loader.Ready && item) {
// Find and store reference to the ScrollView
const scrollView = item.children[0]
if (scrollView && scrollView.toString().includes("ScrollView")) {
root.activeScrollView = scrollView
}
}
}
sourceComponent: Flickable {
// Using a Flickable here with a pressDelay to fix conflict between
// ScrollView and NTextInput. This fixes the weird text selection issue.
id: flickable
anchors.fill: parent
pressDelay: 200
NScrollView {
id: scrollView
anchors.fill: parent
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
padding: Style.marginL * scaling
clip: true
Component.onCompleted: {
root.activeScrollView = scrollView
}
Loader {
active: true
sourceComponent: root.tabsModel[index]?.source
width: scrollView.availableWidth
}
}
}
}

View File

@@ -10,106 +10,108 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL * scaling
property string latestVersion: GitHubService.latestVersion
property string currentVersion: UpdateService.currentVersion
property var contributors: GitHubService.contributors
NText {
text: "Noctalia Shell"
font.pointSize: Style.fontSizeXXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Noctalia Shell"
description: "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell."
}
// Versions
GridLayout {
Layout.alignment: Qt.AlignCenter
columns: 2
rowSpacing: Style.marginXS * scaling
columnSpacing: Style.marginS * scaling
RowLayout {
spacing: Style.marginL * scaling
NText {
text: "Latest Version:"
color: Color.mOnSurface
Layout.alignment: Qt.AlignRight
}
// Versions
GridLayout {
columns: 2
rowSpacing: Style.marginXS * scaling
columnSpacing: Style.marginS * scaling
NText {
text: root.latestVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
NText {
text: "Installed Version:"
color: Color.mOnSurface
Layout.alignment: Qt.AlignRight
}
NText {
text: root.currentVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
}
// Updater
Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.topMargin: Style.marginS * scaling
Layout.preferredWidth: updateText.implicitWidth + 46 * scaling
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
radius: Style.radiusL * scaling
color: updateArea.containsMouse ? Color.mPrimary : Color.transparent
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderS * scaling)
visible: {
if (root.latestVersion === "Unknown")
return false
const latest = root.latestVersion.replace("v", "").split(".")
const current = root.currentVersion.replace("v", "").split(".")
for (var i = 0; i < Math.max(latest.length, current.length); i++) {
const l = parseInt(latest[i] || "0")
const c = parseInt(current[i] || "0")
if (l > c)
return true
if (l < c)
return false
}
return false
}
RowLayout {
anchors.centerIn: parent
spacing: Style.marginS * scaling
NIcon {
text: "system_update"
font.pointSize: Style.fontSizeXXL * scaling
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
NText {
text: "Latest Version:"
color: Color.mOnSurface
}
NText {
id: updateText
text: "Download latest release"
font.pointSize: Style.fontSizeL * scaling
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
text: root.latestVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
NText {
text: "Installed Version:"
color: Color.mOnSurface
}
NText {
text: root.currentVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
}
MouseArea {
id: updateArea
Item {
Layout.fillWidth: true
}
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"])
// Update button
Rectangle {
Layout.alignment: Qt.alignmentRight
Layout.preferredWidth: Math.round(updateRow.implicitWidth + (Style.marginL * scaling * 2))
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
radius: Style.radiusL * scaling
color: updateArea.containsMouse ? Color.mPrimary : Color.transparent
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderS * scaling)
visible: {
if (root.latestVersion === "Unknown")
return false
const latest = root.latestVersion.replace("v", "").split(".")
const current = root.currentVersion.replace("v", "").split(".")
for (var i = 0; i < Math.max(latest.length, current.length); i++) {
const l = parseInt(latest[i] || "0")
const c = parseInt(current[i] || "0")
if (l > c)
return true
if (l < c)
return false
}
return false
}
RowLayout {
id: updateRow
anchors.centerIn: parent
spacing: Style.marginS * scaling
NIcon {
icon: "download"
font.pointSize: Style.fontSizeXXL * scaling
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
}
NText {
id: updateText
text: "Download latest release"
font.pointSize: Style.fontSizeL * scaling
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
}
}
MouseArea {
id: updateArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["xdg-open", "https://github.com/Ly-sec/Noctalia/releases/latest"])
}
}
}
}
@@ -120,17 +122,13 @@ ColumnLayout {
Layout.bottomMargin: Style.marginXL * scaling
}
NText {
text: `Shout-out to our ${root.contributors.length} <b>awesome</b> contributors!`
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignCenter
NHeader {
label: "Contributors"
description: `Shout-out to our ${root.contributors.length} <b>awesome</b> contributors!`
}
GridView {
id: contributorsGrid
Layout.topMargin: Style.marginL * scaling
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: cellWidth * 3 // Fixed 3 columns
Layout.preferredHeight: {

View File

@@ -8,6 +8,12 @@ import qs.Services
ColumnLayout {
id: root
spacing: Style.marginL * scaling
NHeader {
label: "Volumes"
description: "Configure volume controls and audio levels."
}
property real localVolume: AudioService.volume
@@ -20,7 +26,7 @@ ColumnLayout {
// Master Volume
ColumnLayout {
spacing: Style.marginS * scaling
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
@@ -67,7 +73,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NToggle {
label: "Mute Audio Output"
@@ -83,9 +88,8 @@ ColumnLayout {
// Input Volume
ColumnLayout {
spacing: Style.marginS * scaling
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NLabel {
label: "Input Volume"
@@ -117,7 +121,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NToggle {
label: "Mute Audio Input"
@@ -131,7 +134,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NSpinBox {
Layout.fillWidth: true
@@ -158,12 +160,9 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginS * scaling
NText {
text: "Audio Devices"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Audio Devices"
description: "Configure audio input and output devices."
}
// -------------------------------
@@ -203,7 +202,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL * scaling
NLabel {
label: "Input Device"
@@ -234,29 +232,12 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginL * scaling
NText {
text: "Media Player"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Media Player"
description: "Configure your favorite media players."
}
// Miniplayer section
NToggle {
label: "Show Album Art In Bar Media Player"
description: "Show the album art of the currently playing song next to the title."
checked: Settings.data.audio.showMiniplayerAlbumArt
onToggled: checked => Settings.data.audio.showMiniplayerAlbumArt = checked
}
NToggle {
label: "Show Audio Visualizer In Bar Media Player"
description: "Shows an audio visualizer in the background of the miniplayer."
checked: Settings.data.audio.showMiniplayerCava
onToggled: checked => Settings.data.audio.showMiniplayerCava = checked
}
// Preferred player (persistent)
// Preferred player
NTextInput {
label: "Preferred Player"
description: "Substring to match MPRIS player (identity/bus/desktop)."
@@ -374,12 +355,9 @@ ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NText {
text: "Audio Visualizer"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Audio Visualizer"
description: "Customize visual effects that respond to audio playback."
}
// AudioService Visualizer section

View File

@@ -1,125 +1,224 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.SettingsPanel.Extras
import qs.Modules.SettingsPanel.Bar
ColumnLayout {
id: root
spacing: Style.marginL * scaling
ColumnLayout {
spacing: Style.marginL * scaling
// Helper functions to update arrays immutably
function addMonitor(list, name) {
const arr = (list || []).slice()
if (!arr.includes(name))
arr.push(name)
return arr
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name
})
}
RowLayout {
NComboBox {
Layout.fillWidth: true
label: "Bar Position"
description: "Choose where to place the bar on the screen."
model: ListModel {
ListElement {
key: "top"
name: "Top"
}
ListElement {
key: "bottom"
name: "Bottom"
}
}
currentKey: Settings.data.bar.position
onSelected: key => Settings.data.bar.position = key
}
// Handler for drag start - disables panel background clicks
function handleDragStart() {
var panel = PanelService.getPanel("settingsPanel")
if (panel && panel.disableBackgroundClick) {
panel.disableBackgroundClick()
}
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Background Opacity"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Adjust the background opacity of the bar."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.backgroundOpacity
onMoved: Settings.data.bar.backgroundOpacity = value
cutoutColor: Color.mSurface
}
NText {
text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
// Handler for drag end - re-enables panel background clicks
function handleDragEnd() {
var panel = PanelService.getPanel("settingsPanel")
if (panel && panel.enableBackgroundClick) {
panel.enableBackgroundClick()
}
}
NToggle {
label: "Show Active Window's Icon"
description: "Display the app icon next to the title of the currently focused window."
checked: Settings.data.bar.showActiveWindowIcon
onToggled: checked => Settings.data.bar.showActiveWindowIcon = checked
}
NToggle {
label: "Show Battery Percentage"
description: "Display battery percentage at all times."
checked: Settings.data.bar.alwaysShowBatteryPercentage
onToggled: checked => Settings.data.bar.alwaysShowBatteryPercentage = checked
}
NToggle {
label: "Show Network Statistics"
description: "Display network upload and download speeds in the system monitor."
checked: Settings.data.bar.showNetworkStats
onToggled: checked => Settings.data.bar.showNetworkStats = checked
}
NToggle {
label: "Replace SidePanel toggle with distro logo"
description: "Show distro logo instead of the SidePanel toggle button in the bar."
checked: Settings.data.bar.useDistroLogo
onToggled: checked => {
Settings.data.bar.useDistroLogo = checked
}
}
NHeader {
label: "Appearance"
description: "Configure bar appearance and positioning."
}
RowLayout {
NComboBox {
label: "Show Workspaces Labels"
description: "Show the workspace name or index within the workspace indicator."
Layout.fillWidth: true
label: "Bar Position"
description: "Choose where to place the bar on the screen."
model: ListModel {
ListElement {
key: "none"
name: "None"
key: "top"
name: "Top"
}
ListElement {
key: "index"
name: "Index"
key: "bottom"
name: "Bottom"
}
ListElement {
key: "name"
name: "Name"
key: "left"
name: "Left"
}
ListElement {
key: "right"
name: "Right"
}
}
currentKey: Settings.data.bar.showWorkspaceLabel
onSelected: key => Settings.data.bar.showWorkspaceLabel = key
currentKey: Settings.data.bar.position
onSelected: key => Settings.data.bar.position = key
}
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
label: "Background Opacity"
description: "Adjust the background opacity of the bar."
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.backgroundOpacity
onMoved: Settings.data.bar.backgroundOpacity = value
cutoutColor: Color.mSurface
}
NText {
text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
NToggle {
Layout.fillWidth: true
label: "Floating Bar"
description: "Make the bar float with rounded corners and margins. This will hide screen corners."
checked: Settings.data.bar.floating
onToggled: checked => Settings.data.bar.floating = checked
}
// Floating bar options - only show when floating is enabled
ColumnLayout {
visible: Settings.data.bar.floating
spacing: Style.marginS * scaling
Layout.fillWidth: true
NLabel {
label: "Margins"
description: "Adjust the margins around the floating bar."
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL * scaling
ColumnLayout {
spacing: Style.marginXXS * scaling
NText {
text: "Vertical"
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.marginVertical
onMoved: Settings.data.bar.marginVertical = value
cutoutColor: Color.mSurface
}
NText {
text: Math.round(Settings.data.bar.marginVertical * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginXS * scaling
Layout.preferredWidth: 50
horizontalAlignment: Text.AlignRight
color: Color.mOnSurface
}
}
}
ColumnLayout {
spacing: Style.marginXXS * scaling
NText {
text: "Horizontal"
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.marginHorizontal
onMoved: Settings.data.bar.marginHorizontal = value
cutoutColor: Color.mSurface
}
NText {
text: Math.round(Settings.data.bar.marginHorizontal * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginXS * scaling
Layout.preferredWidth: 50
horizontalAlignment: Text.AlignRight
color: Color.mOnSurface
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Monitor Configuration
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NHeader {
label: "Monitors Configuration"
description: "Choose which monitors should display the bar."
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: `${modelData.name || "Unknown"}${modelData.model ? `: ${modelData.model}` : ""}`
description: `${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})`
checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name)
} else {
Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name)
}
}
}
}
}
@@ -134,20 +233,9 @@ ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Widgets Positioning"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
NText {
text: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
NHeader {
label: "Widgets Positioning"
description: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
}
// Bar Sections
@@ -167,6 +255,8 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
}
// Center Section
@@ -179,6 +269,8 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
}
// Right Section
@@ -191,6 +283,8 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
}
}
}
@@ -230,8 +324,7 @@ ColumnLayout {
}
function _reorderWidgetInSection(section, fromIndex, toIndex) {
if (fromIndex >= 0 && fromIndex < Settings.data.bar.widgets[section].length && toIndex >= 0
&& toIndex < Settings.data.bar.widgets[section].length) {
if (fromIndex >= 0 && fromIndex < Settings.data.bar.widgets[section].length && toIndex >= 0 && toIndex < Settings.data.bar.widgets[section].length) {
// Create a new array to avoid modifying the original
var newArray = Settings.data.bar.widgets[section].slice()

View File

@@ -1,324 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
// Time dropdown options (00:00 .. 23:30)
ListModel {
id: timeOptions
}
Component.onCompleted: {
for (var h = 0; h < 24; h++) {
for (var m = 0; m < 60; m += 30) {
var hh = ("0" + h).slice(-2)
var mm = ("0" + m).slice(-2)
var key = hh + ":" + mm
timeOptions.append({
"key": key,
"name": key
})
}
}
}
// Check for wlsunset availability when enabling Night Light
Process {
id: wlsunsetCheck
command: ["which", "wlsunset"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
Settings.data.nightLight.enabled = true
NightLightService.apply()
ToastService.showNotice("Night Light", "Enabled")
} else {
Settings.data.nightLight.enabled = false
ToastService.showWarning("Night Light", "wlsunset not installed")
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
spacing: Style.marginL * scaling
// Brightness Step Section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NSpinBox {
Layout.fillWidth: true
label: "Brightness Step Size"
description: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)."
minimum: 1
maximum: 50
value: Settings.data.brightness.brightnessStep
stepSize: 1
suffix: "%"
onValueChanged: {
Settings.data.brightness.brightnessStep = value
}
}
}
// Monitor Overview Section
ColumnLayout {
spacing: Style.marginL * scaling
NLabel {
label: "Monitors Brightness Control"
description: "Current brightness levels for all detected monitors."
}
// Single monitor display using the same data source as the bar icon
Repeater {
model: BrightnessService.monitors
Rectangle {
Layout.fillWidth: true
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
ColumnLayout {
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: `${model.modelData.name} [${model.modelData.model}]`
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
Item {
Layout.fillWidth: true
}
NText {
text: model.method
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignRight
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: "Brightness:"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
}
NSlider {
Layout.fillWidth: true
from: 0
to: 1
value: model.brightness
stepSize: 0.05
onPressedChanged: {
if (!pressed) {
var monitor = BrightnessService.getMonitorForScreen(model.modelData)
monitor.setBrightness(value)
}
}
}
NText {
text: Math.round(model.brightness * 100) + "%"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignRight
}
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Night Light Section
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
NText {
text: "Night Light"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: "Reduce blue light emission to help you sleep better and reduce eye strain."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
NToggle {
label: "Enable Night Light"
description: "Apply a warm color filter to reduce blue light emission."
checked: Settings.data.nightLight.enabled
onToggled: checked => {
if (checked) {
// Verify wlsunset exists before enabling
wlsunsetCheck.running = true
} else {
Settings.data.nightLight.enabled = false
NightLightService.apply()
ToastService.showNotice("Night Light", "Disabled")
}
}
}
// Temperature
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
NLabel {
label: "Color temperature"
description: "Choose two temperatures in Kelvin."
}
RowLayout {
visible: Settings.data.nightLight.enabled
spacing: Style.marginM * scaling
Layout.fillWidth: false
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
NText {
text: "Night"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.nightTemp
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var nightTemp = parseInt(text)
var dayTemp = parseInt(Settings.data.nightLight.dayTemp)
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
// Clamp value between [1000 .. (dayTemp-500)]
var clampedValue = Math.min(dayTemp - 500, Math.max(1000, nightTemp))
text = Settings.data.nightLight.nightTemp = clampedValue.toString()
}
}
}
Item {}
NText {
text: "Day"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.dayTemp
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var dayTemp = parseInt(text)
var nightTemp = parseInt(Settings.data.nightLight.nightTemp)
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
// Clamp value between [(nightTemp+500) .. 6500]
var clampedValue = Math.max(nightTemp + 500, Math.min(6500, dayTemp))
text = Settings.data.nightLight.dayTemp = clampedValue.toString()
}
}
}
}
}
NToggle {
label: "Automatic Scheduling"
description: `Based on the sunset and sunrise time in <i>${LocationService.stableName}</i> - recommended.`
checked: Settings.data.nightLight.autoSchedule
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
visible: Settings.data.nightLight.enabled
}
// Schedule settings
ColumnLayout {
spacing: Style.marginXS * scaling
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule
RowLayout {
Layout.fillWidth: false
spacing: Style.marginM * scaling
NLabel {
label: "Manual Scheduling"
}
Item {// add a little more spacing
}
NText {
text: "Sunrise Time"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
}
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunrise
placeholder: "Select start time"
onSelected: key => Settings.data.nightLight.manualSunrise = key
preferredWidth: 120 * scaling
}
Item {// add a little more spacing
}
NText {
text: "Sunset Time"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
}
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunset
placeholder: "Select stop time"
onSelected: key => Settings.data.nightLight.manualSunset = key
preferredWidth: 120 * scaling
}
}
}
}

View File

@@ -8,6 +8,7 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL * scaling
// Cache for scheme JSON (can be flat or {dark, light})
property var schemeColorsCache: ({})
@@ -103,225 +104,206 @@ ColumnLayout {
}
}
ColumnLayout {
spacing: 0
// Main Toggles - Dark Mode / Matugen
NHeader {
label: "Behavior"
description: "Main settings for Noctalia's colors."
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
NToggle {
label: "Dark Mode"
description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available."
checked: Settings.data.colorSchemes.darkMode
enabled: true
onToggled: checked => Settings.data.colorSchemes.darkMode = checked
}
// Use Matugen
NToggle {
label: "Enable Matugen"
description: "Automatically generate colors based on your active wallpaper."
checked: Settings.data.colorSchemes.useWallpaperColors
onToggled: checked => {
if (checked) {
// Check if matugen is installed
matugenCheck.running = true
} else {
Settings.data.colorSchemes.useWallpaperColors = false
ToastService.showNotice("Matugen", "Disabled")
if (Settings.data.colorSchemes.predefinedScheme) {
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Predefined Color Schemes
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NHeader {
label: "Predefined Color Schemes"
description: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper."
}
ColumnLayout {
spacing: Style.marginL * scaling
// Color Schemes Grid
GridLayout {
columns: 3
rowSpacing: Style.marginM * scaling
columnSpacing: Style.marginM * scaling
Layout.fillWidth: true
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
NToggle {
label: "Dark Mode"
description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available."
checked: Settings.data.colorSchemes.darkMode
enabled: true
onToggled: checked => Settings.data.colorSchemes.darkMode = checked
}
Repeater {
model: ColorSchemeService.schemes
// Use Matugen
NToggle {
label: "Enable Matugen"
description: "Automatically generate colors based on your active wallpaper."
checked: Settings.data.colorSchemes.useWallpaperColors
onToggled: checked => {
if (checked) {
// Check if matugen is installed
matugenCheck.running = true
} else {
Settings.data.colorSchemes.useWallpaperColors = false
ToastService.showNotice("Matugen", "Disabled")
Rectangle {
id: schemeCard
if (Settings.data.colorSchemes.predefinedScheme) {
property string schemePath: modelData
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NText {
text: "Predefined Color Schemes"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: "These color schemes are only active when 'Use Matugen' is turned off. With Matugen enabled, colors will be automatically generated from your wallpaper. You can still switch between light and dark themes while using Matugen."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
Layout.preferredHeight: 120 * scaling
radius: Style.radiusM * scaling
color: getSchemeColor(modelData, "mSurface")
border.width: Math.max(1, Style.borderL * scaling)
border.color: (!Settings.data.colorSchemes.useWallpaperColors && (Settings.data.colorSchemes.predefinedScheme === modelData.split("/").pop().replace(".json", ""))) ? Color.mSecondary : Color.mOutline
scale: root.cardScaleLow
// Color Schemes Grid
GridLayout {
columns: 3
rowSpacing: Style.marginM * scaling
columnSpacing: Style.marginM * scaling
Layout.fillWidth: true
// Mouse area for selection
MouseArea {
anchors.fill: parent
onClicked: {
// Disable useWallpaperColors when picking a predefined color scheme
Settings.data.colorSchemes.useWallpaperColors = false
Logger.log("ColorSchemeTab", "Disabled matugen setting")
Repeater {
model: ColorSchemeService.schemes
Settings.data.colorSchemes.predefinedScheme = schemePath.split("/").pop().replace(".json", "")
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
}
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
Rectangle {
id: schemeCard
property string schemePath: modelData
Layout.fillWidth: true
Layout.preferredHeight: 120 * scaling
radius: Style.radiusM * scaling
color: getSchemeColor(modelData, "mSurface")
border.width: Math.max(1, Style.borderL * scaling)
border.color: (!Settings.data.colorSchemes.useWallpaperColors
&& (Settings.data.colorSchemes.predefinedScheme === modelData)) ? Color.mPrimary : Color.mOutline
scale: root.cardScaleLow
// Mouse area for selection
MouseArea {
anchors.fill: parent
onClicked: {
// Disable useWallpaperColors when picking a predefined color scheme
Settings.data.colorSchemes.useWallpaperColors = false
Logger.log("ColorSchemeTab", "Disabled matugen setting")
Settings.data.colorSchemes.predefinedScheme = schemePath
ColorSchemeService.applyScheme(schemePath)
}
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
schemeCard.scale = root.cardScaleHigh
}
onExited: {
schemeCard.scale = root.cardScaleLow
}
onEntered: {
schemeCard.scale = root.cardScaleHigh
}
// Card content
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginXL * scaling
onExited: {
schemeCard.scale = root.cardScaleLow
}
}
// Card content
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginXL * scaling
spacing: Style.marginS * scaling
// Scheme name
NText {
text: {
// Remove json and the full path
var chunks = schemePath.replace(".json", "").split("/")
return chunks[chunks.length - 1]
}
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: getSchemeColor(modelData, "mOnSurface")
Layout.fillWidth: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
// Color swatches
RowLayout {
id: swatches
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
// Scheme name
NText {
text: {
// Remove json and the full path
var chunks = schemePath.replace(".json", "").split("/")
return chunks[chunks.length - 1]
}
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: getSchemeColor(modelData, "mOnSurface")
Layout.fillWidth: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
readonly property int swatchSize: 20 * scaling
// Primary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mPrimary")
}
// Color swatches
RowLayout {
id: swatches
// Secondary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mSecondary")
}
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
// Tertiary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mTertiary")
}
readonly property int swatchSize: 20 * scaling
// Primary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mPrimary")
}
// Secondary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mSecondary")
}
// Tertiary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mTertiary")
}
// Error color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mError")
}
// Error color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mError")
}
}
}
// Selection indicator (Checkmark)
Rectangle {
visible: !Settings.data.colorSchemes.useWallpaperColors
&& (Settings.data.colorSchemes.predefinedScheme === schemePath)
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginS * scaling
width: 24 * scaling
height: 24 * scaling
radius: width * 0.5
color: Color.mPrimary
// Selection indicator (Checkmark)
Rectangle {
visible: !Settings.data.colorSchemes.useWallpaperColors && (Settings.data.colorSchemes.predefinedScheme === schemePath.split("/").pop().replace(".json", ""))
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginS * scaling
width: 28 * scaling
height: 28 * scaling
radius: width * 0.5
color: Color.mSecondary
NText {
anchors.centerIn: parent
text: "✓"
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
NIcon {
icon: "check"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSecondary
anchors.centerIn: parent
}
}
// Smooth animations
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
// Smooth animations
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationNormal
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationNormal
}
}
Behavior on border.width {
NumberAnimation {
duration: Style.animationFast
}
Behavior on border.width {
NumberAnimation {
duration: Style.animationFast
}
}
}

View File

@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
@@ -9,49 +10,68 @@ import qs.Widgets
ColumnLayout {
id: root
// Helper functions to update arrays immutably
function addMonitor(list, name) {
const arr = (list || []).slice()
if (!arr.includes(name))
arr.push(name)
return arr
// Time dropdown options (00:00 .. 23:30)
ListModel {
id: timeOptions
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name
})
Component.onCompleted: {
for (var h = 0; h < 24; h++) {
for (var m = 0; m < 60; m += 30) {
var hh = ("0" + h).slice(-2)
var mm = ("0" + m).slice(-2)
var key = hh + ":" + mm
timeOptions.append({
"key": key,
"name": key
})
}
}
}
NText {
text: "Monitor-specific configuration"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
// Check for wlsunset availability when enabling Night Light
Process {
id: wlsunsetCheck
command: ["which", "wlsunset"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
Settings.data.nightLight.enabled = true
NightLightService.apply()
ToastService.showNotice("Night Light", "Enabled")
} else {
Settings.data.nightLight.enabled = false
ToastService.showWarning("Night Light", "wlsunset not installed")
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
NText {
text: "Bars and notifications appear on all displays by default. Choose specific displays below to limit where they're shown."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
spacing: Style.marginL * scaling
NHeader {
label: "Monitor-specific configuration"
description: "Configure scaling and brightness settings individually for each connected display."
}
ColumnLayout {
spacing: Style.marginL * scaling
Layout.topMargin: Style.marginL * scaling
Repeater {
model: Quickshell.screens || []
delegate: Rectangle {
Layout.fillWidth: true
Layout.minimumWidth: 550 * scaling
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
property real localScaling: ScalingService.getScreenScale(modelData)
property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData)
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
@@ -68,122 +88,116 @@ ColumnLayout {
spacing: Style.marginXXS * scaling
NText {
text: (modelData.name || "Unknown")
font.pointSize: Style.fontSizeXL * scaling
text: (`${modelData.name}: ${modelData.model}` || "Unknown")
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
color: Color.mPrimary
}
NText {
text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})`
text: `Resolution: ${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})`
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Scale
ColumnLayout {
spacing: Style.marginL * scaling
spacing: Style.marginS * scaling
Layout.fillWidth: true
NToggle {
Layout.fillWidth: true
label: "Bar"
description: "Enable the bar on this monitor."
checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name)
} else {
Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name)
}
}
}
NToggle {
Layout.fillWidth: true
label: "Notifications"
description: "Enable notifications on this monitor."
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors,
modelData.name)
} else {
Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors,
modelData.name)
}
}
}
NToggle {
Layout.fillWidth: true
label: "Dock"
description: "Enable the dock on this monitor."
checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name)
} else {
Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name)
}
}
}
ColumnLayout {
spacing: Style.marginS * scaling
RowLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL * scaling
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Scale"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Scale the user interface on this monitor."
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
NText {
text: `${Math.round(localScaling * 100)}%`
Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: 50 * scaling
horizontalAlignment: Text.AlignRight
}
NText {
text: "Scale"
Layout.preferredWidth: 80 * scaling
}
RowLayout {
spacing: Style.marginS * scaling
NSlider {
id: scaleSlider
from: 0.7
to: 1.8
stepSize: 0.01
value: localScaling
onPressedChanged: ScalingService.setScreenScale(modelData, value)
Layout.fillWidth: true
Layout.minimumWidth: 200 * scaling
}
NSlider {
id: scaleSlider
from: 0.7
to: 1.8
stepSize: 0.01
value: localScaling
onPressedChanged: ScalingService.setScreenScale(modelData, value)
Layout.fillWidth: true
Layout.minimumWidth: 150 * scaling
}
NText {
text: `${Math.round(localScaling * 100)}%`
Layout.preferredWidth: 50 * scaling
horizontalAlignment: Text.AlignRight
}
// Reset button container
Item {
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 30 * scaling
NIconButton {
icon: "refresh"
sizeRatio: 0.8
tooltipText: "Reset scaling"
onClicked: ScalingService.setScreenScale(modelData, 1.0)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
// Brightness
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
visible: brightnessMonitor !== undefined && brightnessMonitor !== null
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: "Brightness"
Layout.preferredWidth: 80 * scaling
}
NSlider {
Layout.fillWidth: true
Layout.minimumWidth: 200 * scaling
from: 0
to: 1
value: brightnessMonitor ? brightnessMonitor.brightness : 0.5
stepSize: 0.05
onPressedChanged: {
if (!pressed && brightnessMonitor) {
brightnessMonitor.setBrightness(value)
}
}
}
NText {
text: brightnessMonitor ? Math.round(brightnessMonitor.brightness * 100) + "%" : "N/A"
Layout.preferredWidth: 50 * scaling
horizontalAlignment: Text.AlignRight
}
// Empty container to match scale row layout
Item {
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 30 * scaling
// Method text positioned in the button area
NText {
text: brightnessMonitor ? brightnessMonitor.method : ""
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignRight
}
}
}
@@ -192,4 +206,218 @@ ColumnLayout {
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Brightness Section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NHeader {
label: "Brightness"
description: "Adjust brightness related settings."
}
// Brightness Step Section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NSpinBox {
Layout.fillWidth: true
label: "Brightness Step Size"
description: "Adjust the step size for brightness changes (scroll wheel and keyboard shortcuts)."
minimum: 1
maximum: 50
value: Settings.data.brightness.brightnessStep
stepSize: 1
suffix: "%"
onValueChanged: {
Settings.data.brightness.brightnessStep = value
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Night Light Section
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
NHeader {
label: "Night Light"
description: "Reduce blue light emission to help you sleep better and reduce eye strain."
}
}
NToggle {
label: "Enable Night Light"
description: "Apply a warm color filter to reduce blue light emission."
checked: Settings.data.nightLight.enabled
onToggled: checked => {
if (checked) {
// Verify wlsunset exists before enabling
wlsunsetCheck.running = true
} else {
Settings.data.nightLight.enabled = false
Settings.data.nightLight.forced = false
NightLightService.apply()
ToastService.showNotice("Night Light", "Disabled")
}
}
}
// Temperature
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
NLabel {
label: "Color temperature"
description: "Choose two temperatures in Kelvin."
}
RowLayout {
visible: Settings.data.nightLight.enabled
spacing: Style.marginM * scaling
Layout.fillWidth: false
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
NText {
text: "Night"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.nightTemp
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var nightTemp = parseInt(text)
var dayTemp = parseInt(Settings.data.nightLight.dayTemp)
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
// Clamp value between [1000 .. (dayTemp-500)]
var clampedValue = Math.min(dayTemp - 500, Math.max(1000, nightTemp))
text = Settings.data.nightLight.nightTemp = clampedValue.toString()
}
}
}
Item {}
NText {
text: "Day"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.dayTemp
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var dayTemp = parseInt(text)
var nightTemp = parseInt(Settings.data.nightLight.nightTemp)
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
// Clamp value between [(nightTemp+500) .. 6500]
var clampedValue = Math.max(nightTemp + 500, Math.min(6500, dayTemp))
text = Settings.data.nightLight.dayTemp = clampedValue.toString()
}
}
}
}
}
NToggle {
label: "Automatic Scheduling"
description: `Based on the sunset and sunrise time in <i>${LocationService.stableName}</i> - recommended.`
checked: Settings.data.nightLight.autoSchedule
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
visible: Settings.data.nightLight.enabled
}
// Schedule settings
ColumnLayout {
spacing: Style.marginXS * scaling
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
RowLayout {
Layout.fillWidth: false
spacing: Style.marginM * scaling
NLabel {
label: "Manual Scheduling"
}
Item {// add a little more spacing
}
NText {
text: "Sunrise Time"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
}
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunrise
placeholder: "Select start time"
onSelected: key => Settings.data.nightLight.manualSunrise = key
minimumWidth: 120 * scaling
}
Item {// add a little more spacing
}
NText {
text: "Sunset Time"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
}
NComboBox {
model: timeOptions
currentKey: Settings.data.nightLight.manualSunset
placeholder: "Select stop time"
onSelected: key => Settings.data.nightLight.manualSunset = key
minimumWidth: 120 * scaling
}
}
}
// Force activation toggle
NToggle {
label: "Force activation"
description: "Immediately apply night temperature without scheduling or fade."
checked: Settings.data.nightLight.forced
onToggled: checked => {
Settings.data.nightLight.forced = checked
if (checked && !Settings.data.nightLight.enabled) {
// Ensure enabled when forcing
wlsunsetCheck.running = true
} else {
NightLightService.apply()
}
}
visible: Settings.data.nightLight.enabled
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

Some files were not shown because too many files have changed in this diff Show More