diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json
index 01311b44..5c1dfeec 100644
--- a/Assets/Translations/de.json
+++ b/Assets/Translations/de.json
@@ -32,16 +32,6 @@
"reset": "Eckenradius des Bildschirms zurücksetzen"
}
},
- "lockscreen": {
- "section": {
- "label": "Sperrbildschirm",
- "description": "Hier kannst du deinen Sperrbildschirm konfigurieren."
- },
- "lock-on-suspend": {
- "label": "Beim Standby sperren",
- "description": "Bildschirm automatisch sperren, wenn das System in den Standby-Modus wechselt."
- }
- },
"fonts": {
"section": {
"label": "Schriftarten",
@@ -79,7 +69,8 @@
"description": "Wählen die in der Anwendungsoberfläche verwendete Sprache.",
"auto-detect": "Automatisch"
}
- }
+ },
+ "launch-setup-wizard": "Starte den Setup-Assistenten"
},
"audio": {
"title": "Audio",
@@ -243,13 +234,13 @@
"widgets": {
"section": {
"label": "Widget-Positionierung",
- "description": "Widgets per Drag & Drop neu anordnen. Abzeichen zeigen die Verwendung an: [L]inks, [M]itte, [R]echts."
+ "description": "Widgets per Drag & Drop neu anordnen. Symoble zeigen die Verwendung an: [L]inks, [M]itte, [R]echts."
}
},
"monitors": {
"section": {
"label": "Monitor-Anzeige",
- "description": "Statusleiste auf bestimmten Monitoren anzeigen. Standard ist alle, wenn keine ausgewählt sind."
+ "description": "Leiste auf bestimmten Monitoren anzeigen. Standardmäßig auf allen."
}
},
"tray": {
@@ -319,7 +310,7 @@
},
"clipboard-history": {
"label": "Zwischenablage-Verlauf aktivieren",
- "description": "Zugriff auf zuvor kopierte Elemente über den Starter."
+ "description": "Zugriff auf zuvor kopierte Elemente über den Launcher."
},
"sort-by-usage": {
"label": "Nach Häufigkeit sortieren",
@@ -343,8 +334,8 @@
"description": "Erscheinungsbild und Verhalten von Benachrichtigungen konfigurieren."
},
"do-not-disturb": {
- "label": "Nicht stören",
- "description": "Alle Benachrichtigungs-Popups deaktivieren, wenn aktiviert."
+ "label": "Bitte Nicht stören",
+ "description": "Alle Benachrichtigungs-Popups deaktivieren."
},
"enable-osd": {
"label": "Bildschirmanzeige aktivieren",
@@ -384,35 +375,35 @@
"monitors": {
"section": {
"label": "Monitor-Anzeige",
- "description": "Benachrichtigungen auf bestimmten Monitoren anzeigen. Standard ist alle, wenn keine ausgewählt sind."
+ "description": "Benachrichtigungen auf bestimmten Monitoren anzeigen. Standardmäßig werden sie auf allen Monitoren angezeigt"
}
}
},
"osd": {
- "title": "Bildschirmanzeige",
+ "title": "On-Screen Display",
"description": "Bildschirm-Overlays wie Lautstärke- und Helligkeitsanzeigen konfigurieren.",
"section": {
"general": {
"label": "Allgemein",
- "description": "Sichtbarkeit und Verhalten der OSD konfigurieren."
+ "description": "Sichtbarkeit und Verhalten vom On-Screen Display konfigurieren."
}
},
"enabled": {
- "label": "Bildschirmanzeige aktivieren",
+ "label": "On-Screen Display aktivieren",
"description": "Lautstärke- und Helligkeitsänderungen in Echtzeit anzeigen."
},
"always-on-top": {
"label": "Immer im Vordergrund",
- "description": "Bildschirmanzeige über Vollbildfenstern und anderen Ebenen anzeigen."
+ "description": "On-Screen Display über Vollbildfenstern und anderen Ebenen anzeigen."
},
"location": {
"label": "Position",
- "description": "Wo Bildschirmanzeigen erscheinen."
+ "description": "Wo On-Screen Displays erscheinen."
},
"duration": {
"section": {
"label": "Automatisches Ausblenden",
- "description": "Wie lange die OSD sichtbar bleibt, bevor sie automatisch ausgeblendet wird."
+ "description": "Wie lange das On-Screen Display sichtbar bleibt, bevor es automatisch ausgeblendet wird."
},
"auto-hide": {
"label": "Ausblenden nach",
@@ -422,7 +413,7 @@
"monitors": {
"section": {
"label": "Monitor-Anzeige",
- "description": "OSD auf bestimmten Monitoren anzeigen. Standard ist alle, wenn keine ausgewählt sind."
+ "description": "On-Screen Display auf bestimmten Monitoren anzeigen. Standardmäßig auf allen angezeigt."
}
}
},
@@ -471,7 +462,7 @@
"description": "Dauer der Übergangsanimationen in Sekunden."
},
"edge-smoothness": {
- "label": "Übergangskante weichzeichnen",
+ "label": "Übergangseffect weichzeichnen",
"description": "Wendet einen weichen, gefiederten Effekt auf die Kante von Übergängen an."
}
},
@@ -551,15 +542,15 @@
"description": "Terminal-Emulator-Theming.",
"kitty": {
"description": "Schreibt {filepath} und lädt neu",
- "description-missing": "Erfordert kitty Terminal"
+ "description-missing": "Erfordert {app} Terminal"
},
"ghostty": {
"description": "Schreibt {filepath} und lädt neu",
- "description-missing": "Erfordert ghostty Terminal"
+ "description-missing": "Erfordert {app} Terminal"
},
"foot": {
"description": "Schreibt {filepath} und lädt neu",
- "description-missing": "Erfordert foot Terminal"
+ "description-missing": "Erfordert {app} Terminal"
}
},
"programs": {
@@ -567,11 +558,11 @@
"description": "Anwendungsspezifisches Theming.",
"fuzzel": {
"description": "Schreibt {filepath} und lädt neu",
- "description-missing": "Erfordert fuzzel Starter"
+ "description-missing": "Erfordert die Installation von {app}"
},
"vicinae": {
"description": "Schreibt {filepath} und lädt neu",
- "description-missing": "Erfordert {app} Starter"
+ "description-missing": "Erfordert die Installation von {app}"
},
"discord": {
"description": "Schreibt {filepath} für {client}",
@@ -579,7 +570,7 @@
},
"pywalfox": {
"description": "Schreibt {filepath} und führt pywalfox update aus",
- "description-missing": "Erfordert pywalfox Paket"
+ "description-missing": "Erfordert die Installation von {app} "
}
},
"misc": {
@@ -605,7 +596,7 @@
},
"search": {
"label": "Nach einem Standort suchen",
- "description": "z.B. Berlin, Deutschland",
+ "description": "z.B. Dortmund, Deutschland",
"placeholder": "Standortnamen eingeben"
}
},
@@ -634,7 +625,7 @@
},
"week-numbers": {
"label": "Wochennummern anzeigen",
- "description": "Zeigt die Woche des Jahres (z.B. Woche 38) im Kalender an."
+ "description": "Zeigt die Kalender Wochen an (z.B. Woche 38)"
}
}
},
@@ -727,7 +718,7 @@
"description_plural": "Ein Dankeschön an unsere {count} großartigen Mitwirkenden!"
}
},
- "support": "Unterstützen Sie uns"
+ "support": "Unterstütz uns"
},
"hooks": {
"title": "Hooks",
@@ -792,6 +783,10 @@
"description": "Deaktivieren Sie alle Animationen für eine schnellere und reaktionsfreudigere Erfahrung.",
"label": "UI-Animationen deaktivieren"
},
+ "panels-overlay": {
+ "label": "Panels in Overlay-Ebene öffnen",
+ "description": "Panels werden über Vollbildfenstern angezeigt"
+ },
"animation-speed": {
"description": "Globale Animationsgeschwindigkeit anpassen.",
"label": "Animationsgeschwindigkeit",
@@ -802,10 +797,6 @@
"label": "Eckenradius",
"reset": "Rahmenradius zurücksetzen"
},
- "compact-lockscreen": {
- "description": "Zeige nur die Login-Eingabe und Systemsteuerung, blende Wetter- und Medien-Widgets aus.",
- "label": "Kompakter Sperrbildschirm"
- },
"dim-desktop": {
"description": "Den Desktop abdunkeln, wenn Bedienfelder oder Menüs geöffnet sind.",
"label": "Dim Desktop"
@@ -824,6 +815,17 @@
"description": "Tooltips in der gesamten Benutzeroberfläche aktivieren oder deaktivieren.",
"label": "Tooltips anzeigen"
}
+ },
+ "lock-screen": {
+ "compact-lockscreen": {
+ "description": "Zeige nur die Login-Eingabe und Systemsteuerung, blende Wetter- und Medien-Widgets aus.",
+ "label": "Kompakter Sperrbildschirm"
+ },
+ "lock-on-suspend": {
+ "description": "Den Bildschirm beim Suspendieren des Systems automatisch sperren.",
+ "label": "Sperren beim Ruhezustand"
+ },
+ "title": "Sperrbildschirm"
}
},
"general": {
@@ -836,7 +838,7 @@
"mainly-clear": "Überwiegend klar",
"partly-cloudy": "Teilweise bewölkt",
"overcast": "Bedeckt",
- "fog": "Nebel",
+ "fog": "Nebelig",
"drizzle": "Nieselregen",
"snow": "Schnee",
"rain-showers": "Regenschauer",
@@ -852,7 +854,7 @@
"select-file": "Datei auswählen",
"cancel": "Abbrechen",
"search-placeholder": "Dateien und Ordner suchen...",
- "select-current": "Aktuelle auswählen",
+ "select-current": "Aktuelles Objekt auswählen",
"title": "Dateiauswahl"
},
"datetime-tokens": {
diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json
index 88acd5c9..b4814d84 100644
--- a/Assets/Translations/en.json
+++ b/Assets/Translations/en.json
@@ -2,6 +2,7 @@
"settings": {
"general": {
"title": "General",
+ "launch-setup-wizard": "Launch the setup wizard",
"profile": {
"section": {
"label": "Profile",
@@ -32,16 +33,6 @@
"reset": "Reset screen corners radius"
}
},
- "lockscreen": {
- "section": {
- "label": "Lock screen",
- "description": "Configure lock screen behavior."
- },
- "lock-on-suspend": {
- "label": "Lock on suspend",
- "description": "Automatically lock the screen when suspending the system."
- }
- },
"fonts": {
"reset-scaling": "Reset scaling",
"section": {
@@ -806,10 +797,6 @@
"label": "Dim desktop",
"description": "Dim the desktop when panels or menus are open."
},
- "compact-lockscreen": {
- "label": "Compact lock screen",
- "description": "Show only the login input and system controls, hiding weather and media widgets."
- },
"border-radius": {
"label": "Border radius",
"description": "Controls the corner roundness of windows, buttons, and other elements.",
@@ -823,6 +810,21 @@
"animation-disable": {
"label": "Disable UI Animations",
"description": "Disable all animations for a faster, more responsive experience."
+ },
+ "panels-overlay": {
+ "label": "Open panels in overlay layer",
+ "description": "Panels will appear above fullscreen windows"
+ }
+ },
+ "lock-screen": {
+ "title": "Lock screen",
+ "compact-lockscreen": {
+ "label": "Compact lock screen",
+ "description": "Show only the login input and system controls, hiding weather and media widgets."
+ },
+ "lock-on-suspend": {
+ "label": "Lock on suspend",
+ "description": "Automatically lock the screen when suspending the system."
}
}
},
diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json
index 68d2010c..10bc18bc 100644
--- a/Assets/Translations/es.json
+++ b/Assets/Translations/es.json
@@ -32,16 +32,6 @@
"reset": "Restablecer el radio de las esquinas de la pantalla"
}
},
- "lockscreen": {
- "section": {
- "label": "Pantalla de bloqueo",
- "description": "Configura el comportamiento de la pantalla de bloqueo."
- },
- "lock-on-suspend": {
- "label": "Bloquear al suspender",
- "description": "Bloquear automáticamente la pantalla al suspender el sistema."
- }
- },
"fonts": {
"section": {
"label": "Fuentes",
@@ -79,7 +69,8 @@
"description": "Selecciona el idioma utilizado en la interfaz de la aplicación.",
"auto-detect": "Automático"
}
- }
+ },
+ "launch-setup-wizard": "Inicie el asistente de configuración"
},
"audio": {
"title": "Audio",
@@ -513,6 +504,13 @@
"switch": {
"label": "Modo oscuro",
"description": "Cambia a un tema más oscuro para una visualización más fácil por la noche."
+ },
+ "mode": {
+ "description": "Permite el cambio automático entre el modo claro y el modo oscuro.",
+ "label": "Programación del modo oscuro",
+ "location": "Ubicación",
+ "manual": "Manual",
+ "off": "Apagado"
}
},
"predefined": {
@@ -785,6 +783,10 @@
"description": "Desactiva todas las animaciones para una experiencia más rápida y con mayor capacidad de respuesta.",
"label": "Desactivar animaciones de la interfaz de usuario"
},
+ "panels-overlay": {
+ "label": "Abrir paneles en capa superpuesta",
+ "description": "Los paneles aparecerán sobre las ventanas en pantalla completa"
+ },
"animation-speed": {
"description": "Ajustar la velocidad global de la animación.",
"label": "Velocidad de animación",
@@ -795,10 +797,6 @@
"label": "Radio de borde",
"reset": "Restablecer el radio del borde"
},
- "compact-lockscreen": {
- "description": "Mostrar solo el campo de inicio de sesión y los controles del sistema, ocultando los widgets del clima y multimedia.",
- "label": "Pantalla de bloqueo compacta"
- },
"dim-desktop": {
"description": "Atenuar el escritorio cuando los paneles o menús estén abiertos.",
"label": "Dim escritorio"
@@ -817,6 +815,17 @@
"description": "Activar o desactivar los avisos emergentes en toda la interfaz.",
"label": "Mostrar sugerencias"
}
+ },
+ "lock-screen": {
+ "compact-lockscreen": {
+ "description": "Mostrar solo el campo de inicio de sesión y los controles del sistema, ocultando los widgets del clima y multimedia.",
+ "label": "Pantalla de bloqueo compacta"
+ },
+ "lock-on-suspend": {
+ "description": "Bloquear la pantalla automáticamente al suspender el sistema.",
+ "label": "Bloquear al suspender"
+ },
+ "title": "Pantalla de bloqueo"
}
},
"widgets": {
diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json
index b20b2308..790220ed 100644
--- a/Assets/Translations/fr.json
+++ b/Assets/Translations/fr.json
@@ -32,16 +32,6 @@
"reset": "Réinitialiser le rayon des coins de l'écran"
}
},
- "lockscreen": {
- "section": {
- "label": "Écran de verrouillage",
- "description": "Configurer le comportement de l'écran de verrouillage."
- },
- "lock-on-suspend": {
- "label": "Verrouiller lors de la suspension",
- "description": "Verrouiller automatiquement l'écran lors de la mise en veille du système."
- }
- },
"fonts": {
"section": {
"label": "Polices",
@@ -79,7 +69,8 @@
"description": "Sélectionnez la langue utilisée dans l'interface de l'application.",
"auto-detect": "Automatique"
}
- }
+ },
+ "launch-setup-wizard": "Lancer l'assistant d'installation"
},
"audio": {
"title": "Audio",
@@ -513,6 +504,13 @@
"switch": {
"label": "Mode sombre",
"description": "Passe à un thème plus sombre pour une visualisation plus facile la nuit."
+ },
+ "mode": {
+ "description": "Active la commutation automatique entre le mode clair et le mode sombre.",
+ "label": "Programmation du mode sombre",
+ "location": "Emplacement",
+ "manual": "Manuel",
+ "off": "Éteint"
}
},
"predefined": {
@@ -785,6 +783,10 @@
"description": "Désactiver toutes les animations pour une expérience plus rapide et plus réactive.",
"label": "Désactiver les animations de l'interface utilisateur"
},
+ "panels-overlay": {
+ "label": "Ouvrir les panneaux en couche superposée",
+ "description": "Les panneaux apparaîtront au-dessus des fenêtres en plein écran"
+ },
"animation-speed": {
"description": "Ajuster la vitesse globale de l'animation.",
"label": "Vitesse d'animation",
@@ -795,10 +797,6 @@
"label": "Rayon de bordure",
"reset": "Réinitialiser le rayon de la bordure"
},
- "compact-lockscreen": {
- "description": "Afficher uniquement le champ de saisie de connexion et les commandes système, en masquant les widgets météo et multimédia.",
- "label": "Écran de verrouillage compact"
- },
"dim-desktop": {
"description": "Atténuer le bureau lorsque des panneaux ou des menus sont ouverts.",
"label": "Dim bureau"
@@ -817,6 +815,17 @@
"description": "Activer ou désactiver les info-bulles dans toute l'interface.",
"label": "Afficher les infobulles"
}
+ },
+ "lock-screen": {
+ "compact-lockscreen": {
+ "description": "Afficher uniquement le champ de saisie de connexion et les commandes système, en masquant les widgets météo et multimédia.",
+ "label": "Écran de verrouillage compact"
+ },
+ "lock-on-suspend": {
+ "description": "Verrouiller automatiquement l'écran lors de la mise en veille du système.",
+ "label": "Verrouiller à la suspension"
+ },
+ "title": "Écran de verrouillage"
}
},
"widgets": {
diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json
index d78a4ec6..0be6fdf2 100644
--- a/Assets/Translations/pt.json
+++ b/Assets/Translations/pt.json
@@ -32,16 +32,6 @@
"reset": "Redefinir raio dos cantos da tela"
}
},
- "lockscreen": {
- "section": {
- "label": "Tela de bloqueio",
- "description": "Configure o comportamento da tela de bloqueio."
- },
- "lock-on-suspend": {
- "label": "Bloquear ao suspender",
- "description": "Bloquear automaticamente a tela ao suspender o sistema."
- }
- },
"fonts": {
"section": {
"label": "Fontes",
@@ -79,7 +69,8 @@
"description": "Selecione o idioma usado na interface da aplicação.",
"auto-detect": "Automático"
}
- }
+ },
+ "launch-setup-wizard": "Iniciar o assistente de configuração"
},
"audio": {
"title": "Áudio",
@@ -475,6 +466,13 @@
"switch": {
"label": "Modo escuro",
"description": "Muda para um tema mais escuro para facilitar a visualização à noite."
+ },
+ "mode": {
+ "description": "Ativa a mudança automática entre o modo claro e o modo escuro.",
+ "label": "Agendamento do modo escuro",
+ "location": "Localização",
+ "manual": "Manual",
+ "off": "Desligado"
}
},
"predefined": {
@@ -785,6 +783,10 @@
"description": "Desative todas as animações para uma experiência mais rápida e responsiva.",
"label": "Desativar animações da interface do usuário"
},
+ "panels-overlay": {
+ "label": "Abrir painéis na camada de sobreposição",
+ "description": "Os painéis aparecerão sobre janelas em tela cheia"
+ },
"animation-speed": {
"description": "Ajustar a velocidade global da animação.",
"label": "Velocidade da animação",
@@ -795,10 +797,6 @@
"label": "Raio da borda",
"reset": "Redefinir raio da borda"
},
- "compact-lockscreen": {
- "description": "Mostrar apenas a entrada de login e os controles do sistema, ocultando widgets de clima e mídia.",
- "label": "Tela de bloqueio compacta"
- },
"dim-desktop": {
"description": "Escurecer a área de trabalho quando painéis ou menus estiverem abertos.",
"label": "Dim área de trabalho"
@@ -817,6 +815,17 @@
"description": "Ativar ou desativar dicas de ferramentas em toda a interface.",
"label": "Mostrar dicas de ferramenta"
}
+ },
+ "lock-screen": {
+ "compact-lockscreen": {
+ "description": "Mostrar apenas a entrada de login e os controles do sistema, ocultando os widgets de clima e mídia.",
+ "label": "Tela de bloqueio compacta"
+ },
+ "lock-on-suspend": {
+ "description": "Bloquear a tela automaticamente ao suspender o sistema.",
+ "label": "Bloquear ao suspender"
+ },
+ "title": "Tela de bloqueio"
}
},
"widgets": {
diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json
index 0159a41a..90c4cecf 100644
--- a/Assets/Translations/zh-CN.json
+++ b/Assets/Translations/zh-CN.json
@@ -32,16 +32,6 @@
"reset": "重置屏幕圆角半径"
}
},
- "lockscreen": {
- "section": {
- "label": "锁屏",
- "description": "配置锁屏行为。"
- },
- "lock-on-suspend": {
- "label": "挂起时锁定",
- "description": "在系统挂起时自动锁定屏幕。"
- }
- },
"fonts": {
"reset-scaling": "恢复默认缩放",
"section": {
@@ -79,7 +69,8 @@
"description": "选择应用程序界面中使用的语言。",
"auto-detect": "自动检测"
}
- }
+ },
+ "launch-setup-wizard": "启动安装向导"
},
"audio": {
"title": "音频",
@@ -513,6 +504,13 @@
"switch": {
"label": "深色模式",
"description": "切换到更暗的主题,便于夜间观看。"
+ },
+ "mode": {
+ "description": "启用自动切换浅色和深色模式。",
+ "label": "深色模式计划",
+ "location": "位置",
+ "manual": "手册",
+ "off": "关"
}
},
"predefined": {
@@ -785,6 +783,10 @@
"description": "禁用所有动画以获得更快、更流畅的体验。",
"label": "禁用 UI 动画"
},
+ "panels-overlay": {
+ "label": "在覆盖层中打开面板",
+ "description": "面板将显示在全屏窗口上方"
+ },
"animation-speed": {
"description": "调整全局动画速度。",
"label": "动画速度",
@@ -795,10 +797,6 @@
"label": "边框半径",
"reset": "重置边框半径"
},
- "compact-lockscreen": {
- "description": "仅显示登录输入和系统控制,隐藏天气和媒体小部件。",
- "label": "紧凑型锁屏"
- },
"dim-desktop": {
"description": "当面板或菜单打开时,桌面变暗。",
"label": "昏暗的桌面"
@@ -817,6 +815,17 @@
"description": "启用或禁用整个界面的工具提示。",
"label": "显示工具提示"
}
+ },
+ "lock-screen": {
+ "compact-lockscreen": {
+ "description": "仅显示登录输入和系统控制,隐藏天气和媒体小部件。",
+ "label": "紧凑型锁屏"
+ },
+ "lock-on-suspend": {
+ "description": "系统挂起时自动锁定屏幕。",
+ "label": "挂起时锁定"
+ },
+ "title": "锁屏"
}
},
"widgets": {
diff --git a/Assets/settings-default.json b/Assets/settings-default.json
index 57033237..1bf17a16 100644
--- a/Assets/settings-default.json
+++ b/Assets/settings-default.json
@@ -179,24 +179,23 @@
"network": {
"wifiEnabled": true
},
- "notifications": {
- "doNotDisturb": false,
- "monitors": [],
- "location": "top_right",
- "alwaysOnTop": false,
- "lastSeenTs": 0,
- "respectExpireTimeout": false,
- "lowUrgencyDuration": 3,
- "normalUrgencyDuration": 8,
- "criticalUrgencyDuration": 15
- },
- "osd": {
- "enabled": true,
- "location": "top_right",
- "monitors": [],
- "autoHideMs": 2000,
- "alwaysOnTop": false
- },
+ "notifications": {
+ "doNotDisturb": false,
+ "monitors": [],
+ "location": "top_right",
+ "overlayLayer": true,
+ "respectExpireTimeout": false,
+ "lowUrgencyDuration": 3,
+ "normalUrgencyDuration": 8,
+ "criticalUrgencyDuration": 15
+ },
+ "osd": {
+ "enabled": true,
+ "location": "top_right",
+ "monitors": [],
+ "autoHideMs": 2000,
+ "overlayLayer": true
+ },
"audio": {
"volumeStep": 5,
"volumeOverdrive": false,
@@ -210,7 +209,8 @@
"fontFixed": "DejaVu Sans Mono",
"fontDefaultScale": 1,
"fontFixedScale": 1,
- "tooltipsEnabled": true
+ "tooltipsEnabled": true,
+ "panelsOverlayLayer": true
},
"brightness": {
"brightnessStep": 5
diff --git a/Bin/calendar-events.py b/Bin/calendar-events.py
new file mode 100755
index 00000000..febfe2ed
--- /dev/null
+++ b/Bin/calendar-events.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+import gi
+
+gi.require_version('EDataServer', '1.2')
+gi.require_version('ECal', '2.0')
+import json
+import sys
+import time
+from datetime import datetime, timezone
+
+from gi.repository import ECal, EDataServer
+
+start_time = int(sys.argv[1])
+end_time = int(sys.argv[2])
+
+print(f"Starting with time range: {start_time} to {end_time}", file=sys.stderr)
+
+all_events = []
+
+def safe_get_time(ical_time):
+ """Safely get time from ICalTime object"""
+ if not ical_time:
+ return None
+
+ try:
+ year = ical_time.get_year()
+ month = ical_time.get_month()
+ day = ical_time.get_day()
+
+ if year < 1970 or year > 2100 or month < 1 or month > 12 or day < 1 or day > 31:
+ return None
+
+ if ical_time.is_date():
+ local_struct = time.struct_time((year, month, day, 0, 0, 0, 0, 0, -1))
+ return int(time.mktime(local_struct))
+
+ hour = ical_time.get_hour()
+ minute = ical_time.get_minute()
+ second = ical_time.get_second()
+
+ dt = datetime(year, month, day, hour, minute, second, tzinfo=timezone.utc)
+ return int(dt.timestamp())
+ except Exception:
+ return None
+
+print("Getting registry...", file=sys.stderr)
+registry = EDataServer.SourceRegistry.new_sync(None)
+print("Registry obtained", file=sys.stderr)
+
+sources = registry.list_sources(EDataServer.SOURCE_EXTENSION_CALENDAR)
+print(f"Found {len(sources)} calendar sources", file=sys.stderr)
+
+for source in sources:
+ if not source.get_enabled():
+ print(f"Skipping disabled calendar: {source.get_display_name()}", file=sys.stderr)
+ continue
+
+ calendar_name = source.get_display_name()
+ print(f"\nProcessing calendar: {calendar_name}", file=sys.stderr)
+
+ try:
+ print(f" Connecting to {calendar_name}...", file=sys.stderr)
+ client = ECal.Client.connect_sync(
+ source,
+ ECal.ClientSourceType.EVENTS,
+ 30,
+ None
+ )
+ print(f" Connected to {calendar_name}", file=sys.stderr)
+
+ start_dt = datetime.fromtimestamp(start_time, tz=timezone.utc)
+ end_dt = datetime.fromtimestamp(end_time, tz=timezone.utc)
+
+ start_str = start_dt.strftime("%Y%m%dT%H%M%SZ")
+ end_str = end_dt.strftime("%Y%m%dT%H%M%SZ")
+
+ query = f'(occur-in-time-range? (make-time "{start_str}") (make-time "{end_str}"))'
+ print(f" Query: {query}", file=sys.stderr)
+
+ print(f" Getting object list for {calendar_name}...", file=sys.stderr)
+ success, ical_objects = client.get_object_list_sync(query, None)
+ print(f" Got object list for {calendar_name}: success={success}, count={len(ical_objects) if ical_objects else 0}", file=sys.stderr)
+
+ if not success or not ical_objects:
+ print(f" No events found in {calendar_name}", file=sys.stderr)
+ continue
+
+ print(f" Processing {len(ical_objects)} events from {calendar_name}...", file=sys.stderr)
+ for idx, ical_obj in enumerate(ical_objects):
+ try:
+ if hasattr(ical_obj, 'get_summary'):
+ comp = ical_obj
+ else:
+ comp = ECal.Component.new_from_string(ical_obj)
+
+ if not comp:
+ continue
+
+ summary = comp.get_summary() or "(No title)"
+
+ start_timestamp = safe_get_time(comp.get_dtstart())
+ if start_timestamp is None:
+ continue
+
+ end_timestamp = safe_get_time(comp.get_dtend())
+ if end_timestamp is None or end_timestamp == start_timestamp:
+ end_timestamp = start_timestamp + 3600
+
+ location = comp.get_location() or ""
+ description = comp.get_description() or ""
+
+ all_events.append({
+ 'summary': summary,
+ 'start': start_timestamp,
+ 'end': end_timestamp,
+ 'location': location,
+ 'description': description,
+ 'calendar': calendar_name
+ })
+
+ if (idx + 1) % 10 == 0:
+ print(f" Processed {idx + 1} events from {calendar_name}...", file=sys.stderr)
+ except Exception as e:
+ print(f" Error processing event {idx} in {calendar_name}: {e}", file=sys.stderr)
+ continue
+
+ print(f" Finished processing {calendar_name}, found {len([e for e in all_events if e['calendar'] == calendar_name])} events", file=sys.stderr)
+
+ except Exception as e:
+ print(f" Error for {calendar_name}: {e}", file=sys.stderr)
+
+print(f"\nSorting {len(all_events)} total events...", file=sys.stderr)
+all_events.sort(key=lambda x: x['start'])
+print("Done! Outputting JSON...", file=sys.stderr)
+print(json.dumps(all_events))
diff --git a/Bin/check-calendar.py b/Bin/check-calendar.py
new file mode 100755
index 00000000..463d2e40
--- /dev/null
+++ b/Bin/check-calendar.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python3
+import gi
+
+gi.require_version('EDataServer', '1.2')
+gi.require_version('ECal', '2.0')
+
+try:
+ from gi.repository import ECal, EDataServer
+ print("available")
+except ImportError as e:
+ print(f"unavailable: {e}")
diff --git a/Bin/list-calendars.py b/Bin/list-calendars.py
new file mode 100755
index 00000000..12c67bd1
--- /dev/null
+++ b/Bin/list-calendars.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+import gi
+
+gi.require_version('EDataServer', '1.2')
+import json
+
+from gi.repository import EDataServer
+
+registry = EDataServer.SourceRegistry.new_sync(None)
+sources = registry.list_sources(EDataServer.SOURCE_EXTENSION_CALENDAR)
+
+calendars = []
+for source in sources:
+ if source.get_enabled():
+ calendars.append({
+ 'uid': source.get_uid(),
+ 'name': source.get_display_name(),
+ 'enabled': True
+ })
+
+print(json.dumps(calendars))
diff --git a/Commons/I18n.qml b/Commons/I18n.qml
index 4ec5e86f..59eeb922 100644
--- a/Commons/I18n.qml
+++ b/Commons/I18n.qml
@@ -8,7 +8,6 @@ import qs.Commons
Singleton {
id: root
-
property bool isLoaded: false
property string langCode: ""
property string systemDetectedLangCode: ""
diff --git a/Commons/Settings.qml b/Commons/Settings.qml
index 2a392176..9e7a92d8 100644
--- a/Commons/Settings.qml
+++ b/Commons/Settings.qml
@@ -312,8 +312,7 @@ Singleton {
property bool doNotDisturb: false
property list monitors: []
property string location: "top_right"
- property bool alwaysOnTop: false
- property real lastSeenTs: 0
+ property bool overlayLayer: true
property bool respectExpireTimeout: false
property int lowUrgencyDuration: 3
property int normalUrgencyDuration: 8
@@ -326,7 +325,7 @@ Singleton {
property string location: "top_right"
property list monitors: []
property int autoHideMs: 2000
- property bool alwaysOnTop: false
+ property bool overlayLayer: true
}
// audio
@@ -346,6 +345,7 @@ Singleton {
property real fontDefaultScale: 1.0
property real fontFixedScale: 1.0
property bool tooltipsEnabled: true
+ property bool panelsOverlayLayer: true
}
// brightness
diff --git a/Commons/TablerIcons.qml b/Commons/TablerIcons.qml
index a840d72f..59b226d3 100644
--- a/Commons/TablerIcons.qml
+++ b/Commons/TablerIcons.qml
@@ -119,6 +119,7 @@ Singleton {
"settings-notifications": "bell",
"settings-osd": "picture-in-picture",
"settings-about": "info-square-rounded",
+ "settings-lock-screen": "lock",
"bluetooth": "bluetooth",
"bt-device-generic": "bluetooth",
"bt-device-headphones": "headphones",
diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml
index b882324d..888552bb 100644
--- a/Modules/Bar/Calendar/CalendarPanel.qml
+++ b/Modules/Bar/Calendar/CalendarPanel.qml
@@ -10,6 +10,7 @@ import qs.Widgets
NPanel {
id: root
+ property ShellScreen screen
readonly property var now: Time.date
preferredWidth: (Settings.data.location.showWeekNumberInCalendar ? 400 : 380) * Style.uiScaleRatio
@@ -347,6 +348,14 @@ NPanel {
grid.year = newDate.getFullYear()
grid.month = newDate.getMonth()
content.isCurrentMonth = content.checkIsCurrentMonth()
+ const now = new Date()
+ const monthStart = new Date(grid.year, grid.month, 1)
+ const monthEnd = new Date(grid.year, grid.month + 1, 0)
+
+ const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000)))
+ const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000)))
+
+ CalendarService.loadEvents(daysAhead + 30, daysBehind + 30)
}
}
NIconButton {
@@ -355,6 +364,7 @@ NPanel {
grid.month = Time.date.getMonth()
grid.year = Time.date.getFullYear()
content.isCurrentMonth = true
+ CalendarService.loadEvents()
}
}
NIconButton {
@@ -364,6 +374,14 @@ NPanel {
grid.year = newDate.getFullYear()
grid.month = newDate.getMonth()
content.isCurrentMonth = content.checkIsCurrentMonth()
+ const now = new Date()
+ const monthStart = new Date(grid.year, grid.month, 1)
+ const monthEnd = new Date(grid.year, grid.month + 1, 0)
+
+ const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000)))
+ const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000)))
+
+ CalendarService.loadEvents(daysAhead + 30, daysBehind + 30)
}
}
}
@@ -405,6 +423,71 @@ NPanel {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
+
+ // Helper function to check if a date has events
+ function hasEventsOnDate(year, month, day) {
+ if (!CalendarService.available || CalendarService.events.length === 0)
+ return false
+
+ const targetDate = new Date(year, month, day)
+ const targetStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000
+ const targetEnd = targetStart + 86400 // +24 hours
+
+ return CalendarService.events.some(event => {
+ // Check if event starts or overlaps with this day
+ return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd)
+ })
+ }
+
+ // Helper function to get events for a specific date
+ function getEventsForDate(year, month, day) {
+ if (!CalendarService.available || CalendarService.events.length === 0)
+ return []
+
+ const targetDate = new Date(year, month, day)
+ const targetStart = Math.floor(new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000)
+ const targetEnd = targetStart + 86400 // +24 hours
+
+ return CalendarService.events.filter(event => {
+ return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd)
+ })
+ }
+
+ // Helper function to check if an event is all-day
+ function isAllDayEvent(event) {
+ const duration = event.end - event.start
+ const startDate = new Date(event.start * 1000)
+ const isAtMidnight = startDate.getHours() === 0 && startDate.getMinutes() === 0
+ return duration === 86400 && isAtMidnight
+ }
+
+ // Helper function to check if an event is multi-day
+ function isMultiDayEvent(event) {
+ if (isAllDayEvent(event)) {
+ return false
+ }
+
+ const startDate = new Date(event.start * 1000)
+ const endDate = new Date(event.end * 1000)
+
+ const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
+ const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate())
+
+ return startDateOnly.getTime() !== endDateOnly.getTime()
+ }
+
+ // Helper function to get color for a specific event
+ function getEventColor(event, isToday) {
+ if (isMultiDayEvent(event)) {
+ return isToday ? Color.mOnSecondary : Color.mTertiary
+ } else if (isAllDayEvent(event)) {
+ return isToday ? Color.mOnSecondary : Color.mSecondary
+ } else {
+ return isToday ? Color.mOnSecondary : Color.mPrimary
+ }
+ }
+
+ // Column of week numbers
ColumnLayout {
visible: Settings.data.location.showWeekNumberInCalendar
Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0
@@ -475,6 +558,55 @@ NPanel {
pointSize: Style.fontSizeM
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightMedium
}
+
+ // Event indicator dots
+ Row {
+ visible: parent.parent.parent.parent.parent.hasEventsOnDate(model.year, model.month, model.day)
+ spacing: 2
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: Style.marginXS
+
+ readonly property int currentYear: model.year
+ readonly property int currentMonth: model.month
+ readonly property int currentDay: model.day
+ readonly property bool isToday: model.today
+
+ Repeater {
+ model: parent.parent.parent.parent.parent.parent.getEventsForDate(parent.currentYear, parent.currentMonth, parent.currentDay)
+
+ Rectangle {
+ width: 4
+ height: width
+ radius: width / 2
+ color: parent.parent.parent.parent.parent.parent.getEventColor(modelData, model.today)
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+
+ onEntered: {
+ const events = parent.parent.parent.parent.parent.getEventsForDate(model.year, model.month, model.day)
+ if (events.length > 0) {
+ const summaries = events.map(e => e.summary).join('\n')
+ TooltipService.show(Screen, parent, summaries)
+ TooltipService.updateText(summaries)
+ }
+ }
+
+ onClicked: {
+ const dateWithSlashes = `${model.month.toString().padStart(2, '0')}/${model.day.toString().padStart(2, '0')}/${model.year.toString().substring(2)}`
+ Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes])
+ }
+
+ onExited: {
+ TooltipService.hide()
+ }
+ }
+
Behavior on color {
ColorAnimation {
duration: Style.animationFast
diff --git a/Modules/Bar/Extras/BarPill.qml b/Modules/Bar/Extras/BarPill.qml
index ba1601d3..aef33339 100644
--- a/Modules/Bar/Extras/BarPill.qml
+++ b/Modules/Bar/Extras/BarPill.qml
@@ -16,8 +16,7 @@ Item {
property bool autoHide: false
property bool forceOpen: false
property bool forceClose: false
- property bool disableOpen: false
- property bool rightOpen: false
+ property bool oppositeDirection: false
property bool hovered: false
readonly property string barPosition: Settings.data.bar.position
@@ -51,8 +50,7 @@ Item {
autoHide: root.autoHide
forceOpen: root.forceOpen
forceClose: root.forceClose
- disableOpen: root.disableOpen
- rightOpen: root.rightOpen
+ oppositeDirection: root.oppositeDirection
hovered: root.hovered
density: root.density
onShown: root.shown()
@@ -76,8 +74,7 @@ Item {
autoHide: root.autoHide
forceOpen: root.forceOpen
forceClose: root.forceClose
- disableOpen: root.disableOpen
- rightOpen: root.rightOpen
+ oppositeDirection: root.oppositeDirection
hovered: root.hovered
density: root.density
onShown: root.shown()
diff --git a/Modules/Bar/Extras/BarPillHorizontal.qml b/Modules/Bar/Extras/BarPillHorizontal.qml
index 5a732263..e5e5ccda 100644
--- a/Modules/Bar/Extras/BarPillHorizontal.qml
+++ b/Modules/Bar/Extras/BarPillHorizontal.qml
@@ -18,8 +18,7 @@ Item {
property bool autoHide: false
property bool forceOpen: false
property bool forceClose: false
- property bool disableOpen: false
- property bool rightOpen: false
+ property bool oppositeDirection: false
property bool hovered: false
// Effective shown state (true if hovered/animated open or forced)
@@ -79,18 +78,18 @@ Item {
width: revealed ? pillMaxWidth : 1
height: pillHeight
- x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right
- (iconCircle.x + iconCircle.width / 2) - width // Opens left
+ x: oppositeDirection ? (iconCircle.x + iconCircle.width / 2) : // Opens right
+ (iconCircle.x + iconCircle.width / 2) - width // Opens left
opacity: revealed ? Style.opacityFull : Style.opacityNone
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
readonly property int halfPillHeight: Math.round(pillHeight * 0.5)
- topLeftRadius: rightOpen ? 0 : halfPillHeight
- bottomLeftRadius: rightOpen ? 0 : halfPillHeight
- topRightRadius: rightOpen ? halfPillHeight : 0
- bottomRightRadius: rightOpen ? halfPillHeight : 0
+ topLeftRadius: oppositeDirection ? 0 : halfPillHeight
+ bottomLeftRadius: oppositeDirection ? 0 : halfPillHeight
+ topRightRadius: oppositeDirection ? halfPillHeight : 0
+ bottomRightRadius: oppositeDirection ? halfPillHeight : 0
anchors.verticalCenter: parent.verticalCenter
NText {
@@ -99,10 +98,10 @@ Item {
x: {
// Better text horizontal centering
var centerX = (parent.width - width) / 2
- var offset = rightOpen ? Style.marginXS : -Style.marginXS
+ var offset = oppositeDirection ? Style.marginXS : -Style.marginXS
if (forceOpen) {
// If its force open, the icon disc background is the same color as the bg pill move text slightly
- offset += rightOpen ? -Style.marginXXS : Style.marginXXS
+ offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS
}
return centerX + offset
}
@@ -139,7 +138,7 @@ Item {
color: hovered ? Color.mTertiary : Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
anchors.verticalCenter: parent.verticalCenter
- x: rightOpen ? 0 : (parent.width - width)
+ x: oppositeDirection ? 0 : (parent.width - width)
Behavior on color {
ColorAnimation {
@@ -245,7 +244,7 @@ Item {
hovered = true
root.entered()
TooltipService.show(Screen, pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong)
- if (disableOpen || forceClose) {
+ if (forceClose) {
return
}
if (!forceOpen) {
diff --git a/Modules/Bar/Extras/BarPillVertical.qml b/Modules/Bar/Extras/BarPillVertical.qml
index 485d9732..fe94a534 100644
--- a/Modules/Bar/Extras/BarPillVertical.qml
+++ b/Modules/Bar/Extras/BarPillVertical.qml
@@ -16,8 +16,7 @@ Item {
property bool autoHide: false
property bool forceOpen: false
property bool forceClose: false
- property bool disableOpen: false
- property bool rightOpen: false
+ property bool oppositeDirection: false
property bool hovered: false
// Bar position detection for pill direction
@@ -25,8 +24,8 @@ Item {
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
// Determine pill direction based on section position
- readonly property bool openDownward: rightOpen
- readonly property bool openUpward: !rightOpen
+ readonly property bool openDownward: oppositeDirection
+ readonly property bool openUpward: !oppositeDirection
// Effective shown state (true if animated open or forced, but not if force closed)
readonly property bool revealed: !forceClose && (forceOpen || showPill)
@@ -114,7 +113,7 @@ Item {
var offset = openDownward ? Math.round(pillPaddingVertical * 0.75) : -Math.round(pillPaddingVertical * 0.75)
if (forceOpen) {
// If its force open, the icon disc background is the same color as the bg pill move text slightly
- offset += rightOpen ? -Style.marginXXS : Style.marginXXS
+ offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS
}
return offset
}
@@ -284,7 +283,7 @@ Item {
hovered = true
root.entered()
TooltipService.show(Screen, pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong)
- if (disableOpen || forceClose) {
+ if (forceClose) {
return
}
if (!forceOpen) {
diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml
index 2058bf1a..ae639ffb 100644
--- a/Modules/Bar/Widgets/ActiveWindow.qml
+++ b/Modules/Bar/Widgets/ActiveWindow.qml
@@ -45,12 +45,12 @@ Item {
readonly property string windowTitle: CompositorService.getFocusedWindowTitle() || "No active window"
readonly property string fallbackIcon: "user-desktop"
- implicitHeight: visible ? (isVerticalBar ? calculatedVerticalDimension() : Style.barHeight) : 0
- implicitWidth: visible ? (isVerticalBar ? calculatedVerticalDimension() : dynamicWidth) : 0
+ implicitHeight: visible ? (isVerticalBar ? (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : calculatedVerticalDimension()) : Style.capsuleHeight) : 0
+ implicitWidth: visible ? (isVerticalBar ? (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : calculatedVerticalDimension()) : (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : dynamicWidth)) : 0
// "visible": Always Visible, "hidden": Hide When Empty, "transparent": Transparent When Empty
- visible: hideMode !== "hidden" || hasFocusedWindow
- opacity: hideMode !== "transparent" || hasFocusedWindow ? 1.0 : 0
+ visible: (hideMode !== "hidden" || hasFocusedWindow) || opacity > 0
+ opacity: ((hideMode !== "hidden" || hasFocusedWindow) && (hideMode !== "transparent" || hasFocusedWindow)) ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
@@ -58,6 +58,20 @@ Item {
}
}
+ Behavior on implicitWidth {
+ NumberAnimation {
+ duration: Style.animationNormal
+ easing.type: Easing.InOutCubic
+ }
+ }
+
+ Behavior on implicitHeight {
+ NumberAnimation {
+ duration: Style.animationNormal
+ easing.type: Easing.InOutCubic
+ }
+ }
+
function calculatedVerticalDimension() {
return Math.round((Style.baseWidgetSize - 5) * scaling)
}
@@ -93,7 +107,7 @@ Item {
}
// Otherwise, adapt to content
if (!hasFocusedWindow) {
- return maxWidth
+ return Math.min(calculateContentWidth(), maxWidth)
}
// Use content width but don't exceed user-set maximum width
return Math.min(calculateContentWidth(), maxWidth)
@@ -155,10 +169,9 @@ Item {
Rectangle {
id: windowActiveRect
visible: root.visible
- anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
- width: isVerticalBar ? root.width : dynamicWidth
- height: isVerticalBar ? width : Style.capsuleHeight
+ width: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : calculatedVerticalDimension()) : ((!hasFocusedWindow) && (hideMode === "hidden") ? 0 : dynamicWidth)
+ height: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : calculatedVerticalDimension()) : Style.capsuleHeight
radius: isVerticalBar ? width / 2 : Style.radiusM
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
@@ -307,6 +320,14 @@ Item {
font.weight: Style.fontWeightMedium
verticalAlignment: Text.AlignVCenter
color: Color.mOnSurface
+ onTextChanged: {
+ if (root.scrollingMode === "always") {
+ titleContainer.isScrolling = false
+ titleContainer.isResetting = false
+ scrollContainer.scrollX = 0
+ scrollStartTimer.restart()
+ }
+ }
}
// Second copy for seamless scrolling
@@ -343,13 +364,6 @@ Item {
easing.type: Easing.Linear
}
}
-
- Behavior on Layout.preferredWidth {
- NumberAnimation {
- duration: Style.animationSlow
- easing.type: Easing.InOutCubic
- }
- }
}
}
diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml
index d29c817e..e19e564f 100644
--- a/Modules/Bar/Widgets/Battery.qml
+++ b/Modules/Bar/Widgets/Battery.qml
@@ -87,14 +87,13 @@ Item {
id: pill
density: Settings.data.bar.density
- rightOpen: BarService.getPillDirection(root)
+ oppositeDirection: BarService.getPillDirection(root)
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady)
text: (isReady || testMode) ? Math.round(percent) : "-"
suffix: "%"
autoHide: false
forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow"
- forceClose: displayMode === "alwaysHide"
- disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery))
+ forceClose: displayMode === "alwaysHide" || !isReady || (!testMode && !battery.isLaptopBattery)
onClicked: PanelService.getPanel("batteryPanel")?.toggle(this)
tooltipText: {
let lines = []
diff --git a/Modules/Bar/Widgets/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml
index e6f50f0f..1c3fd2b0 100644
--- a/Modules/Bar/Widgets/Bluetooth.qml
+++ b/Modules/Bar/Widgets/Bluetooth.qml
@@ -1,27 +1,66 @@
import QtQuick
-import QtQuick.Layouts
-import QtQuick.Controls
import Quickshell
-import Quickshell.Wayland
import qs.Commons
import qs.Services
-import qs.Widgets
+import qs.Modules.Bar.Extras
-NIconButton {
+Item {
id: root
property ShellScreen screen
- baseSize: Style.capsuleHeight
- applyUiScale: false
- density: Settings.data.bar.density
- colorBg: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
- colorFg: Color.mOnSurface
- colorBorder: Color.transparent
- colorBorderHover: Color.transparent
- tooltipText: I18n.tr("tooltips.bluetooth-devices")
- tooltipDirection: BarService.getTooltipDirection()
- icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
- onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
- onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
+ // 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 isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
+ readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
+
+ implicitWidth: pill.width
+ implicitHeight: pill.height
+
+ BarPill {
+ id: pill
+
+ density: Settings.data.bar.density
+ oppositeDirection: BarService.getPillDirection(root)
+ icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
+ text: {
+ if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 0) {
+ const firstDevice = BluetoothService.connectedDevices[0]
+ return firstDevice.name || firstDevice.deviceName
+ }
+ return ""
+ }
+ suffix: {
+ if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 1) {
+ return ` + ${BluetoothService.connectedDevices.length - 1}`
+ }
+ return ""
+ }
+ autoHide: false
+ forceOpen: !isBarVertical && root.displayMode === "alwaysShow"
+ forceClose: isBarVertical || root.displayMode === "alwaysHide" || BluetoothService.connectedDevices.length === 0
+ onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
+ onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
+ tooltipText: {
+ if (pill.text !== "") {
+ return pill.text
+ }
+ return I18n.tr("tooltips.bluetooth-devices")
+ }
+ }
}
diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml
index 5c02e490..60efc609 100644
--- a/Modules/Bar/Widgets/Brightness.qml
+++ b/Modules/Bar/Widgets/Brightness.qml
@@ -77,7 +77,7 @@ Item {
id: pill
density: Settings.data.bar.density
- rightOpen: BarService.getPillDirection(root)
+ oppositeDirection: BarService.getPillDirection(root)
icon: getIcon()
autoHide: false // Important to be false so we can hover as long as we want
text: {
diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml
index 4015c4f4..652d391a 100644
--- a/Modules/Bar/Widgets/CustomButton.qml
+++ b/Modules/Bar/Widgets/CustomButton.qml
@@ -45,14 +45,13 @@ Item {
BarPill {
id: pill
- rightOpen: BarService.getPillDirection(root)
+ oppositeDirection: BarService.getPillDirection(root)
icon: customIcon
text: _dynamicText
density: Settings.data.bar.density
autoHide: false
forceOpen: _dynamicText !== ""
- forceClose: false
- disableOpen: true
+ forceClose: true
tooltipText: {
if (!hasExec) {
return "Custom button, configure in settings."
diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml
index 6d597227..41ffd027 100644
--- a/Modules/Bar/Widgets/KeyboardLayout.qml
+++ b/Modules/Bar/Widgets/KeyboardLayout.qml
@@ -43,7 +43,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
density: Settings.data.bar.density
- rightOpen: BarService.getPillDirection(root)
+ oppositeDirection: BarService.getPillDirection(root)
icon: "keyboard"
autoHide: false // Important to be false so we can hover as long as we want
text: currentLayout.toUpperCase()
diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml
index 86338210..6fe3d709 100644
--- a/Modules/Bar/Widgets/Microphone.qml
+++ b/Modules/Bar/Widgets/Microphone.qml
@@ -89,7 +89,7 @@ Item {
BarPill {
id: pill
- rightOpen: BarService.getPillDirection(root)
+ oppositeDirection: BarService.getPillDirection(root)
icon: getIcon()
density: Settings.data.bar.density
autoHide: false // Important to be false so we can hover as long as we want
diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml
index 6d6652fe..ccceeaa3 100644
--- a/Modules/Bar/Widgets/NotificationHistory.qml
+++ b/Modules/Bar/Widgets/NotificationHistory.qml
@@ -31,12 +31,8 @@ NIconButton {
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 since = NotificationService.lastSeenTs
var count = 0
var model = NotificationService.historyList
for (var i = 0; i < model.count; i++) {
diff --git a/Modules/Bar/Widgets/TaskbarGrouped.qml b/Modules/Bar/Widgets/TaskbarGrouped.qml
new file mode 100644
index 00000000..616032c8
--- /dev/null
+++ b/Modules/Bar/Widgets/TaskbarGrouped.qml
@@ -0,0 +1,223 @@
+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
+
+Item {
+ id: root
+
+ property ShellScreen screen
+
+ // Widget properties passed from Bar.qml for per-instance settings
+ property string widgetId: ""
+ property string section: ""
+ property int sectionWidgetIndex: -1
+ property int sectionWidgetsCount: 0
+
+ readonly property bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
+ readonly property string density: Settings.data.bar.density
+ readonly property real itemSize: (density === "compact") ? Style.capsuleHeight * 0.9 : Style.capsuleHeight * 0.8
+ 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 hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : false
+ property ListModel localWorkspaces: ListModel {}
+
+ function refreshWorkspaces() {
+ localWorkspaces.clear()
+ if (!screen)
+ return
+
+ const screenName = screen.name.toLowerCase()
+
+ for (var i = 0; i < CompositorService.workspaces.count; i++) {
+ const ws = CompositorService.workspaces.get(i)
+
+ if (ws.output.toLowerCase() !== screenName)
+ continue
+ if (hideUnoccupied && !ws.isOccupied && !ws.isFocused)
+ continue
+
+ // Copy all properties from ws and add windows
+ var workspaceData = Object.assign({}, ws)
+ workspaceData.windows = CompositorService.getWindowsForWorkspace(ws.id)
+
+ localWorkspaces.append(workspaceData)
+ }
+ }
+
+ Component.onCompleted: {
+ refreshWorkspaces()
+ }
+
+ onScreenChanged: refreshWorkspaces()
+
+ implicitWidth: isVerticalBar ? taskbarGrid.implicitWidth + Style.marginM * 2 : Math.round(taskbarGrid.implicitWidth + Style.marginM * 2)
+ implicitHeight: isVerticalBar ? Math.round(taskbarGrid.implicitHeight + Style.marginM * 2) : Style.barHeight
+
+ Connections {
+ target: CompositorService
+
+ function onWorkspacesChanged() {
+ refreshWorkspaces()
+ }
+
+ function onWindowListChanged() {
+ refreshWorkspaces()
+ }
+ }
+
+ Component {
+ id: workspaceRepeaterDelegate
+
+ Rectangle {
+ id: container
+
+ required property var model
+ property var workspaceModel: model
+ property bool hasWindows: workspaceModel.windows.count > 0
+
+ radius: Style.radiusS
+ border.color: workspaceModel.isFocused ? Color.mPrimary : Color.mOutline
+ border.width: 1
+ width: (hasWindows ? iconsFlow.implicitWidth : root.itemSize * 0.8) + (root.isVerticalBar ? Style.marginXS : Style.marginL)
+ height: (hasWindows ? iconsFlow.implicitHeight : root.itemSize * 0.8) + (root.isVerticalBar ? Style.marginL : Style.marginXS)
+ color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ enabled: !hasWindows
+ cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
+ onClicked: {
+ CompositorService.switchToWorkspace(workspaceModel)
+ }
+ }
+
+ Flow {
+ id: iconsFlow
+
+ anchors.centerIn: parent
+ spacing: 4
+ flow: root.isVerticalBar ? Flow.TopToBottom : Flow.LeftToRight
+
+ Repeater {
+ model: workspaceModel.windows
+
+ delegate: Item {
+ id: taskbarItem
+
+ property bool itemHovered: false
+
+ width: root.itemSize * 0.8
+ height: root.itemSize * 0.8
+
+ // Smooth scale animation on hover
+ scale: itemHovered ? 1.1 : 1.0
+
+ Behavior on scale {
+ NumberAnimation {
+ duration: Style.animationNormal
+ easing.type: Easing.OutBack
+ }
+ }
+
+ IconImage {
+ id: appIcon
+
+ width: parent.width
+ height: parent.height
+ source: ThemeIcons.iconForAppId(model.appId)
+ smooth: true
+ asynchronous: true
+ opacity: model.isFocused ? Style.opacityFull : 0.6
+ layer.enabled: widgetSettings.colorizeIcons === true
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Style.animationNormal
+ easing.type: Easing.InOutCubic
+ }
+ }
+
+ Rectangle {
+ id: focusIndicator
+ anchors.bottomMargin: -2
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: model.isFocused ? 4 : 0
+ height: model.isFocused ? 4 : 0
+ color: model.isFocused ? Color.mPrimary : Color.transparent
+ radius: width * 0.5
+ }
+
+ layer.effect: ShaderEffect {
+ property color targetColor: Color.mOnSurface
+ property real colorizeMode: 0
+ fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+
+ onPressed: function (mouse) {
+ if (!model) {
+ return
+ }
+
+ if (mouse.button === Qt.LeftButton) {
+ CompositorService.focusWindow(model)
+ } else if (mouse.button === Qt.RightButton) {
+ CompositorService.closeWindow(model)
+ }
+ }
+ onEntered: {
+ taskbarItem.itemHovered = true
+ TooltipService.show(Screen, taskbarItem, model.title || model.appId || "Unknown app.", BarService.getTooltipDirection())
+ }
+ onExited: {
+ taskbarItem.itemHovered = false
+ TooltipService.hide()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Flow {
+ id: taskbarGrid
+
+ anchors.verticalCenter: isVerticalBar ? undefined : parent.verticalCenter
+ anchors.left: isVerticalBar ? undefined : parent.left
+ anchors.leftMargin: isVerticalBar ? 0 : Style.marginM
+ anchors.horizontalCenter: isVerticalBar ? parent.horizontalCenter : undefined
+ anchors.top: isVerticalBar ? parent.top : undefined
+ anchors.topMargin: isVerticalBar ? Style.marginM : 0
+
+ spacing: Style.marginS
+ flow: isVerticalBar ? Flow.TopToBottom : Flow.LeftToRight
+
+ Repeater {
+ model: localWorkspaces
+ delegate: workspaceRepeaterDelegate
+ }
+ }
+}
diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml
index 10018771..edf75b9d 100644
--- a/Modules/Bar/Widgets/Volume.qml
+++ b/Modules/Bar/Widgets/Volume.qml
@@ -75,7 +75,7 @@ Item {
id: pill
density: Settings.data.bar.density
- rightOpen: BarService.getPillDirection(root)
+ oppositeDirection: BarService.getPillDirection(root)
icon: getIcon()
autoHide: false // Important to be false so we can hover as long as we want
text: Math.round(AudioService.volume * 100)
diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml
index d2b996b5..25de98ae 100644
--- a/Modules/Bar/Widgets/WiFi.qml
+++ b/Modules/Bar/Widgets/WiFi.qml
@@ -1,46 +1,88 @@
import QtQuick
-import QtQuick.Layouts
-import QtQuick.Controls
import Quickshell
-import Quickshell.Wayland
import qs.Commons
import qs.Services
-import qs.Widgets
+import qs.Modules.Bar.Extras
-NIconButton {
+Item {
id: root
property ShellScreen screen
- density: Settings.data.bar.density
- baseSize: Style.capsuleHeight
- applyUiScale: false
- colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
- colorFg: Color.mOnSurface
- colorBorder: Color.transparent
- colorBorderHover: Color.transparent
- tooltipText: I18n.tr("tooltips.manage-wifi")
- tooltipDirection: BarService.getTooltipDirection()
- icon: {
- try {
- if (NetworkService.ethernetConnected) {
- return "ethernet"
+ // 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]
}
- let connected = false
- let signalStrength = 0
- for (const net in NetworkService.networks) {
- if (NetworkService.networks[net].connected) {
- connected = true
- signalStrength = NetworkService.networks[net].signal
- break
+ }
+ return {}
+ }
+
+ readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
+ readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
+
+ implicitWidth: pill.width
+ implicitHeight: pill.height
+
+ BarPill {
+ id: pill
+
+ density: Settings.data.bar.density
+ oppositeDirection: BarService.getPillDirection(root)
+ icon: {
+ try {
+ if (NetworkService.ethernetConnected) {
+ return "ethernet"
}
+ let connected = false
+ let signalStrength = 0
+ for (const net in NetworkService.networks) {
+ if (NetworkService.networks[net].connected) {
+ connected = true
+ signalStrength = NetworkService.networks[net].signal
+ break
+ }
+ }
+ return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off"
+ } catch (error) {
+ Logger.e("Wi-Fi", "Error getting icon:", error)
+ return "signal_wifi_bad"
}
- return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off"
- } catch (error) {
- Logger.e("Wi-Fi", "Error getting icon:", error)
- return "signal_wifi_bad"
+ }
+ text: {
+ try {
+ if (NetworkService.ethernetConnected) {
+ return ""
+ }
+ for (const net in NetworkService.networks) {
+ if (NetworkService.networks[net].connected) {
+ return net
+ }
+ }
+ return ""
+ } catch (error) {
+ Logger.e("Wi-Fi", "Error getting ssid:", error)
+ return "error"
+ }
+ }
+ autoHide: false
+ forceOpen: !isBarVertical && root.displayMode === "alwaysShow"
+ forceClose: isBarVertical || root.displayMode === "alwaysHide" || !pill.text
+ onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
+ onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
+ tooltipText: {
+ if (pill.text !== "") {
+ return pill.text
+ }
+ return I18n.tr("tooltips.manage-wifi")
}
}
- onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
- onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
}
diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml
index 09a91ff8..2b8add42 100644
--- a/Modules/LockScreen/LockScreen.qml
+++ b/Modules/LockScreen/LockScreen.qml
@@ -17,11 +17,18 @@ Loader {
id: lockScreen
active: false
+ // Track if triggered via deprecated IPC call
+ property bool triggeredViaDeprecatedCall: false
+
Timer {
id: unloadAfterUnlockTimer
interval: 250
repeat: false
- onTriggered: lockScreen.active = false
+ onTriggered: {
+ lockScreen.active = false
+ // Reset the deprecation flag when unlocking
+ lockScreen.triggeredViaDeprecatedCall = false
+ }
}
function scheduleUnloadAfterUnlock() {
@@ -424,6 +431,64 @@ Loader {
}
}
+ // Deprecation warning (shown above error notification)
+ Rectangle {
+ width: Math.min(650, parent.width - 40)
+ implicitHeight: deprecationContent.implicitHeight + 24
+ height: implicitHeight
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: (Settings.data.general.compactLockScreen ? 320 : 400) * Style.uiScaleRatio
+ radius: Style.radiusL
+ color: Qt.alpha(Color.mTertiary, 0.95)
+ border.color: Color.mTertiary
+ border.width: 2
+ visible: lockScreen.triggeredViaDeprecatedCall
+ opacity: visible ? 1.0 : 0.0
+
+ ColumnLayout {
+ id: deprecationContent
+ anchors.fill: parent
+ anchors.margins: 12
+ spacing: 6
+
+ RowLayout {
+ Layout.alignment: Qt.AlignHCenter
+ spacing: 8
+
+ NIcon {
+ icon: "alert-triangle"
+ pointSize: Style.fontSizeL
+ color: Color.mOnTertiary
+ }
+
+ NText {
+ text: "Deprecated IPC Call"
+ color: Color.mOnTertiary
+ pointSize: Style.fontSizeL
+ font.weight: Font.Bold
+ }
+ }
+
+ NText {
+ text: "The 'lockScreen toggle' IPC call is deprecated. Use 'lockScreen lock' instead."
+ color: Color.mOnTertiary
+ pointSize: Style.fontSizeM
+ horizontalAlignment: Text.AlignHCenter
+ Layout.alignment: Qt.AlignHCenter
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 300
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+
// Error notification
Rectangle {
width: 450
diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml
index 00b93297..e6ea0702 100644
--- a/Modules/Notification/Notification.qml
+++ b/Modules/Notification/Notification.qml
@@ -45,7 +45,7 @@ Variants {
screen: modelData
WlrLayershell.namespace: "noctalia-notifications"
- WlrLayershell.layer: (Settings.data.notifications && Settings.data.notifications.alwaysOnTop) ? WlrLayer.Overlay : WlrLayer.Top
+ WlrLayershell.layer: (Settings.data.notifications && Settings.data.notifications.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top
color: Color.transparent
@@ -161,6 +161,7 @@ Variants {
// Animate when notifications are added/removed
Behavior on implicitHeight {
+ enabled: !Settings.data.general.animationDisabled
SpringAnimation {
spring: 2.0
damping: 0.4
@@ -196,6 +197,7 @@ Variants {
anchors.right: parent.right
height: 2
color: Color.transparent
+ visible: !Settings.data.general.animationDisabled
// Pre-calculate available width for the progress bar
readonly property real availableWidth: parent.width - (2 * parent.radius)
@@ -222,7 +224,7 @@ Variants {
// Smooth progress animation
Behavior on width {
- enabled: !card.isRemoving // Disable during removal animation
+ enabled: !card.isRemoving && !Settings.data.general.animationDisabled // Disable during removal animation or when animations disabled
NumberAnimation {
duration: 100 // Quick but smooth
easing.type: Easing.Linear
@@ -230,7 +232,7 @@ Variants {
}
Behavior on x {
- enabled: !card.isRemoving
+ enabled: !card.isRemoving && !Settings.data.general.animationDisabled
NumberAnimation {
duration: 100
easing.type: Easing.Linear
@@ -312,14 +314,21 @@ Variants {
// Animate in when the item is created
Component.onCompleted: {
- // Start from slide position
- slideOffset = slideInOffset
- scaleValue = 0.8
- opacityValue = 0.0
+ if (Settings.data.general.animationDisabled) {
+ // No animation - set to final state immediately
+ slideOffset = 0
+ scaleValue = 1.0
+ opacityValue = 1.0
+ } else {
+ // Start from slide position
+ slideOffset = slideInOffset
+ scaleValue = 0.8
+ opacityValue = 0.0
- // Delay animation based on index for staggered effect
- delayTimer.interval = animationDelay
- delayTimer.start()
+ // Delay animation based on index for staggered effect
+ delayTimer.interval = animationDelay
+ delayTimer.start()
+ }
}
// Timer for staggered animation start
@@ -341,9 +350,11 @@ Variants {
return
// Prevent multiple animations
isRemoving = true
- slideOffset = slideOutOffset
- scaleValue = 0.8
- opacityValue = 0.0
+ if (!Settings.data.general.animationDisabled) {
+ slideOffset = slideOutOffset
+ scaleValue = 0.8
+ opacityValue = 0.0
+ }
}
// Timer for delayed removal after animation
@@ -365,6 +376,7 @@ Variants {
// Animation behaviors with spring physics
Behavior on scale {
+ enabled: !Settings.data.general.animationDisabled
SpringAnimation {
spring: 3
damping: 0.4
@@ -374,6 +386,7 @@ Variants {
}
Behavior on opacity {
+ enabled: !Settings.data.general.animationDisabled
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
@@ -381,6 +394,7 @@ Variants {
}
Behavior on y {
+ enabled: !Settings.data.general.animationDisabled
SpringAnimation {
spring: 2.5
damping: 0.3
diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml
index 8bf5583f..b4121199 100644
--- a/Modules/Notification/NotificationHistoryPanel.qml
+++ b/Modules/Notification/NotificationHistoryPanel.qml
@@ -17,7 +17,7 @@ NPanel {
panelKeyboardFocus: true
onOpened: function () {
- Settings.data.notifications.lastSeenTs = Time.timestamp * 1000
+ NotificationService.updateLastSeenTs()
}
panelContent: Rectangle {
@@ -148,6 +148,7 @@ NPanel {
border.width: Math.max(1, Style.borderS)
Behavior on height {
+ enabled: !Settings.data.general.animationDisabled
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
@@ -156,6 +157,7 @@ NPanel {
// Smooth color transition on hover
Behavior on color {
+ enabled: !Settings.data.general.animationDisabled
ColorAnimation {
duration: Style.animationFast
}
diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml
index 5c98c45d..c460b672 100644
--- a/Modules/OSD/OSD.qml
+++ b/Modules/OSD/OSD.qml
@@ -190,7 +190,7 @@ Variants {
color: Color.transparent
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
- WlrLayershell.layer: (Settings.data.osd && Settings.data.osd.alwaysOnTop) ? WlrLayer.Overlay : WlrLayer.Top
+ WlrLayershell.layer: (Settings.data.osd && Settings.data.osd.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top
exclusionMode: PanelWindow.ExclusionMode.Ignore
Rectangle {
diff --git a/Modules/Settings/Bar/BarWidgetSettingsDialog.qml b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml
index 4b35b7fd..ed7562e4 100644
--- a/Modules/Settings/Bar/BarWidgetSettingsDialog.qml
+++ b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml
@@ -122,6 +122,7 @@ Popup {
const widgetSettingsMap = {
"ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml",
"Battery": "WidgetSettings/BatterySettings.qml",
+ "Bluetooth": "WidgetSettings/BluetoothSettings.qml",
"Brightness": "WidgetSettings/BrightnessSettings.qml",
"Clock": "WidgetSettings/ClockSettings.qml",
"ControlCenter": "WidgetSettings/ControlCenterSettings.qml",
@@ -133,6 +134,7 @@ Popup {
"Spacer": "WidgetSettings/SpacerSettings.qml",
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
"Volume": "WidgetSettings/VolumeSettings.qml",
+ "WiFi": "WidgetSettings/WiFiSettings.qml",
"Workspace": "WidgetSettings/WorkspaceSettings.qml",
"Taskbar": "WidgetSettings/TaskbarSettings.qml",
"Tray": "WidgetSettings/TraySettings.qml"
diff --git a/Modules/Settings/Bar/WidgetSettings/BluetoothSettings.qml b/Modules/Settings/Bar/WidgetSettings/BluetoothSettings.qml
new file mode 100644
index 00000000..d6548476
--- /dev/null
+++ b/Modules/Settings/Bar/WidgetSettings/BluetoothSettings.qml
@@ -0,0 +1,40 @@
+import QtQuick
+import QtQuick.Layouts
+import qs.Commons
+import qs.Widgets
+
+ColumnLayout {
+ id: root
+ spacing: Style.marginM
+
+ // Properties to receive data from parent
+ property var widgetData: null
+ property var widgetMetadata: null
+
+ // Local state
+ property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
+
+ function saveSettings() {
+ var settings = Object.assign({}, widgetData || {})
+ settings.displayMode = valueDisplayMode
+ return settings
+ }
+
+ NComboBox {
+ label: I18n.tr("bar.widget-settings.battery.display-mode.label")
+ description: I18n.tr("bar.widget-settings.battery.display-mode.description")
+ minimumWidth: 134
+ model: [{
+ "key": "onhover",
+ "name": I18n.tr("options.display-mode.on-hover")
+ }, {
+ "key": "alwaysShow",
+ "name": I18n.tr("options.display-mode.always-show")
+ }, {
+ "key": "alwaysHide",
+ "name": I18n.tr("options.display-mode.always-hide")
+ }]
+ currentKey: root.valueDisplayMode
+ onSelected: key => root.valueDisplayMode = key
+ }
+}
diff --git a/Modules/Settings/Bar/WidgetSettings/WiFiSettings.qml b/Modules/Settings/Bar/WidgetSettings/WiFiSettings.qml
new file mode 100644
index 00000000..d6548476
--- /dev/null
+++ b/Modules/Settings/Bar/WidgetSettings/WiFiSettings.qml
@@ -0,0 +1,40 @@
+import QtQuick
+import QtQuick.Layouts
+import qs.Commons
+import qs.Widgets
+
+ColumnLayout {
+ id: root
+ spacing: Style.marginM
+
+ // Properties to receive data from parent
+ property var widgetData: null
+ property var widgetMetadata: null
+
+ // Local state
+ property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
+
+ function saveSettings() {
+ var settings = Object.assign({}, widgetData || {})
+ settings.displayMode = valueDisplayMode
+ return settings
+ }
+
+ NComboBox {
+ label: I18n.tr("bar.widget-settings.battery.display-mode.label")
+ description: I18n.tr("bar.widget-settings.battery.display-mode.description")
+ minimumWidth: 134
+ model: [{
+ "key": "onhover",
+ "name": I18n.tr("options.display-mode.on-hover")
+ }, {
+ "key": "alwaysShow",
+ "name": I18n.tr("options.display-mode.always-show")
+ }, {
+ "key": "alwaysHide",
+ "name": I18n.tr("options.display-mode.always-hide")
+ }]
+ currentKey: root.valueDisplayMode
+ onSelected: key => root.valueDisplayMode = key
+ }
+}
diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml
index e5b64566..47ebc40a 100644
--- a/Modules/Settings/SettingsPanel.qml
+++ b/Modules/Settings/SettingsPanel.qml
@@ -26,6 +26,7 @@ NPanel {
Audio,
Bar,
ColorScheme,
+ LockScreen,
ControlCenter,
OSD,
Display,
@@ -118,6 +119,11 @@ NPanel {
id: userInterfaceTab
UserInterfaceTab {}
}
+ Component {
+ id: lockScreenTab
+ LockScreenTab {}
+ }
+
// Order *DOES* matter
function updateTabsModel() {
let newTabs = [{
@@ -150,6 +156,11 @@ NPanel {
"label": "settings.launcher.title",
"icon": "settings-launcher",
"source": launcherTab
+ }, {
+ "id": SettingsPanel.Tab.LockScreen,
+ "label": "settings.lock-screen.title",
+ "icon": "settings-lock-screen",
+ "source": lockScreenTab
}, {
"id": SettingsPanel.Tab.Audio,
"label": "settings.audio.title",
diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml
index 314cc07d..bbd02a8e 100644
--- a/Modules/Settings/Tabs/ColorSchemeTab.qml
+++ b/Modules/Settings/Tabs/ColorSchemeTab.qml
@@ -159,7 +159,6 @@ ColumnLayout {
label: I18n.tr("settings.color-scheme.dark-mode.switch.label")
description: I18n.tr("settings.color-scheme.dark-mode.switch.description")
checked: Settings.data.colorSchemes.darkMode
- enabled: true
onToggled: checked => {
Settings.data.colorSchemes.darkMode = checked
root.cacheVersion++ // Force UI update for dark/light variants
@@ -241,6 +240,7 @@ ColumnLayout {
NToggle {
label: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label")
description: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.description")
+ enabled: ProgramCheckerService.matugenAvailable
checked: Settings.data.colorSchemes.useWallpaperColors
onToggled: checked => {
if (checked) {
@@ -665,24 +665,22 @@ ColumnLayout {
}
}
NCheckbox {
- label: "Vicinae"
- description: ProgramCheckerService.vicinaeAvailable
- ? I18n.tr("settings.color-scheme.templates.programs.vicinae.description", {
- "filepath": "~/.local/share/vicinae/themes/matugen.toml"
- })
- : I18n.tr("settings.color-scheme.templates.programs.vicinae.description-missing", {
- "app": "vicinae"
- })
- checked: Settings.data.templates.vicinae
- enabled: ProgramCheckerService.vicinaeAvailable
- opacity: ProgramCheckerService.vicinaeAvailable ? 1.0 : 0.6
- onToggled: checked => {
- if (ProgramCheckerService.vicinaeAvailable) {
- Settings.data.templates.vicinae = checked
- AppThemeService.generate()
- }
- }
-}
+ label: "Vicinae"
+ description: ProgramCheckerService.vicinaeAvailable ? I18n.tr("settings.color-scheme.templates.programs.vicinae.description", {
+ "filepath": "~/.local/share/vicinae/themes/matugen.toml"
+ }) : I18n.tr("settings.color-scheme.templates.programs.vicinae.description-missing", {
+ "app": "vicinae"
+ })
+ checked: Settings.data.templates.vicinae
+ enabled: ProgramCheckerService.vicinaeAvailable
+ opacity: ProgramCheckerService.vicinaeAvailable ? 1.0 : 0.6
+ onToggled: checked => {
+ if (ProgramCheckerService.vicinaeAvailable) {
+ Settings.data.templates.vicinae = checked
+ AppThemeService.generate()
+ }
+ }
+ }
}
// Miscellaneous
diff --git a/Modules/Settings/Tabs/GeneralTab.qml b/Modules/Settings/Tabs/GeneralTab.qml
index 40b5276d..d750f3c7 100644
--- a/Modules/Settings/Tabs/GeneralTab.qml
+++ b/Modules/Settings/Tabs/GeneralTab.qml
@@ -204,20 +204,24 @@ ColumnLayout {
Layout.fillWidth: true
label: I18n.tr("settings.general.language.select.label")
description: I18n.tr("settings.general.language.select.description")
- model: [
- { "key": "", "name": I18n.tr("settings.general.language.select.auto-detect") + " (" + I18n.systemDetectedLangCode + ")" }
- ].concat(I18n.availableLanguages.map(function(langCode) {
- return { "key": langCode, "name": langCode }
- }))
+ model: [{
+ "key": "",
+ "name": I18n.tr("settings.general.language.select.auto-detect") + " (" + I18n.systemDetectedLangCode + ")"
+ }].concat(I18n.availableLanguages.map(function (langCode) {
+ return {
+ "key": langCode,
+ "name": langCode
+ }
+ }))
currentKey: Settings.data.general.language
onSelected: key => {
- Settings.data.general.language = key
- if (key === "") {
- I18n.detectLanguage() // Re-detect system language if "Automatic" is selected
- } else {
- I18n.setLanguage(key) // Set specific language
- }
- }
+ Settings.data.general.language = key
+ if (key === "") {
+ I18n.detectLanguage() // Re-detect system language if "Automatic" is selected
+ } else {
+ I18n.setLanguage(key) // Set specific language
+ }
+ }
}
}
@@ -226,26 +230,13 @@ ColumnLayout {
Layout.topMargin: Style.marginXL
Layout.bottomMargin: Style.marginXL
}
- ColumnLayout {
- spacing: Style.marginL
- Layout.fillWidth: true
- NHeader {
- label: I18n.tr("settings.general.lockscreen.section.label")
- description: I18n.tr("settings.general.lockscreen.section.description")
+ NButton {
+ visible: !DistroService.isNixOS
+ text: I18n.tr("settings.general.launch-setup-wizard")
+ onClicked: {
+ setupWizardLoader.active = false
+ setupWizardLoader.active = true
}
-
- NToggle {
- label: I18n.tr("settings.general.lockscreen.lock-on-suspend.label")
- description: I18n.tr("settings.general.lockscreen.lock-on-suspend.description")
- checked: Settings.data.general.lockOnSuspend
- onToggled: Settings.data.general.lockOnSuspend = checked
- }
- }
-
- NDivider {
- Layout.fillWidth: true
- Layout.topMargin: Style.marginXL
- Layout.bottomMargin: Style.marginXL
}
}
diff --git a/Modules/Settings/Tabs/LockScreenTab.qml b/Modules/Settings/Tabs/LockScreenTab.qml
new file mode 100644
index 00000000..2b9d0ea7
--- /dev/null
+++ b/Modules/Settings/Tabs/LockScreenTab.qml
@@ -0,0 +1,31 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import qs.Commons
+import qs.Services
+import qs.Widgets
+
+ColumnLayout {
+ id: root
+
+ NToggle {
+ label: I18n.tr("settings.lock-screen.lock-on-suspend.label")
+ description: I18n.tr("settings.lock-screen.lock-on-suspend.description")
+ checked: Settings.data.general.lockOnSuspend
+ onToggled: Settings.data.general.lockOnSuspend = checked
+ }
+
+ NToggle {
+ label: I18n.tr("settings.lock-screen.compact-lockscreen.label")
+ description: I18n.tr("settings.lock-screen.compact-lockscreen.description")
+ checked: Settings.data.general.compactLockScreen
+ onToggled: checked => Settings.data.general.compactLockScreen = checked
+ }
+
+ NDivider {
+ Layout.fillWidth: true
+ Layout.topMargin: Style.marginXL
+ Layout.bottomMargin: Style.marginXL
+ }
+}
diff --git a/Modules/Settings/Tabs/NotificationsTab.qml b/Modules/Settings/Tabs/NotificationsTab.qml
index beae8cdc..316b620d 100644
--- a/Modules/Settings/Tabs/NotificationsTab.qml
+++ b/Modules/Settings/Tabs/NotificationsTab.qml
@@ -68,8 +68,8 @@ ColumnLayout {
NToggle {
label: I18n.tr("settings.notifications.settings.always-on-top.label")
description: I18n.tr("settings.notifications.settings.always-on-top.description")
- checked: Settings.data.notifications.alwaysOnTop
- onToggled: checked => Settings.data.notifications.alwaysOnTop = checked
+ checked: Settings.data.notifications.overlayLayer
+ onToggled: checked => Settings.data.notifications.overlayLayer = checked
}
// OSD settings moved to the dedicated OSD tab
diff --git a/Modules/Settings/Tabs/OsdTab.qml b/Modules/Settings/Tabs/OsdTab.qml
index edb16fd3..365b2396 100644
--- a/Modules/Settings/Tabs/OsdTab.qml
+++ b/Modules/Settings/Tabs/OsdTab.qml
@@ -86,8 +86,8 @@ ColumnLayout {
NToggle {
label: I18n.tr("settings.osd.always-on-top.label")
description: I18n.tr("settings.osd.always-on-top.description")
- checked: Settings.data.osd.alwaysOnTop
- onToggled: checked => Settings.data.osd.alwaysOnTop = checked
+ checked: Settings.data.osd.overlayLayer
+ onToggled: checked => Settings.data.osd.overlayLayer = checked
}
NLabel {
diff --git a/Modules/Settings/Tabs/UserInterfaceTab.qml b/Modules/Settings/Tabs/UserInterfaceTab.qml
index 13557c29..e3190927 100644
--- a/Modules/Settings/Tabs/UserInterfaceTab.qml
+++ b/Modules/Settings/Tabs/UserInterfaceTab.qml
@@ -34,10 +34,10 @@ ColumnLayout {
}
NToggle {
- label: I18n.tr("settings.user-interface.compact-lockscreen.label")
- description: I18n.tr("settings.user-interface.compact-lockscreen.description")
- checked: Settings.data.general.compactLockScreen
- onToggled: checked => Settings.data.general.compactLockScreen = checked
+ label: I18n.tr("settings.user-interface.panels-overlay.label")
+ description: I18n.tr("settings.user-interface.panels-overlay.description")
+ checked: Settings.data.ui.panelsOverlayLayer
+ onToggled: checked => Settings.data.ui.panelsOverlayLayer = checked
}
NDivider {
diff --git a/Modules/SetupWizard/SetupAppearanceStep.qml b/Modules/SetupWizard/SetupAppearanceStep.qml
index e247e070..a59b2ad3 100644
--- a/Modules/SetupWizard/SetupAppearanceStep.qml
+++ b/Modules/SetupWizard/SetupAppearanceStep.qml
@@ -124,14 +124,14 @@ ColumnLayout {
spacing: 2
NText {
- text: I18n.tr("settings.color-scheme.color-source.dark-mode.label")
+ text: I18n.tr("settings.color-scheme.dark-mode.switch.label")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
- text: I18n.tr("settings.color-scheme.color-source.dark-mode.description")
+ text: I18n.tr("settings.color-scheme.dark-mode.switch.description")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
@@ -167,7 +167,7 @@ ColumnLayout {
color: Color.mSurface
NIcon {
- icon: "color-picker"
+ icon: ProgramCheckerService.matugenAvailable ? "color-picker" : "alert-triangle"
pointSize: Style.fontSizeL
color: Color.mPrimary
anchors.centerIn: parent
@@ -196,7 +196,6 @@ ColumnLayout {
NToggle {
enabled: ProgramCheckerService.matugenAvailable
- opacity: ProgramCheckerService.matugenAvailable ? 1.0 : 0.6
checked: Settings.data.colorSchemes.useWallpaperColors && ProgramCheckerService.matugenAvailable
onToggled: checked => {
if (!ProgramCheckerService.matugenAvailable)
@@ -214,34 +213,6 @@ ColumnLayout {
}
}
- // Matugen not available notice
- RowLayout {
- Layout.fillWidth: true
- spacing: Style.marginS
- visible: !ProgramCheckerService.matugenAvailable
-
- Rectangle {
- width: 28
- height: 28
- radius: Style.radiusM
- color: Color.mSurface
- NIcon {
- icon: "alert-triangle"
- pointSize: Style.fontSizeL
- color: Color.mPrimary
- anchors.centerIn: parent
- }
- }
- NText {
- text: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.description")
- // Reuse description; availability is visually indicated
- pointSize: Style.fontSizeS
- color: Color.mOnSurfaceVariant
- wrapMode: Text.WordWrap
- Layout.fillWidth: true
- }
- }
-
// Matugen scheme type (visible when wallpaper colors enabled and matugen available)
ColumnLayout {
Layout.fillWidth: true
diff --git a/Modules/Toast/ToastScreen.qml b/Modules/Toast/ToastScreen.qml
index badb00b6..fafd9e04 100644
--- a/Modules/Toast/ToastScreen.qml
+++ b/Modules/Toast/ToastScreen.qml
@@ -197,7 +197,7 @@ Item {
color: Color.transparent
- WlrLayershell.layer: WlrLayer.Overlay
+ WlrLayershell.layer: (Settings.data.notifications && Settings.data.notifications.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: PanelWindow.ExclusionMode.Ignore
diff --git a/Services/AppThemeService.qml b/Services/AppThemeService.qml
index ac5eb72a..c54662ce 100644
--- a/Services/AppThemeService.qml
+++ b/Services/AppThemeService.qml
@@ -70,8 +70,8 @@ Singleton {
"vicinae": {
"input": "vicinae.toml",
"outputs": [{
- "path": "~/.local/share/vicinae/themes/matugen.toml"
- }],
+ "path": "~/.local/share/vicinae/themes/matugen.toml"
+ }],
"postProcess": () => `cp -n ${Quickshell.shellDir}/Assets/noctalia.svg ~/.local/share/vicinae/themes/noctalia.svg && ${colorsApplyScript} vicinae\n`
}
})
diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml
index a5823db6..f825f2b5 100644
--- a/Services/BarWidgetRegistry.qml
+++ b/Services/BarWidgetRegistry.qml
@@ -30,6 +30,7 @@ Singleton {
"Spacer": spacerComponent,
"SystemMonitor": systemMonitorComponent,
"Taskbar": taskbarComponent,
+ "TaskbarGrouped": taskbarGroupedComponent,
"Tray": trayComponent,
"Volume": volumeComponent,
"WiFi": wiFiComponent,
@@ -53,6 +54,10 @@ Singleton {
"displayMode": "onhover",
"warningThreshold": 30
},
+ "Bluetooth": {
+ "allowUserSettings": true,
+ "displayMode": "onhover"
+ },
"Brightness": {
"allowUserSettings": true,
"displayMode": "onhover"
@@ -124,11 +129,18 @@ Singleton {
"hideMode": "hidden",
"colorizeIcons": false
},
+ "TaskbarGrouped": {
+ "allowUserSettings": true
+ },
"Tray": {
"allowUserSettings": true,
"blacklist": [],
"colorizeIcons": false
},
+ "WiFi": {
+ "allowUserSettings": true,
+ "displayMode": "onhover"
+ },
"Workspace": {
"allowUserSettings": true,
"labelMode": "index",
@@ -216,6 +228,9 @@ Singleton {
property Component taskbarComponent: Component {
Taskbar {}
}
+ property Component taskbarGroupedComponent: Component {
+ TaskbarGrouped {}
+ }
function init() {
Logger.i("BarWidgetRegistry", "Service started")
diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml
index d6c56e79..f6a42743 100644
--- a/Services/BluetoothService.qml
+++ b/Services/BluetoothService.qml
@@ -21,6 +21,12 @@ Singleton {
return dev && (dev.paired || dev.trusted)
})
}
+ readonly property var connectedDevices: {
+ if (!adapter || !adapter.devices) {
+ return []
+ }
+ return adapter.devices.values.filter(dev => dev && dev.connected)
+ }
readonly property var allDevicesWithBattery: {
if (!adapter || !adapter.devices) {
diff --git a/Services/CalendarService.qml b/Services/CalendarService.qml
new file mode 100644
index 00000000..b152d177
--- /dev/null
+++ b/Services/CalendarService.qml
@@ -0,0 +1,250 @@
+pragma Singleton
+
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.Commons
+
+Singleton {
+ id: root
+
+ // Core state
+ property var events: ([])
+ property bool loading: false
+ property bool available: false
+ property string lastError: ""
+ property var calendars: ([])
+
+ // Persistent cache
+ property string cacheFile: Settings.cacheDir + "calendar.json"
+
+ // Python scripts
+ readonly property string checkCalendarAvailableScript: Quickshell.shellDir + '/Bin/check-calendar.py'
+ readonly property string listCalendarsScript: Quickshell.shellDir + '/Bin/list-calendars.py'
+ readonly property string calendarEventsScript: Quickshell.shellDir + '/Bin/calendar-events.py'
+
+ // Cache file handling
+ FileView {
+ id: cacheFileView
+ path: root.cacheFile
+ printErrors: false
+
+ JsonAdapter {
+ id: cacheAdapter
+ property var cachedEvents: ([])
+ property var cachedCalendars: ([])
+ property string lastUpdate: ""
+ }
+
+ onLoadFailed: {
+ cacheAdapter.cachedEvents = ([])
+ cacheAdapter.cachedCalendars = ([])
+ cacheAdapter.lastUpdate = ""
+ }
+
+ onLoaded: {
+ loadFromCache()
+ }
+ }
+
+ Component.onCompleted: {
+ Logger.i("Calendar", "Service initialized")
+ loadFromCache()
+ checkAvailability()
+ }
+
+ // Save cache with debounce
+ Timer {
+ id: saveDebounce
+ interval: 1000
+ onTriggered: cacheFileView.writeAdapter()
+ }
+
+ function saveCache() {
+ saveDebounce.restart()
+ }
+
+ // Load events and calendars from cache
+ function loadFromCache() {
+ if (cacheAdapter.cachedEvents && cacheAdapter.cachedEvents.length > 0) {
+ root.events = cacheAdapter.cachedEvents
+ Logger.i("Calendar", `Loaded ${cacheAdapter.cachedEvents.length} cached event(s)`)
+ }
+
+ if (cacheAdapter.cachedCalendars && cacheAdapter.cachedCalendars.length > 0) {
+ root.calendars = cacheAdapter.cachedCalendars
+ Logger.i("Calendar", `Loaded ${cacheAdapter.cachedCalendars.length} cached calendar(s)`)
+ }
+
+ if (cacheAdapter.lastUpdate) {
+ Logger.i("Calendar", `Cache last updated: ${cacheAdapter.lastUpdate}`)
+ }
+ }
+
+ // Auto-refresh timer (every 5 minutes)
+ Timer {
+ id: refreshTimer
+ interval: 300000
+ running: true
+ repeat: true
+ onTriggered: loadEvents()
+ }
+
+ // Core functions
+ function checkAvailability() {
+ availabilityCheckProcess.running = true
+ }
+
+ function loadCalendars() {
+ listCalendarsProcess.running = true
+ }
+
+ function loadEvents(daysAhead = 31, daysBehind = 14) {
+ if (loading)
+ return
+
+ loading = true
+ lastError = ""
+
+ const now = new Date()
+ const startDate = new Date(now.getTime() - (daysBehind * 24 * 60 * 60 * 1000))
+ const endDate = new Date(now.getTime() + (daysAhead * 24 * 60 * 60 * 1000))
+
+ loadEventsProcess.startTime = Math.floor(startDate.getTime() / 1000)
+ loadEventsProcess.endTime = Math.floor(endDate.getTime() / 1000)
+ loadEventsProcess.running = true
+
+ Logger.i("Calendar", `Loading events (${daysBehind} days behind, ${daysAhead} days ahead): ${startDate.toLocaleDateString()} to ${endDate.toLocaleDateString()}`)
+ }
+
+ // Helper to format date/time
+ function formatDateTime(timestamp) {
+ const date = new Date(timestamp * 1000)
+ return Qt.formatDateTime(date, "yyyy-MM-dd hh:mm")
+ }
+
+ // Process to check for evolution-data-server libraries
+ Process {
+ id: availabilityCheckProcess
+ running: false
+ command: ["sh", "-c", "command -v python3 >/dev/null 2>&1 && python3 " + root.checkCalendarAvailableScript + " || echo 'unavailable: python3 not installed'"]
+
+ stdout: StdioCollector {
+ onStreamFinished: {
+ const result = text.trim()
+ root.available = result === "available"
+
+ if (root.available) {
+ Logger.i("Calendar", "EDS libraries available")
+ loadCalendars()
+ } else {
+ Logger.w("Calendar", "EDS libraries not available: " + result)
+ root.lastError = "Evolution Data Server libraries not installed"
+ }
+ }
+ }
+
+ stderr: StdioCollector {
+ onStreamFinished: {
+ if (text.trim()) {
+ Logger.d("Calendar", "Availability check error: " + text)
+ root.available = false
+ root.lastError = "Failed to check library availability"
+ }
+ }
+ }
+ }
+
+ // Process to list available calendars
+ Process {
+ id: listCalendarsProcess
+ running: false
+ command: ["python3", root.listCalendarsScript]
+
+ stdout: StdioCollector {
+ onStreamFinished: {
+ try {
+ const result = JSON.parse(text.trim())
+ root.calendars = result
+ cacheAdapter.cachedCalendars = result
+ saveCache()
+
+ Logger.i("Calendar", `Found ${result.length} calendar(s)`)
+
+ // Auto-load events after discovering calendars
+ // Only load if we have calendars and no cached events
+ if (result.length > 0 && root.events.length === 0) {
+ loadEvents()
+ } else if (result.length > 0) {
+ // If we already have cached events, load in background
+ loadEvents()
+ }
+ } catch (e) {
+ Logger.d("Calendar", "Failed to parse calendars: " + e)
+ root.lastError = "Failed to parse calendar list"
+ }
+ }
+ }
+
+ stderr: StdioCollector {
+ onStreamFinished: {
+ if (text.trim()) {
+ Logger.d("Calendar", "List calendars error: " + text)
+ root.lastError = text.trim()
+ }
+ }
+ }
+ }
+
+ // Process to load events
+ Process {
+ id: loadEventsProcess
+ running: false
+ property int startTime: 0
+ property int endTime: 0
+
+ command: ["python3", root.calendarEventsScript, startTime.toString(), endTime.toString()]
+
+ stdout: StdioCollector {
+ onStreamFinished: {
+ root.loading = false
+
+ try {
+ const result = JSON.parse(text.trim())
+ root.events = result
+ cacheAdapter.cachedEvents = result
+ cacheAdapter.lastUpdate = new Date().toISOString()
+ saveCache()
+
+ Logger.i("Calendar", `Loaded ${result.length} event(s)`)
+ } catch (e) {
+ Logger.d("Calendar", "Failed to parse events: " + e)
+ root.lastError = "Failed to parse events"
+
+ // Fall back to cached events if available
+ if (cacheAdapter.cachedEvents.length > 0) {
+ root.events = cacheAdapter.cachedEvents
+ Logger.i("Calendar", "Using cached events")
+ }
+ }
+ }
+ }
+
+ stderr: StdioCollector {
+ onStreamFinished: {
+ root.loading = false
+
+ if (text.trim()) {
+ Logger.d("Calendar", "Load events error: " + text)
+ root.lastError = text.trim()
+
+ // Fall back to cached events if available
+ if (cacheAdapter.cachedEvents.length > 0) {
+ root.events = cacheAdapter.cachedEvents
+ Logger.i("Calendar", "Using cached events due to error")
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml
index 307dc9b7..cc752fbd 100644
--- a/Services/CompositorService.qml
+++ b/Services/CompositorService.qml
@@ -256,6 +256,17 @@ Singleton {
return ""
}
+ function getWindowsForWorkspace(workspaceId) {
+ var windowsInWs = []
+ for (var i = 0; i < windows.count; i++) {
+ var window = windows.get(i)
+ if (window.workspaceId === workspaceId) {
+ windowsInWs.push(window)
+ }
+ }
+ return windowsInWs
+ }
+
// Generic workspace switching
function switchToWorkspace(workspace) {
if (backend && backend.switchToWorkspace) {
diff --git a/Services/IPCService.qml b/Services/IPCService.qml
index 3d1d3924..12c7c204 100644
--- a/Services/IPCService.qml
+++ b/Services/IPCService.qml
@@ -77,9 +77,26 @@ Item {
IpcHandler {
target: "lockScreen"
- function toggle() {
+
+ // New preferred method - lock the screen
+ function lock() {
// Only lock if not already locked (prevents the red screen issue)
// Note: No unlock via IPC for security reasons
+ if (!lockScreen.active) {
+ lockScreen.triggeredViaDeprecatedCall = false
+ lockScreen.active = true
+ }
+ }
+
+ // Deprecated: Use 'lockScreen lock' instead
+ function toggle() {
+ // Mark as triggered via deprecated call - warning will show in lock screen
+ lockScreen.triggeredViaDeprecatedCall = true
+
+ // Log deprecation warning for users checking logs
+ Logger.w("IPC", "The 'lockScreen toggle' IPC call is deprecated. Use 'lockScreen lock' instead.")
+
+ // Still functional for backward compatibility
if (!lockScreen.active) {
lockScreen.active = true
}
diff --git a/Services/MatugenTemplates.qml b/Services/MatugenTemplates.qml
index 19066213..8740aaf8 100644
--- a/Services/MatugenTemplates.qml
+++ b/Services/MatugenTemplates.qml
@@ -97,7 +97,8 @@ Singleton {
}, {
"name": "ghostty",
"path": "Terminal/ghostty",
- "output": "~/.config/ghostty/themes/noctalia"
+ "output": "~/.config/ghostty/themes/noctalia",
+ "post_hook": "pkill -SIGUSR2 ghostty"
}, {
"name": "kitty",
"path": "Terminal/kitty.conf",
diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml
index 4bb5042d..8a55af35 100644
--- a/Services/NotificationService.qml
+++ b/Services/NotificationService.qml
@@ -16,6 +16,10 @@ Singleton {
property int maxVisible: 5
property int maxHistory: 100
property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json")
+ property string stateFile: Settings.cacheDir + "notifications-state.json"
+
+ // State
+ property real lastSeenTs: 0
// Models
property ListModel activeList: ListModel {}
@@ -264,7 +268,7 @@ Singleton {
saveHistory()
}
- // Persistence
+ // Persistence - History
FileView {
id: historyFileView
path: historyFile
@@ -281,6 +285,23 @@ Singleton {
}
}
+ // Persistence - State (lastSeenTs, etc.)
+ FileView {
+ id: stateFileView
+ path: stateFile
+ printErrors: false
+ onLoaded: loadState()
+ onLoadFailed: error => {
+ if (error === 2)
+ writeAdapter()
+ }
+
+ JsonAdapter {
+ id: stateAdapter
+ property real lastSeenTs: 0
+ }
+ }
+
Timer {
id: saveTimer
interval: 200
@@ -337,6 +358,35 @@ Singleton {
}
}
+ function loadState() {
+ try {
+ root.lastSeenTs = stateAdapter.lastSeenTs || 0
+
+ // Migration: if state file is empty but settings has lastSeenTs, migrate it
+ if (root.lastSeenTs === 0 && Settings.data.notifications && Settings.data.notifications.lastSeenTs) {
+ root.lastSeenTs = Settings.data.notifications.lastSeenTs
+ saveState()
+ Logger.i("Notifications", "Migrated lastSeenTs from settings to state file")
+ }
+ } catch (e) {
+ Logger.e("Notifications", "Load state failed:", e)
+ }
+ }
+
+ function saveState() {
+ try {
+ stateAdapter.lastSeenTs = root.lastSeenTs
+ stateFileView.writeAdapter()
+ } catch (e) {
+ Logger.e("Notifications", "Save state failed:", e)
+ }
+ }
+
+ function updateLastSeenTs() {
+ root.lastSeenTs = Time.timestamp * 1000
+ saveState()
+ }
+
function getAppName(name) {
if (!name || name.trim() === "")
return "Unknown"
diff --git a/Services/ProgramCheckerService.qml b/Services/ProgramCheckerService.qml
index ca06dc0c..153b35c9 100644
--- a/Services/ProgramCheckerService.qml
+++ b/Services/ProgramCheckerService.qml
@@ -97,8 +97,8 @@ Singleton {
"kittyAvailable": ["which", "kitty"],
"ghosttyAvailable": ["which", "ghostty"],
"footAvailable": ["which", "foot"],
- "fuzzelAvailable": ["which", "fuzzel"],
- "vicinaeAvailable": ["which", "vicinae"],
+ "fuzzelAvailable": ["which", "fuzzel"],
+ "vicinaeAvailable": ["which", "vicinae"],
"app2unitAvailable": ["which", "app2unit"],
"gpuScreenRecorderAvailable": ["sh", "-c", "command -v gpu-screen-recorder >/dev/null 2>&1 || (command -v flatpak >/dev/null 2>&1 && flatpak list --app | grep -q 'com.dec05eba.gpu_screen_recorder')"],
"wlsunsetAvailable": ["which", "wlsunset"]
diff --git a/Widgets/NFilePicker.qml b/Widgets/NFilePicker.qml
index b9bd7863..4b82e77d 100644
--- a/Widgets/NFilePicker.qml
+++ b/Widgets/NFilePicker.qml
@@ -39,8 +39,8 @@ Popup {
function openFilePicker() {
if (!root.currentPath)
root.currentPath = root.initialPath
- shouldResetSelection = true
- open()
+ shouldResetSelection = true
+ open()
}
function getFileIcon(fileName) {
@@ -92,18 +92,18 @@ Popup {
function formatFileSize(bytes) {
if (bytes === 0)
return "0 B"
- const k = 1024, sizes = ["B", "KB", "MB", "GB", "TB"]
- const i = Math.floor(Math.log(bytes) / Math.log(k))
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]
+ const k = 1024, sizes = ["B", "KB", "MB", "GB", "TB"]
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]
}
function confirmSelection() {
if (filePickerPanel.currentSelection.length === 0)
return
- root.selectedPaths = filePickerPanel.currentSelection
- root.accepted(filePickerPanel.currentSelection)
- root.close()
+ root.selectedPaths = filePickerPanel.currentSelection
+ root.accepted(filePickerPanel.currentSelection)
+ root.close()
}
function updateFilteredModel() {
@@ -126,14 +126,14 @@ Popup {
if (root.selectionMode === "folders" && !fileIsDir)
continue
- if (searchText === "" || fileName.toLowerCase().includes(searchText)) {
- filteredModel.append({
- "fileName": fileName,
- "filePath": filePath,
- "fileIsDir": fileIsDir,
- "fileSize": fileSize
- })
- }
+ if (searchText === "" || fileName.toLowerCase().includes(searchText)) {
+ filteredModel.append({
+ "fileName": fileName,
+ "filePath": filePath,
+ "fileIsDir": fileIsDir,
+ "fileSize": fileSize
+ })
+ }
}
}
@@ -165,19 +165,19 @@ Popup {
focus: true
Keys.onPressed: event => {
- if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_F) {
- filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar
- if (filePickerPanel.showSearchBar)
- Qt.callLater(() => searchInput.forceActiveFocus())
- event.accepted = true
- } else if (event.key === Qt.Key_Escape && filePickerPanel.showSearchBar) {
- filePickerPanel.showSearchBar = false
- filePickerPanel.searchText = ""
- filePickerPanel.filterText = ""
- root.updateFilteredModel()
- event.accepted = true
- }
- }
+ if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_F) {
+ filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar
+ if (filePickerPanel.showSearchBar)
+ Qt.callLater(() => searchInput.forceActiveFocus())
+ event.accepted = true
+ } else if (event.key === Qt.Key_Escape && filePickerPanel.showSearchBar) {
+ filePickerPanel.showSearchBar = false
+ filePickerPanel.searchText = ""
+ filePickerPanel.filterText = ""
+ root.updateFilteredModel()
+ event.accepted = true
+ }
+ }
ColumnLayout {
anchors.fill: parent
@@ -473,11 +473,11 @@ Popup {
bottomMargin: Style.marginS
ScrollBar.vertical: scrollBarComponent.createObject(gridView, {
- "parent": gridView,
- "x": gridView.mirrored ? 0 : gridView.width - width,
- "y": 0,
- "height": gridView.height
- })
+ "parent": gridView,
+ "x": gridView.mirrored ? 0 : gridView.width - width,
+ "y": 0,
+ "height": gridView.height
+ })
delegate: Rectangle {
id: gridItem
@@ -533,8 +533,8 @@ Popup {
property bool isImage: {
if (model.fileIsDir)
return false
- const ext = model.fileName.split('.').pop().toLowerCase()
- return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(ext)
+ const ext = model.fileName.split('.').pop().toLowerCase()
+ return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(ext)
}
Image {
@@ -574,10 +574,10 @@ Popup {
color: {
if (isSelected)
return Color.mSecondary
- else if (mouseArea.containsMouse)
- return model.fileIsDir ? Color.mOnTertiary : Color.mOnTertiary
- else
- return model.fileIsDir ? Color.mPrimary : Color.mOnSurfaceVariant
+ else if (mouseArea.containsMouse)
+ return model.fileIsDir ? Color.mOnTertiary : Color.mOnTertiary
+ else
+ return model.fileIsDir ? Color.mPrimary : Color.mOnSurfaceVariant
}
anchors.centerIn: parent
visible: !iconContainer.isImage || thumbnail.status !== Image.Ready
@@ -608,10 +608,10 @@ Popup {
color: {
if (isSelected)
return Color.mSecondary
- else if (mouseArea.containsMouse)
- return Color.mOnTertiary
- else
- return Color.mOnSurfaceVariant
+ else if (mouseArea.containsMouse)
+ return Color.mOnTertiary
+ else
+ return Color.mOnSurfaceVariant
}
pointSize: Style.fontSizeS
font.weight: isSelected ? Style.fontWeightBold : Style.fontWeightRegular
@@ -630,37 +630,37 @@ Popup {
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
- if (mouse.button === Qt.LeftButton) {
- if (model.fileIsDir) {
- // In folder mode, single click selects the folder
- if (root.selectionMode === "folders") {
- filePickerPanel.currentSelection = [model.filePath]
- }
- // In file mode, single click on folder does nothing (must double-click to enter)
- } else {
- // Single click on file selects it (only in file mode)
- if (root.selectionMode === "files") {
- filePickerPanel.currentSelection = [model.filePath]
- }
- }
- }
- }
+ if (mouse.button === Qt.LeftButton) {
+ if (model.fileIsDir) {
+ // In folder mode, single click selects the folder
+ if (root.selectionMode === "folders") {
+ filePickerPanel.currentSelection = [model.filePath]
+ }
+ // In file mode, single click on folder does nothing (must double-click to enter)
+ } else {
+ // Single click on file selects it (only in file mode)
+ if (root.selectionMode === "files") {
+ filePickerPanel.currentSelection = [model.filePath]
+ }
+ }
+ }
+ }
onDoubleClicked: mouse => {
- if (mouse.button === Qt.LeftButton) {
- if (model.fileIsDir) {
- // Double-click on folder always navigates into it
- folderModel.folder = "file://" + model.filePath
- root.currentPath = model.filePath
- } else {
- // Double-click on file selects and confirms (only in file mode)
- if (root.selectionMode === "files") {
- filePickerPanel.currentSelection = [model.filePath]
- root.confirmSelection()
- }
- }
- }
- }
+ if (mouse.button === Qt.LeftButton) {
+ if (model.fileIsDir) {
+ // Double-click on folder always navigates into it
+ folderModel.folder = "file://" + model.filePath
+ root.currentPath = model.filePath
+ } else {
+ // Double-click on file selects and confirms (only in file mode)
+ if (root.selectionMode === "files") {
+ filePickerPanel.currentSelection = [model.filePath]
+ root.confirmSelection()
+ }
+ }
+ }
+ }
}
}
}
@@ -680,9 +680,9 @@ Popup {
color: {
if (filePickerPanel.currentSelection.includes(model.filePath))
return Color.mSecondary
- if (mouseArea.containsMouse)
- return Color.mTertiary
- return Color.transparent
+ if (mouseArea.containsMouse)
+ return Color.mTertiary
+ return Color.transparent
}
radius: Style.radiusS
Behavior on color {
@@ -728,37 +728,37 @@ Popup {
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
- if (mouse.button === Qt.LeftButton) {
- if (model.fileIsDir) {
- // In folder mode, single click selects the folder
- if (root.selectionMode === "folders") {
- filePickerPanel.currentSelection = [model.filePath]
- }
- // In file mode, single click on folder does nothing (must double-click to enter)
- } else {
- // Single click on file selects it (only in file mode)
- if (root.selectionMode === "files") {
- filePickerPanel.currentSelection = [model.filePath]
- }
- }
- }
- }
+ if (mouse.button === Qt.LeftButton) {
+ if (model.fileIsDir) {
+ // In folder mode, single click selects the folder
+ if (root.selectionMode === "folders") {
+ filePickerPanel.currentSelection = [model.filePath]
+ }
+ // In file mode, single click on folder does nothing (must double-click to enter)
+ } else {
+ // Single click on file selects it (only in file mode)
+ if (root.selectionMode === "files") {
+ filePickerPanel.currentSelection = [model.filePath]
+ }
+ }
+ }
+ }
onDoubleClicked: mouse => {
- if (mouse.button === Qt.LeftButton) {
- if (model.fileIsDir) {
- // Double-click on folder always navigates into it
- folderModel.folder = "file://" + model.filePath
- root.currentPath = model.filePath
- } else {
- // Double-click on file selects and confirms (only in file mode)
- if (root.selectionMode === "files") {
- filePickerPanel.currentSelection = [model.filePath]
- root.confirmSelection()
- }
- }
- }
- }
+ if (mouse.button === Qt.LeftButton) {
+ if (model.fileIsDir) {
+ // Double-click on folder always navigates into it
+ folderModel.folder = "file://" + model.filePath
+ root.currentPath = model.filePath
+ } else {
+ // Double-click on file selects and confirms (only in file mode)
+ if (root.selectionMode === "files") {
+ filePickerPanel.currentSelection = [model.filePath]
+ root.confirmSelection()
+ }
+ }
+ }
+ }
}
}
}
@@ -816,7 +816,7 @@ Popup {
Component.onCompleted: {
if (!root.currentPath)
root.currentPath = root.initialPath
- folderModel.folder = "file://" + root.currentPath
+ folderModel.folder = "file://" + root.currentPath
}
}
}
diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml
index 72e4905e..527f31c6 100644
--- a/Widgets/NPanel.qml
+++ b/Widgets/NPanel.qml
@@ -9,6 +9,8 @@ Loader {
property ShellScreen screen
+ property bool useOverlay: Settings.data.ui.panelsOverlayLayer
+
property Component panelContent: null
property real preferredWidth: 700
property real preferredHeight: 900
@@ -157,6 +159,7 @@ Loader {
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "noctalia-panel"
+ WlrLayershell.layer: useOverlay ? WlrLayer.Overlay : WlrLayer.Top
WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
Region {
diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml
index 6043a51d..f11f9ca4 100644
--- a/Widgets/NTextInput.qml
+++ b/Widgets/NTextInput.qml
@@ -75,31 +75,31 @@ ColumnLayout {
propagateComposedEvents: false
onPressed: mouse => {
- mouse.accepted = true
- // Focus the input and position cursor
- input.forceActiveFocus()
- var inputPos = mapToItem(inputContainer, mouse.x, mouse.y)
- if (inputPos.x >= 0 && inputPos.x <= inputContainer.width) {
- var textPos = inputPos.x - Style.marginM
- if (textPos >= 0 && textPos <= input.width) {
- input.cursorPosition = input.positionAt(textPos, input.height / 2)
- }
- }
- }
+ mouse.accepted = true
+ // Focus the input and position cursor
+ input.forceActiveFocus()
+ var inputPos = mapToItem(inputContainer, mouse.x, mouse.y)
+ if (inputPos.x >= 0 && inputPos.x <= inputContainer.width) {
+ var textPos = inputPos.x - Style.marginM
+ if (textPos >= 0 && textPos <= input.width) {
+ input.cursorPosition = input.positionAt(textPos, input.height / 2)
+ }
+ }
+ }
onReleased: mouse => {
- mouse.accepted = true
- }
+ mouse.accepted = true
+ }
onDoubleClicked: mouse => {
- mouse.accepted = true
- input.selectAll()
- }
+ mouse.accepted = true
+ input.selectAll()
+ }
onPositionChanged: mouse => {
- mouse.accepted = true
- }
+ mouse.accepted = true
+ }
onWheel: wheel => {
- wheel.accepted = true
- }
+ wheel.accepted = true
+ }
}
// Container for the actual text field
@@ -167,32 +167,32 @@ ColumnLayout {
property int selectionStart: 0
onPressed: mouse => {
- mouse.accepted = true
- input.forceActiveFocus()
- var pos = input.positionAt(mouse.x, mouse.y)
- input.cursorPosition = pos
- selectionStart = pos
- }
+ mouse.accepted = true
+ input.forceActiveFocus()
+ var pos = input.positionAt(mouse.x, mouse.y)
+ input.cursorPosition = pos
+ selectionStart = pos
+ }
onPositionChanged: mouse => {
- if (mouse.buttons & Qt.LeftButton) {
- mouse.accepted = true
- var pos = input.positionAt(mouse.x, mouse.y)
- input.select(selectionStart, pos)
- }
- }
+ if (mouse.buttons & Qt.LeftButton) {
+ mouse.accepted = true
+ var pos = input.positionAt(mouse.x, mouse.y)
+ input.select(selectionStart, pos)
+ }
+ }
onDoubleClicked: mouse => {
- mouse.accepted = true
- input.selectAll()
- }
+ mouse.accepted = true
+ input.selectAll()
+ }
onReleased: mouse => {
- mouse.accepted = true
- }
+ mouse.accepted = true
+ }
onWheel: wheel => {
- wheel.accepted = true
- }
+ wheel.accepted = true
+ }
}
}
NIconButton {
diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml
index 18259ed6..45ad8bc3 100644
--- a/Widgets/NToggle.qml
+++ b/Widgets/NToggle.qml
@@ -9,6 +9,7 @@ RowLayout {
property string label: ""
property string description: ""
+ property bool enabled: true
property bool checked: false
property bool hovering: false
property int baseSize: Math.round(Style.baseWidgetSize * 0.8 * Style.uiScaleRatio)
@@ -18,6 +19,7 @@ RowLayout {
signal exited
Layout.fillWidth: true
+ opacity: enabled ? 1.0 : 0.6
NLabel {
label: root.label
@@ -71,14 +73,20 @@ RowLayout {
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
+ if (!enabled)
+ return
hovering = true
root.entered()
}
onExited: {
+ if (!enabled)
+ return
hovering = false
root.exited()
}
onClicked: {
+ if (!enabled)
+ return
root.toggled(!root.checked)
}
}