diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 757038cc..8b1ad10a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -15,7 +15,10 @@ Mark the relevant option with an "x". ## Testing Describe how you tested your changes and mark the relevant items. -- [ ] Tested on compositor: (e.g., Hyprland, Sway, Niri) + +- [ ] Tested on niri +- [ ] Tested on Hyprland +- [ ] Tested on sway - [ ] Tested with different bar positions and density settings - [ ] Tested with multiple monitors (if applicable) diff --git a/Assets/MatugenTemplates/spicetify.ini b/Assets/MatugenTemplates/spicetify.ini new file mode 100644 index 00000000..55c68652 --- /dev/null +++ b/Assets/MatugenTemplates/spicetify.ini @@ -0,0 +1,27 @@ +[Comfy] +text = {{colors.on_surface.default.hex_stripped}} +subtext = {{colors.on_surface_variant.default.hex_stripped}} +main = {{colors.surface.default.hex_stripped}} +main-elevated = {{colors.surface.default.hex_stripped}} +main-transition = {{colors.surface_container_lowest.default.hex_stripped}} +highlight = {{colors.surface_container_low.default.hex_stripped}} +highlight-elevated = {{colors.surface_container_highest.default.hex_stripped}} +sidebar = {{colors.surface.default.hex_stripped}} +player = {{colors.surface.default.hex_stripped}} +card = {{colors.surface.default.hex_stripped}} +shadow = {{colors.surface.default.hex_stripped}} +selected-row = {{colors.on_surface.default.hex_stripped}} +button = {{colors.primary.default.hex_stripped}} +button-active = {{colors.primary.default.hex_stripped}} +button-disabled = {{colors.primary.default.hex_stripped}} +tab-active = {{colors.surface.default.hex_stripped}} +notification = {{colors.tertiary.default.hex_stripped}} +notification-error = {{colors.error.default.hex_stripped}} +misc = {{colors.surface.default.hex_stripped}} +play-button = {{colors.secondary.default.hex_stripped}} +play-button-active = {{colors.secondary.default.hex_stripped}} +progress-fg = {{colors.primary.default.hex_stripped}} +progress-bg = {{colors.surface.default.hex_stripped}} +heart = {{colors.error.default.hex_stripped}} +pagelink-active = {{colors.on_tertiary_container.default.hex_stripped}} +radio-btn-active = {{colors.on_tertiary_container.default.hex_stripped}} diff --git a/Assets/MatugenTemplates/vesktop.css b/Assets/MatugenTemplates/vesktop.css index a4b7da11..7208535a 100644 --- a/Assets/MatugenTemplates/vesktop.css +++ b/Assets/MatugenTemplates/vesktop.css @@ -170,6 +170,27 @@ h3[class*="postTitleText"]:hover { opacity: 0.85 !important; } +/* Improve message input/editor text readability */ +[class*="editor"] [class*="markup"], +[class*="slateTextArea"] [class*="markup"], +[class*="slateTextArea"], +[role="textbox"][class*="markup"], +[role="textbox"][class*="editor"], +[class*="editor"] [class*="slateTextArea"], +[data-slate-editor="true"] { + color: var(--text-3) !important; + opacity: 0.9 !important; + line-height: 1.4 !important; +} + +/* Empty/placeholder text in message input */ +[class*="emptyText"], +[class*="emptyText"] span, +[data-slate-zero-width] { + color: var(--text-4) !important; + opacity: 0.7 !important; +} + [class*="messageContent"][class*="deleted"], [class*="text-sm"][class*="messageContent"][class*="deleted"], [data-text-variant*="text-sm"][class*="messageContent"][class*="deleted"] { @@ -197,4 +218,39 @@ h3[class*="postTitleText"]:hover { [class*="tooltip"] div { color: var(--text-2) !important; opacity: 1 !important; +} + +/* Improve link readability - ONLY target link callout containers (not channels or other UI) */ +/* Override CSS variables only within link callout containers */ +div[class*="linkCalloutContainer"] { + --text-muted: var(--text-3) !important; + --text-default: var(--accent-1) !important; +} + +/* Make muted parts (protocol, path) more readable - only direct child spans in link containers */ +div[class*="linkCalloutContainer"] > span[class*="normal"][data-text-variant*="normal"][style*="--text-muted"], +div[class*="linkCalloutContainer"] > span[class*="normal"][style*="color: var(--text-muted)"] { + color: var(--text-3) !important; + opacity: 0.9 !important; +} + +/* Make the domain (semibold part) stand out with accent color - only direct child spans in link containers */ +div[class*="linkCalloutContainer"] > span[class*="semibold"], +div[class*="linkCalloutContainer"] > span[data-text-variant*="semibold"], +div[class*="linkCalloutContainer"] > span[style*="color: var(--text-default)"] { + color: var(--accent-1) !important; + font-weight: 600 !important; + opacity: 1 !important; +} + +/* Hover state for link containers - only affects spans inside link containers */ +div[class*="linkCalloutContainer"]:hover > span[class*="normal"][data-text-variant*="normal"] { + color: var(--text-2) !important; + opacity: 1 !important; +} + +div[class*="linkCalloutContainer"]:hover > span[class*="semibold"], +div[class*="linkCalloutContainer"]:hover > span[data-text-variant*="semibold"] { + color: var(--accent-1) !important; + opacity: 1 !important; } \ No newline at end of file diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index f82cef4e..d116b8ce 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -105,13 +105,17 @@ "use-distro-logo": { "description": "Logo Ihrer Distribution anstelle eines benutzerdefinierten Symbols verwenden.", "label": "Distro-Logo anstelle von Symbol verwenden" + }, + "colorize-distro-logo": { + "description": "Wende die Designfarben auf das Logo deiner Distribution an.", + "label": "Distro-Logo einfärben" } }, "custom-button": { "browse": "Durchsuchen", "collapse-condition": { - "description": "If the output text matches this value, the button will collapse.", - "label": "Collapse condition" + "description": "Wenn der Ausgabetext diesem Wert entspricht, wird die Schaltfläche eingeklappt.", + "label": "Einklappbedingung" }, "display-command-output": { "description": "Befehl eingeben, der in regelmäßigen Abständen ausgeführt wird. Die erste Zeile seiner Ausgabe wird als Text angezeigt.", @@ -119,6 +123,10 @@ "stream-description": "Geben Sie einen Befehl ein, der kontinuierlich ausgeführt werden soll." }, "dynamic-text": "Dynamischer Text", + "hide-vertical": { + "description": "Wenn aktiviert, wird der Text aus der Befehlsausgabe nicht angezeigt, wenn sich die Leiste in einem vertikalen Layout befindet (links oder rechts).", + "label": "Text in vertikaler Leiste ausblenden" + }, "icon": { "description": "Symbol aus der Bibliothek auswählen.", "label": "Symbol" @@ -145,7 +153,7 @@ }, "text-stream": { "description": "Gestreamte Zeilen aus dem Befehl werden als Text auf der Schaltfläche angezeigt.", - "label": "Strom" + "label": "Stream" } }, "dialog": { @@ -161,7 +169,7 @@ "lock-keys": { "browse": "Durchsuchen", "show-caps-lock": { - "description": "Caps Lock Status anzeigen.", + "description": "Caps-Lock-Status anzeigen.", "label": "Feststelltaste" }, "show-num-lock": { @@ -503,6 +511,10 @@ "top_left": "Oben links", "top_right": "Oben rechts" }, + "visualizer-quality": { + "high": "Hoch", + "low": "Niedrig" + }, "visualizer-types": { "linear": "Linear", "mirrored": "Gespiegelt", @@ -693,6 +705,10 @@ "description": "Bevorzugte und ignorierte Medienanwendungen festlegen.", "label": "Medienplayer" }, + "visualizer-quality": { + "description": "Hohe Qualität verbraucht mehr GPU-Ressourcen.", + "label": "Visualisierungsqualität" + }, "visualizer-type": { "description": "Visualisierungstyp für Medienwiedergabe wählen.", "label": "Visualisierungstyp" @@ -796,13 +812,13 @@ "matugen-scheme-type": { "description": { "scheme-content": "Leitet Farben ab, die eng mit dem zugrunde liegenden Bild übereinstimmen.", - "scheme-expressive": "Lebendige Farbpalette mit verspielter Sättigung", - "scheme-fidelity": "Farbtreue Palette, die die Originalfarbtöne beibehält", - "scheme-fruit-salad": "Farbenfroher Mix aus leuchtenden, kontrastreichen Akzenten", + "scheme-expressive": "Lebendige Farbpalette mit verspielter Sättigung.", + "scheme-fidelity": "Farbtreue Palette, die die Originalfarbtöne beibehält.", + "scheme-fruit-salad": "Farbenfroher Mix aus leuchtenden, kontrastreichen Akzenten.", "scheme-monochrome": "Minimale Farbpalette, die auf einem einzigen Farbton basiert.", "scheme-neutral": "Gedämpfte Farbpalette mit dezenten und beruhigenden Tönen.", - "scheme-rainbow": "Vielfältige Palette, die das gesamte Spektrum abdeckt", - "scheme-tonal-spot": "Ausgewogene Farbpalette mit gezielten Akzenten" + "scheme-rainbow": "Vielfältige Palette, die das gesamte Spektrum abdeckt.", + "scheme-tonal-spot": "Ausgewogene Farbpalette mit gezielten Akzenten." }, "label": "Matugen-Schema-Typ" }, @@ -849,7 +865,11 @@ }, "programs": { "code": { - "description": "Schreibe {Dateipfad}. Das Hyprluna-Theme muss manuell installiert und aktiviert werden.", + "description": "Schreibe {Dateipfad}. Das Hyprluna-Theme muss manuell installiert und aktiviert werden", + "description-missing": "Benötigt die Installation von {app}" + }, + "spicetify": { + "description": "Schreibe {Dateipfad}. Das Comfy-Theme muss manuell installiert und aktiviert werden", "description-missing": "Benötigt die Installation von {app}" }, "description": "Anwendungsspezifisches Theming.", @@ -871,7 +891,7 @@ "description-missing": "Erfordert die Installation von {app}" }, "walker": { - "description": "Schreibt {filepath} und setzt das Theme auf noctalia", + "description": "Schreibt {filepath} und setzt das Theme auf Noctalia", "description-missing": "Erfordert die Installation von {app}" } }, @@ -899,7 +919,7 @@ }, "label": "Terminal", "wezterm": { - "description": "Schreibe {Dateipfad} und lade neu.", + "description": "Schreibe {Dateipfad} und lade neu", "description-missing": "Erfordert die Installation von {app}" } }, @@ -942,7 +962,7 @@ "label": "Befehl" }, "enable-on-state-logic": { - "description": "Aktiviert ein zweites Symbol und 'aktiv' Zustand basierend auf einem Prüfbefehl.", + "description": "Aktiviert ein zweites Symbol und 'Aktiv'-Zustand basierend auf einem Prüfbefehl.", "label": "Aktiv-Zustand-Logik aktivieren" }, "general-tooltip-text": { @@ -966,11 +986,11 @@ "label": "Rechtsklick-Befehl" }, "on-state-command": { - "description": "Befehl, der ausgeführt wird, um zu prüfen, ob sich die Schaltfläche im 'aktiv' Zustand befinden soll. Gibt 0 für aktiv, ungleich 0 für inaktiv zurück.", + "description": "Befehl, der ausgeführt wird, um zu prüfen, ob sich die Schaltfläche im 'Aktiv'-Zustand befinden soll. Gibt 0 für aktiv, ungleich 0 für inaktiv zurück.", "label": "Befehl für Aktiv-Zustand-Prüfung" }, "on-state-icon": { - "description": "Das Symbol für die Schaltfläche, wenn sie sich im 'aktiv' Zustand befindet.", + "description": "Das Symbol für die Schaltfläche, wenn sie sich im 'Aktiv'-Zustand befindet.", "label": "Symbol im Aktiv-Zustand" }, "state-checks": { @@ -1087,7 +1107,7 @@ } }, "enabled": { - "description": "Dock vollständig anzeigen oder ausblenden", + "description": "Dock vollständig anzeigen oder ausblenden.", "label": "Dock aktivieren" }, "monitors": { @@ -1377,7 +1397,7 @@ "label": "Bildschirmanzeige aktivieren" }, "enabled": { - "description": "Benachrichtigungs-Daemon aktivieren oder deaktivieren, erfordert einen Neustart von noctalia-shell.", + "description": "Benachrichtigungs-Daemon aktivieren oder deaktivieren, erfordert einen Neustart von Noctalia Shell.", "label": "Benachrichtigungen aktivieren" }, "location": { @@ -1505,7 +1525,7 @@ }, "dim-desktop": { "description": "Den Desktop abdunkeln, wenn Fenster oder Menüs geöffnet sind.", - "label": "Dimmer Schreibtisch" + "label": "Desktop abdunkeln" }, "panels-attached-to-bar": { "description": "Die Paneele rasten an der Stange und den Bildschirmrändern ein und sorgen so für ein nahtloses Erscheinungsbild mit stilvollen, umgekehrten Ecken.", @@ -1719,10 +1739,6 @@ "kofi": { "opened": "Ko-fi-Seite in Ihrem Browser geöffnet" }, - "matugen": { - "failed": "Matugen-Verarbeitung fehlgeschlagen", - "failed-general": "Matugen ist beim Verarbeiten der Vorlagen auf einen Fehler gestoßen" - }, "missing-control-center": { "description": "Das Kontrollzentrum-Widget wurde aus der Leiste entfernt. Um es erneut über die Leiste zu öffnen, fügen Sie das Widget wieder hinzu. Sie können es auch durch Rechtsklick auf die Leiste öffnen.", "label": "Letztes Kontrollzentrum-Widget entfernt" @@ -1755,9 +1771,15 @@ "started": "Aufnahme gestartet", "stopping": "Aufnahme wird gestoppt…" }, + "theming-processor-failed": { + "desc-generic": "Beim Verarbeiten von Vorlagen ist ein Fehler aufgetreten.", + "title-matugen": "Matugen-Templating-Verarbeitung fehlgeschlagen", + "title-predefined": "Die Verarbeitung des vordefinierten Farbschemas ist fehlgeschlagen." + }, "wallpaper-colors": { "disabled": "Hintergrundbild-Farben deaktiviert", "enabled": "Hintergrundbild-Farben aktiviert", + "label": "Hintergrundfarbengenerierung", "not-installed": "Matugen nicht installiert - erforderlich für Hintergrundbild-Farbextraktion" }, "wifi": { @@ -1798,6 +1820,8 @@ "night-light-enabled": "Nachtlicht ist aktiviert.\nLinksklick zum Wechseln des Modus.\nRechtsklick für Einstellungen.", "night-light-forced": "Nachtlicht ist erzwungen.\nLinksklick zum Wechseln des Modus.\nRechtsklick für Einstellungen.", "night-light-not-installed": "Nachtlicht ist nicht verfügbar.\nwlsunset ist nicht installiert.", + "noctalia-performance-disabled": "Noctalia-Leistungsmodus ist deaktiviert.\nLinksklick zum Aktivieren.", + "noctalia-performance-enabled": "Noctalia-Leistungsmodus ist aktiviert.\nLinksklick zum Deaktivieren.", "open-control-center": "Kontrollzentrum öffnen", "open-notification-history-disable-dnd": "Benachrichtigungsverlauf öffnen\nRechtsklick um 'Nicht stören' zu deaktivieren.", "open-notification-history-enable-dnd": "Benachrichtigungsverlauf öffnen\nRechtsklick um 'Nicht stören' zu aktivieren.", diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index ff6b949f..3197e2bd 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -105,6 +105,10 @@ "use-distro-logo": { "description": "Use your distribution's logo instead of a custom icon.", "label": "Use distro logo instead of icon" + }, + "colorize-distro-logo": { + "description": "Apply theme colors to your distribution logo.", + "label": "Colorize distro logo" } }, "custom-button": { @@ -119,6 +123,10 @@ "stream-description": "Enter a command to run continuously." }, "dynamic-text": "Dynamic text", + "hide-vertical": { + "description": "If enabled, the text from the command output will not be shown when the bar is in a vertical layout (left or right).", + "label": "Hide text in vertical bar" + }, "icon": { "description": "Select an icon from the library.", "label": "Icon" @@ -503,6 +511,10 @@ "top_left": "Top left", "top_right": "Top right" }, + "visualizer-quality": { + "high": "High", + "low": "Low" + }, "visualizer-types": { "linear": "Linear", "mirrored": "Mirrored", @@ -682,19 +694,23 @@ "placeholder": "e.g. spotify, vlc, mpv" }, "scrolling-speed": { - "description": "Time in seconds for the title to scroll from start to end", + "description": "Time in seconds for the title to scroll from start to end.", "label": "Scrolling speed" }, "scrolling-title": { - "description": "Enable continuous scrolling for long media titles", + "description": "Enable continuous scrolling for long media titles.", "label": "Scrolling title" }, "section": { "description": "Set your preferred and ignored media applications.", "label": "Media players" }, + "visualizer-quality": { + "description": "High quality use more GPU ressources.", + "label": "Visualization quality" + }, "visualizer-type": { - "description": "Choose a visualization type for media playback", + "description": "Choose a visualization type for media playback.", "label": "Visualization type" } }, @@ -852,6 +868,10 @@ "description": "Write {filepath}. Hyprluna theme needs to be installed and activated manually.", "description-missing": "Requires {app} to be installed" }, + "spicetify": { + "description": "Write {filepath}. Comfy theme needs to be installed and activated manually.", + "description-missing": "Requires {app} to be installed" + }, "description": "Application-specific theming.", "discord": { "description": "Write {filepath} for {client}. Hyprluna theme needs to be activated manually.", @@ -1719,10 +1739,6 @@ "kofi": { "opened": "Ko-fi page opened in your browser" }, - "matugen": { - "failed": "Matugen processing failed", - "failed-general": "Matugen encountered an error while processing templates" - }, "missing-control-center": { "description": "The Control Center widget has been removed from the bar. To access it from the bar again, you will need to re-add the widget. You can open it with right clicking on the bar too.", "label": "Last Control Center widget removed" @@ -1755,9 +1771,15 @@ "started": "Recording started", "stopping": "Stopping recording…" }, + "theming-processor-failed": { + "desc-generic": "Encountered an error while processing templates", + "title-matugen": "Matugen templating processing failed", + "title-predefined": "Predefined color cheme processing failed" + }, "wallpaper-colors": { "disabled": "Wallpaper colors disabled", "enabled": "Wallpaper colors enabled", + "label": "Wallpaper colors generation", "not-installed": "Matugen not installed - required for wallpaper color extraction" }, "wifi": { @@ -1798,6 +1820,8 @@ "night-light-enabled": "Night light is enabled.\nLeft click to cycle mode.\nRight click to access settings.", "night-light-forced": "Night light is forced.\nLeft click to cycle mode.\nRight click to access settings.", "night-light-not-installed": "Night light is not available.\nwlsunset is not installed.", + "noctalia-performance-disabled": "Noctalia performance mode is disabled.\nLeft click to enable.", + "noctalia-performance-enabled": "Noctalia performance mode is enabled.\nLeft click to disable.", "open-control-center": "Open control center", "open-notification-history-disable-dnd": "Open notification history\nRight-click to disable \"Do not disturb\".", "open-notification-history-enable-dnd": "Open notification history\nRight-click to enable \"Do not disturb\".", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 147d9b3f..16677a9d 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -105,6 +105,10 @@ "use-distro-logo": { "description": "Usar el logo de tu distribución en lugar de un icono personalizado.", "label": "Usar logo de la distribución en lugar de icono" + }, + "colorize-distro-logo": { + "description": "Aplica los colores del tema a tu logotipo de distribución.", + "label": "Colorear logotipo de la distribución" } }, "custom-button": { @@ -119,6 +123,10 @@ "stream-description": "Introduce un comando para ejecutar continuamente." }, "dynamic-text": "Texto dinámico", + "hide-vertical": { + "description": "Si está activado, el texto de la salida del comando no se mostrará cuando la barra esté en un diseño vertical (izquierda o derecha).", + "label": "Ocultar texto en barra vertical" + }, "icon": { "description": "Selecciona un icono de la biblioteca.", "label": "Icono" @@ -503,6 +511,10 @@ "top_left": "Superior izquierda", "top_right": "Superior derecha" }, + "visualizer-quality": { + "high": "Alto/Alta", + "low": "Bajo" + }, "visualizer-types": { "linear": "Lineal", "mirrored": "Espejado", @@ -693,6 +705,10 @@ "description": "Establece tus aplicaciones multimedia preferidas e ignoradas.", "label": "Reproductores multimedia" }, + "visualizer-quality": { + "description": "Alta calidad usa más recursos de la GPU.", + "label": "Calidad de visualización" + }, "visualizer-type": { "description": "Elige un tipo de visualización para la reproducción de medios.", "label": "Tipo de visualización" @@ -852,6 +868,10 @@ "description": "Escribe {filepath}. El tema Hyprluna debe ser instalado y activado manualmente.", "description-missing": "Requiere que {app} esté instalado/a." }, + "spicetify": { + "description": "Escribe {filepath}. El tema Comfy debe ser instalado y activado manualmente.", + "description-missing": "Requiere que {app} esté instalado/a." + }, "description": "Tematización específica de aplicaciones.", "discord": { "description": "Escribir {filepath} para {client}", @@ -1719,10 +1739,6 @@ "kofi": { "opened": "Página de Ko-fi abierta en tu navegador" }, - "matugen": { - "failed": "Error en el procesamiento de Matugen", - "failed-general": "Matugen encontró un error al procesar las plantillas" - }, "missing-control-center": { "description": "El widget del Centro de control se eliminó de la barra. Para acceder a él nuevamente desde la barra, debes volver a añadir el widget. También puedes abrirlo haciendo clic derecho en la barra.", "label": "Se eliminó el último widget del Centro de control" @@ -1755,9 +1771,15 @@ "started": "Grabación iniciada", "stopping": "Deteniendo grabación..." }, + "theming-processor-failed": { + "desc-generic": "Se encontró un error al procesar las plantillas.", + "title-matugen": "Falló el procesamiento de la plantilla Matugen.", + "title-predefined": "Falló el procesamiento del esquema de color predefinido." + }, "wallpaper-colors": { "disabled": "Colores del fondo de pantalla desactivados", "enabled": "Colores del fondo de pantalla activados", + "label": "Generación de colores para fondos de pantalla", "not-installed": "Matugen no instalado - requerido para extraer colores del fondo de pantalla" }, "wifi": { @@ -1798,6 +1820,8 @@ "night-light-enabled": "Luz nocturna activada.\nClic izquierdo para cambiar de modo.\nClic derecho para acceder a la configuración.", "night-light-forced": "Luz nocturna forzada.\nClic izquierdo para cambiar de modo.\nClic derecho para acceder a la configuración.", "night-light-not-installed": "Luz nocturna no disponible.\nwlsunset no está instalado.", + "noctalia-performance-disabled": "El modo de rendimiento de Noctalia está desactivado.\nClic izquierdo para activar.", + "noctalia-performance-enabled": "El modo de rendimiento de Noctalia está activado.\nClic izquierdo para desactivar.", "open-control-center": "Abrir el centro de control", "open-notification-history-disable-dnd": "Abrir historial de notificaciones\nClic derecho para desactivar \"No molestar\".", "open-notification-history-enable-dnd": "Abrir historial de notificaciones\nClic derecho para activar \"No molestar\".", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 8409bd0f..17facb56 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -105,6 +105,10 @@ "use-distro-logo": { "description": "Utiliser le logo de votre distribution au lieu d'une icône personnalisée.", "label": "Utiliser le logo de la distribution au lieu de l'icône" + }, + "colorize-distro-logo": { + "description": "Appliquez les couleurs du thème au logo de votre distribution.", + "label": "Coloriser le logo de la distribution" } }, "custom-button": { @@ -119,6 +123,10 @@ "stream-description": "Entrez une commande à exécuter en continu." }, "dynamic-text": "Texte dynamique", + "hide-vertical": { + "description": "Si activé, le texte de la sortie de la commande ne sera pas affiché lorsque la barre est en disposition verticale (gauche ou droite).", + "label": "Masquer le texte dans la barre verticale" + }, "icon": { "description": "Sélectionnez une icône dans la bibliothèque.", "label": "Icône" @@ -503,6 +511,10 @@ "top_left": "En haut à gauche", "top_right": "En haut à droite" }, + "visualizer-quality": { + "high": "Haut/Haute", + "low": "Bas" + }, "visualizer-types": { "linear": "Linéaire", "mirrored": "Miroir", @@ -693,6 +705,10 @@ "description": "Définissez vos applications multimédias préférées et ignorées.", "label": "Lecteurs multimédias" }, + "visualizer-quality": { + "description": "L'utilisation de haute qualité nécessite plus de ressources GPU.", + "label": "Qualité de la visualisation" + }, "visualizer-type": { "description": "Choisissez un type de visualisation pour la lecture multimédia.", "label": "Type de visualisation" @@ -852,6 +868,10 @@ "description": "Écrire {filepath}. Le thème Hyprluna doit être installé et activé manuellement.", "description-missing": "Nécessite l'installation de {app}" }, + "spicetify": { + "description": "Écrire {filepath}. Le thème Comfy doit être installé et activé manuellement.", + "description-missing": "Nécessite l'installation de {app}" + }, "description": "Thématisation spécifique aux applications.", "discord": { "description": "Écrire {filepath} pour {client}", @@ -1719,10 +1739,6 @@ "kofi": { "opened": "Page Ko-fi ouverte dans votre navigateur" }, - "matugen": { - "failed": "Échec du traitement Matugen", - "failed-general": "Matugen a rencontré une erreur lors du traitement des modèles" - }, "missing-control-center": { "description": "Le widget du Centre de contrôle a été retiré de la barre. Pour y accéder à nouveau depuis la barre, veuillez ré‑ajouter le widget. Vous pouvez aussi l'ouvrir en cliquant avec le bouton droit sur la barre.", "label": "Dernier widget du Centre de contrôle supprimé" @@ -1755,9 +1771,15 @@ "started": "Enregistrement démarré", "stopping": "Arrêt de l'enregistrement..." }, + "theming-processor-failed": { + "desc-generic": "Une erreur s'est produite lors du traitement des modèles.", + "title-matugen": "Le traitement du modèle Matugen a échoué.", + "title-predefined": "Le traitement du schéma de couleurs prédéfini a échoué." + }, "wallpaper-colors": { "disabled": "Couleurs du fond d'écran désactivées", "enabled": "Couleurs du fond d'écran activées", + "label": "Génération de couleurs pour fonds d'écran", "not-installed": "Matugen non installé - requis pour l'extraction des couleurs du fond d'écran" }, "wifi": { @@ -1798,6 +1820,8 @@ "night-light-enabled": "L'éclairage nocturne est activé.\nClic gauche pour changer de mode.\nClic droit pour accéder aux paramètres.", "night-light-forced": "L'éclairage nocturne est forcé.\nClic gauche pour changer de mode.\nClic droit pour accéder aux paramètres.", "night-light-not-installed": "L'éclairage nocturne n'est pas disponible.\nwlsunset n'est pas installé.", + "noctalia-performance-disabled": "Le mode performance Noctalia est désactivé.\nClic gauche pour activer.", + "noctalia-performance-enabled": "Le mode performance Noctalia est activé.\nClic gauche pour désactiver.", "open-control-center": "Ouvrir le centre de contrôle", "open-notification-history-disable-dnd": "Ouvrir l'historique des notifications\nClic droit pour désactiver \"Ne pas déranger\".", "open-notification-history-enable-dnd": "Ouvrir l'historique des notifications\nClic droit pour activer \"Ne pas déranger\".", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index dab199b6..46e3666f 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -105,6 +105,10 @@ "use-distro-logo": { "description": "Usar o logo da sua distribuição em vez de um ícone personalizado.", "label": "Usar logo da distribuição em vez de ícone" + }, + "colorize-distro-logo": { + "description": "Aplicar as cores do tema ao logotipo da sua distribuição.", + "label": "Colorir logo da distribuição" } }, "custom-button": { @@ -119,6 +123,10 @@ "stream-description": "Insira um comando para executar continuamente." }, "dynamic-text": "Texto dinâmico", + "hide-vertical": { + "description": "Se ativado, o texto da saída do comando não será exibido quando a barra estiver em um layout vertical (esquerda ou direita).", + "label": "Ocultar texto na barra vertical" + }, "icon": { "description": "Selecione um ícone da biblioteca.", "label": "Ícone" @@ -503,6 +511,10 @@ "top_left": "Superior esquerda", "top_right": "Superior direita" }, + "visualizer-quality": { + "high": "Alto(a)", + "low": "Baixo(a)" + }, "visualizer-types": { "linear": "Linear", "mirrored": "Espelhado", @@ -693,6 +705,10 @@ "description": "Defina seus aplicativos de mídia preferidos e ignorados.", "label": "Reprodutores de mídia" }, + "visualizer-quality": { + "description": "Alta qualidade usa mais recursos da GPU.", + "label": "Qualidade da visualização" + }, "visualizer-type": { "description": "Escolha um tipo de visualização para a reprodução de mídia", "label": "Tipo de visualização" @@ -852,6 +868,10 @@ "description": "Escreva em {filepath}. O tema Hyprluna precisa ser instalado e ativado manualmente.", "description-missing": "Requer que o {app} esteja instalado." }, + "spicetify": { + "description": "Escreva em {filepath}. O tema Comfy precisa ser instalado e ativado manualmente.", + "description-missing": "Requer que o {app} esteja instalado." + }, "description": "Tematização específica de aplicativos.", "discord": { "description": "Escrever {filepath} para {client}", @@ -1719,10 +1739,6 @@ "kofi": { "opened": "Página do Ko-fi aberta no seu navegador" }, - "matugen": { - "failed": "Falha no processamento do Matugen", - "failed-general": "Matugen encontrou um erro ao processar os modelos" - }, "missing-control-center": { "description": "O widget da Central de Controle foi removido da barra. Para acessá-lo novamente pela barra, adicione o widget novamente. Você também pode abri-lo clicando com o botão direito na barra.", "label": "Último widget da Central de Controle removido" @@ -1755,9 +1771,15 @@ "started": "Gravação iniciada", "stopping": "Parando gravação…" }, + "theming-processor-failed": { + "desc-generic": "Ocorreu um erro ao processar os templates.", + "title-matugen": "Falha no processamento do template Matugen", + "title-predefined": "O processamento do esquema de cores predefinido falhou." + }, "wallpaper-colors": { "disabled": "Cores do papel de parede desativadas", "enabled": "Cores do papel de parede ativadas", + "label": "Geração de cores para papel de parede", "not-installed": "Matugen não instalado - necessário para extração de cores do papel de parede" }, "wifi": { @@ -1798,6 +1820,8 @@ "night-light-enabled": "Luz noturna ativada.\nClique esquerdo para alternar o modo.\nClique direito para acessar as configurações.", "night-light-forced": "Luz noturna forçada.\nClique esquerdo para alternar o modo.\nClique direito para acessar as configurações.", "night-light-not-installed": "Luz noturna não disponível.\nwlsunset não está instalado.", + "noctalia-performance-disabled": "O modo de desempenho Noctalia está desativado.\nClique esquerdo para ativar.", + "noctalia-performance-enabled": "O modo de desempenho Noctalia está ativado.\nClique esquerdo para desativar.", "open-control-center": "Abrir central de controle", "open-notification-history-disable-dnd": "Abrir histórico de notificações\nClique direito para desativar \"Não perturbe\".", "open-notification-history-enable-dnd": "Abrir histórico de notificações\nClique direito para ativar \"Não perturbe\".", diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 749063e9..01e237db 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -105,6 +105,10 @@ "use-distro-logo": { "description": "Özel ikon yerine dağıtımınızın logosunu kullanın.", "label": "Dağıtım logosunu ikon yerine kullan" + }, + "colorize-distro-logo": { + "description": "Dağıtım logonuza tema renklerini uygula.", + "label": "Dağıtım logosunu renklendir" } }, "custom-button": { @@ -119,6 +123,10 @@ "stream-description": "Sürekli çalıştırılacak bir komut girin." }, "dynamic-text": "Dinamik metin", + "hide-vertical": { + "description": "Etkinleştirilirse, komut çıktısındaki metin, çubuk dikey düzende (sol veya sağ) olduğunda gösterilmeyecektir.", + "label": "Dikey çubukta metni gizle" + }, "icon": { "description": "Kütüphaneden bir ikon seçin.", "label": "İkon" @@ -503,6 +511,10 @@ "top_left": "Sol üst", "top_right": "Sağ üst" }, + "visualizer-quality": { + "high": "Yüksek", + "low": "Düşük" + }, "visualizer-types": { "linear": "Doğrusal", "mirrored": "Yansıtılmış", @@ -693,6 +705,10 @@ "description": "Tercih ettiğiniz ve yoksayacağınız medya uygulamalarını ayarlayın.", "label": "Medya oynatıcılar" }, + "visualizer-quality": { + "description": "Yüksek kalite daha fazla GPU kaynağı kullanır.", + "label": "Görselleştirme kalitesi" + }, "visualizer-type": { "description": "Medya oynatımı için bir görselleştirme türü seçin", "label": "Görselleştirme türü" @@ -852,6 +868,10 @@ "description": "{filepath} dosyasına yaz. Hyprluna temasının kurulu ve manuel olarak etkinleştirilmiş olması gerekir.", "description-missing": "Kurulum için {app} gereklidir" }, + "spicetify": { + "description": "{filepath} dosyasına yaz. Comfy temasının kurulu ve manuel olarak etkinleştirilmiş olması gerekir.", + "description-missing": "Kurulum için {app} gereklidir" + }, "description": "Uygulamaya özel temalandırma.", "discord": { "description": "{client} için {filepath} dosyasına yaz. Hyprluna temasının manuel olarak etkinleştirilmesi gerekir.", @@ -1719,10 +1739,6 @@ "kofi": { "opened": "Ko-fi sayfası tarayıcınızda açıldı" }, - "matugen": { - "failed": "Matugen işlemi başarısız oldu", - "failed-general": "Matugen şablonları işlerken bir hata ile karşılaştı" - }, "missing-control-center": { "description": "Kontrol Merkezi widget'ı çubuktan kaldırıldı. Çubuktan tekrar erişmek için widget'ı yeniden eklemeniz gerekecek. Çubuğa sağ tıklayarak da açabilirsiniz.", "label": "Son Kontrol Merkezi widget'ı kaldırıldı" @@ -1755,9 +1771,15 @@ "started": "Kayıt başladı", "stopping": "Kayıt durduruluyor..." }, + "theming-processor-failed": { + "desc-generic": "Şablonlar işlenirken bir hatayla karşılaşıldı.", + "title-matugen": "Matugen şablon işleme başarısız oldu.", + "title-predefined": "Önceden tanımlanmış renk şeması işleme başarısız oldu." + }, "wallpaper-colors": { "disabled": "Duvar kağıdı renkleri devre dışı", "enabled": "Duvar kağıdı renkleri etkin", + "label": "Duvar kağıdı renkleri oluşturma", "not-installed": "Matugen kurulu değil - duvar kağıdı renk çıkarmak için gerekli" }, "wifi": { @@ -1798,6 +1820,8 @@ "night-light-enabled": "Gece ışığı etkin.\nMod arasında geçiş yapmak için sol tık.\nAyarlara erişmek için sağ tık.", "night-light-forced": "Gece ışığı zorla.\nMod arasında geçiş yapmak için sol tık.\nAyarlara erişmek için sağ tık.", "night-light-not-installed": "Gece ışığı mevcut değil.\nwlsunset yüklü değil.", + "noctalia-performance-disabled": "Noctalia performans modu devre dışı.\nEtkinleştirmek için sol tıklayın.", + "noctalia-performance-enabled": "Noctalia performans modu etkin.\nDevre dışı bırakmak için sol tıklayın.", "open-control-center": "Kontrol merkezini aç", "open-notification-history-disable-dnd": "Bildirim geçmişini aç\n\"Rahatsız etmeyi\" devre dışı bırakmak için sağ tık.", "open-notification-history-enable-dnd": "Bildirim geçmişini aç\n\"Rahatsız etmeyi\" etkinleştirmek için sağ tık.", diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index f32018c2..3a16c814 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -105,6 +105,10 @@ "use-distro-logo": { "description": "Використовувати логотип вашого дистрибутива замість власного значка.", "label": "Використовувати логотип дистрибутива замість значка" + }, + "colorize-distro-logo": { + "description": "Застосувати кольори теми до логотипа вашого дистрибутива.", + "label": "Розфарбовувати логотип дистрибутива" } }, "custom-button": { @@ -119,6 +123,10 @@ "stream-description": "Введіть команду для безперервного запуску." }, "dynamic-text": "Динамічний текст", + "hide-vertical": { + "description": "Якщо увімкнено, текст з виводу команди не відображатиметься, коли панель знаходиться у вертикальному розташуванні (ліворуч або праворуч).", + "label": "Приховати текст у вертикальній панелі" + }, "icon": { "description": "Вибрати значок з бібліотеки.", "label": "Значок" @@ -503,6 +511,10 @@ "top_left": "Згори ліворуч", "top_right": "Згори праворуч" }, + "visualizer-quality": { + "high": "Високий", + "low": "Низький" + }, "visualizer-types": { "linear": "Лінійний", "mirrored": "Дзеркальний", @@ -693,6 +705,10 @@ "description": "Встановіть бажані та ігноровані медіапрограми.", "label": "Медіаплеєри" }, + "visualizer-quality": { + "description": "Висока якість використовує більше ресурсів GPU.", + "label": "Якість візуалізації" + }, "visualizer-type": { "description": "Виберіть тип візуалізації для відтворення медіа", "label": "Тип візуалізації" @@ -852,6 +868,10 @@ "description": "Записати {filepath}. Тему Hyprluna потрібно встановити та активувати вручну.", "description-missing": "Потрібна установка {app}" }, + "spicetify": { + "description": "Записати {filepath}. Тему Comfy потрібно встановити та активувати вручну.", + "description-missing": "Потрібна установка {app}" + }, "description": "Оформлення окремих програм.", "discord": { "description": "Записати {filepath} для {client}. Тему Hyprluna потрібно активувати вручну.", @@ -1719,10 +1739,6 @@ "kofi": { "opened": "Сторінка Ko-fi відкрита у вашому браузері" }, - "matugen": { - "failed": "Помилка обробки Matugen", - "failed-general": "Matugen зіткнувся з помилкою під час обробки шаблонів" - }, "missing-control-center": { "description": "Віджет Центру керування видалено з панелі. Щоб знову отримати до нього доступ з панелі, вам потрібно повторно додати віджет. Ви також можете відкрити його, клацнувши правою кнопкою миші на панелі.", "label": "Останній віджет Центру керування видалено" @@ -1755,9 +1771,15 @@ "started": "Запис розпочато", "stopping": "Зупинка запису…" }, + "theming-processor-failed": { + "desc-generic": "Виникла помилка під час обробки шаблонів.", + "title-matugen": "Помилка обробки шаблонів Matugen", + "title-predefined": "Помилка обробки попередньо визначеної колірної схеми" + }, "wallpaper-colors": { "disabled": "Кольори шпалер вимкнено", "enabled": "Кольори шпалер увімкнено", + "label": "Генерація кольорів шпалер", "not-installed": "Matugen не встановлено - потрібний для витягу кольорів зі шпалер" }, "wifi": { @@ -1798,6 +1820,8 @@ "night-light-enabled": "Нічне світло увімкнено.\nЛівий клік для циклічного режиму.\nПравий клік для доступу до налаштувань.", "night-light-forced": "Нічне світло примусово.\nЛівий клік для циклічного режиму.\nПравий клік для доступу до налаштувань.", "night-light-not-installed": "Нічне світло недоступне.\nwlsunset не встановлено.", + "noctalia-performance-disabled": "Режим продуктивності Noctalia вимкнено.\nЛівий клік для увімкнення.", + "noctalia-performance-enabled": "Режим продуктивності Noctalia увімкнено.\nЛівий клік для вимкнення.", "open-control-center": "Відкрити центр керування", "open-notification-history-disable-dnd": "Відкрити історію сповіщень\nПравий клік для вимкнення \"Не турбувати\".", "open-notification-history-enable-dnd": "Відкрити історію сповіщень\nПравий клік для увімкнення \"Не турбувати\".", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 4c6c57d6..ab79009d 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -105,6 +105,10 @@ "use-distro-logo": { "description": "使用您的发行版徽标而不是自定义图标。", "label": "使用发行版徽标而不是图标" + }, + "colorize-distro-logo": { + "description": "将主题颜色应用到您的发行版徽标。", + "label": "为发行版徽标着色" } }, "custom-button": { @@ -119,6 +123,10 @@ "stream-description": "输入一个要持续运行的命令。" }, "dynamic-text": "动态文本", + "hide-vertical": { + "description": "如果启用,当栏处于垂直布局(左或右)时,将不显示命令输出的文本。", + "label": "在垂直栏中隐藏文本" + }, "icon": { "description": "从库中选择图标。", "label": "图标" @@ -402,11 +410,11 @@ } }, "colors": { - "error": "错误", - "onSurface": "表面之上", - "primary": "主要的", - "secondary": "次要的", - "tertiary": "第三纪" + "error": "错误色", + "onSurface": "表层对应色", + "primary": "主要色", + "secondary": "次要色", + "tertiary": "第三色" }, "control-center": { "position": { @@ -503,6 +511,10 @@ "top_left": "左上", "top_right": "右上" }, + "visualizer-quality": { + "high": "高", + "low": "低" + }, "visualizer-types": { "linear": "线性", "mirrored": "镜像", @@ -693,6 +705,10 @@ "description": "设置您首选和要忽略的媒体应用程序。", "label": "媒体播放器" }, + "visualizer-quality": { + "description": "高质量使用更多 GPU 资源。", + "label": "可视化质量" + }, "visualizer-type": { "description": "选择媒体播放的可视化类型", "label": "可视化类型" @@ -820,7 +836,7 @@ "description": "启用自动切换浅色和深色模式。", "label": "深色模式计划", "location": "位置", - "manual": "手册", + "manual": "手动", "off": "关" }, "switch": { @@ -852,6 +868,10 @@ "description": "写入 {filepath}。Hyprluna 主题需要手动安装和激活。", "description-missing": "需要安装 {app}" }, + "spicetify": { + "description": "写入 {filepath}。Comfy 主题需要手动安装和激活。", + "description-missing": "需要安装 {app}" + }, "description": "应用程序特定主题。", "discord": { "description": "为 {client} 写入 {filepath}", @@ -1574,7 +1594,7 @@ "label": "填充模式" }, "section": { - "label": "外观和感觉" + "label": "外观与体验" }, "transition-duration": { "description": "过渡动画的持续时间(秒)。", @@ -1717,11 +1737,7 @@ "enabled": "已启用" }, "kofi": { - "opened": "Ko-fi页面已在您的浏览器中打开" - }, - "matugen": { - "failed": "Matugen 处理失败", - "failed-general": "Matugen 在处理模板时遇到错误" + "opened": "Ko-fi 页面已在您的浏览器中打开" }, "missing-control-center": { "description": "控制中心小部件已从状态栏中移除。要再次从状态栏访问它,您需要重新添加小部件。您也可以通过右键点击状态栏来打开它。", @@ -1755,9 +1771,15 @@ "started": "录制已开始", "stopping": "正在停止录制…" }, + "theming-processor-failed": { + "desc-generic": "处理模板时遇到错误", + "title-matugen": "Matugen模板处理失败", + "title-predefined": "预定义的颜色方案处理失败" + }, "wallpaper-colors": { "disabled": "壁纸颜色已禁用", "enabled": "壁纸颜色已启用", + "label": "壁纸颜色生成", "not-installed": "Matugen 未安装 - 壁纸颜色提取所需" }, "wifi": { @@ -1798,6 +1820,8 @@ "night-light-enabled": "夜间模式已启用。\n左键点击循环模式。\n右键点击访问设置。", "night-light-forced": "夜间模式已强制启用。\n左键点击循环模式。\n右键点击访问设置。", "night-light-not-installed": "夜间模式不可用。\nwlsunset 未安装。", + "noctalia-performance-disabled": "Noctalia 性能模式已禁用。\n左键点击以启用。", + "noctalia-performance-enabled": "Noctalia 性能模式已启用。\n左键点击以禁用。", "open-control-center": "打开控制中心", "open-notification-history-disable-dnd": "打开通知历史记录\n右键点击禁用\"勿扰模式\"。", "open-notification-history-enable-dnd": "打开通知历史记录\n右键点击启用\"勿扰模式\"。", diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 2c83016d..94b3dfe2 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -1,5 +1,5 @@ { - "settingsVersion": 20, + "settingsVersion": 21, "setupCompleted": false, "bar": { "position": "top", @@ -206,7 +206,6 @@ }, "notifications": { "enabled": true, - "doNotDisturb": false, "monitors": [], "location": "top_right", "overlayLayer": true, @@ -226,8 +225,9 @@ "audio": { "volumeStep": 5, "volumeOverdrive": false, - "cavaFrameRate": 60, + "cavaFrameRate": 30, "visualizerType": "linear", + "visualizerQuality": "high", "mprisBlacklist": [], "preferredPlayer": "" }, @@ -257,17 +257,18 @@ "wezterm": false, "fuzzel": false, "discord": false, + "pywalfox": false, + "vicinae": false, + "walker": false, + "code": false, + "spicetify": false, + "enableUserTemplates": false, "discord_vesktop": false, "discord_webcord": false, "discord_armcord": false, "discord_equibop": false, "discord_lightcord": false, - "discord_dorion": false, - "pywalfox": false, - "vicinae": false, - "walker": false, - "code": false, - "enableUserTemplates": false + "discord_dorion": false }, "nightLight": { "enabled": false, diff --git a/Bin/test-notification-replace.sh b/Bin/test-notification-replace.sh new file mode 100755 index 00000000..50fcf1ae --- /dev/null +++ b/Bin/test-notification-replace.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Test script for notification replacement functionality + +echo "Testing notification replacement..." +echo "" + +# Send initial notification and capture the ID +echo "Step 1: Sending initial notification 'asdf'..." +NOTIF_ID=$(notify-send -p "asdf") +echo "Notification ID: $NOTIF_ID" +echo "" + +# Wait a moment for the notification to appear +sleep 1 + +# Replace the notification +echo "Step 2: Replacing notification $NOTIF_ID with 'test'..." +notify-send -r "$NOTIF_ID" -p "test" +echo "" + +echo "The notification should now show 'test' instead of 'asdf'." +echo "If it still shows 'asdf', the replacement is not working." + diff --git a/CLAUDE.md b/CLAUDE.md index 694af961..e6a59884 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,12 +82,14 @@ Core services that power the shell (40+ services): - `SystemStatService.qml` - System resource monitoring **UI & Theming:** -- `AppThemeService.qml` - Application theming engine +- `AppThemeService.qml` - Main theming facade/orchestrator +- `ColorPaletteGenerator.qml` - Material Design 3 palette generation +- `TemplateRegistry.qml` - Template configurations (apps, terminals, Discord) +- `TemplateProcessor.qml` - Matugen CLI + sed script execution - `ColorSchemeService.qml` - Color scheme management - `DarkModeService.qml` - Dark/light mode switching - `FontService.qml` - Font management - `WallpaperService.qml` - Wallpaper handling (with Matugen integration) -- `MatugenTemplates.qml` - Material You color generation templates - `NightLightService.qml` - Blue light filter **Features:** @@ -121,12 +123,11 @@ Common components used throughout the shell: - `I18n.qml` - Internationalization/translations - `Color.qml` - Color utilities and helpers - `Icons.qml` - Icon management -- `TablerIcons.qml` - Tabler icon set (207KB icon definitions) +- `IconsTabler.qml` - Tabler icon set (207KB icon definitions) - `ThemeIcons.qml` - Theme-specific icons - `Logger.qml` - Logging utility - `Style.qml` - Shared styling definitions - `Time.qml` - Time utilities -- `KeyboardLayout.qml` - Keyboard layout definitions #### `/Widgets/` - Reusable UI Components 40+ custom QML widgets with the "N" prefix (Noctalia): @@ -134,8 +135,8 @@ Common components used throughout the shell: - **Input**: NButton, NIconButton, NIconButtonHot, NTextInput, NSlider, NSpinBox, NToggle, NCheckbox, NRadioButton, NComboBox, NSearchableComboBox - **Display**: NLabel, NText, NIcon, NHeader, NImageCached, NImageCircled, NImageRounded - **Dialogs**: NColorPickerDialog, NFilePicker -- **Special**: NContextMenu, NColorPicker, NIconPicker, NCircleStat, NCollapsible, NSectionEditor, NReorderCheckboxes, NDateTimeTokens, NBusyIndicator, NShapedRectangle -- **System**: NFullScreenWindow, BarExclusionZone +- **Special**: NContextMenu, NColorPicker, NIconPicker, NCircleStat, NCollapsible, NSectionEditor, NReorderCheckboxes, NDateTimeTokens, NBusyIndicator +- **System**: MainScreen, BarExclusionZone #### `/Helpers/` - JavaScript Utilities Helper JavaScript modules: @@ -321,6 +322,66 @@ BarWidgetRegistry and ControlCenterWidgetRegistry: - Dynamic widget loading - Easy extension point for custom widgets +### Singleton Pattern (Quickshell) +Services in Quickshell use the `pragma Singleton` directive: +- Add `pragma Singleton` at the top of the QML file (before imports) +- No qmldir needed - Quickshell handles singleton resolution automatically +- Import with standard path: `import qs.Services` or `import qs.Services.Theming` +- Access via singleton name: `AppThemeService.generate()` + +Example: +```qml +pragma Singleton + +import QtQuick +import qs.Commons + +Singleton { + id: root + + function doSomething() { + // singleton logic + } +} +``` + +### Theming Architecture +The theming system was refactored (Jan 2025) into 4 focused services in `Services/`: + +**AppThemeService.qml** (~60 lines) - Main facade +- Public API: `init()`, `generate()`, `generateFromWallpaper()`, `generateFromPredefinedScheme()` +- Orchestrates theme generation based on wallpaper vs predefined schemes +- Maintains backward compatibility with existing code + +**ColorPaletteGenerator.qml** (~100 lines) - Pure color logic +- Single function: `generatePalette(colors, isDarkMode, isStrict)` +- Generates complete Material Design 3 palette from base colors +- No dependencies on other services (pure computation) + +**TemplateRegistry.qml** (~230 lines) - Configuration source of truth +- Defines all application templates (`applications` array) +- Defines terminal templates (`terminals` array) +- Consolidated Discord clients (7 variants in single config) +- Provides `discordClients` array for ProgramCheckerService +- User template TOML generation + +**TemplateProcessor.qml** (~370 lines) - Template execution +- Dual-path architecture: + - Wallpaper colors: Uses matugen CLI for color extraction + template processing + - Predefined schemes: Uses sed scripts to apply colors to templates +- Handles terminal themes (copies pre-rendered files for predefined schemes) +- User template support (matugen json/image subcommands) + +**Adding new application templates:** +1. Add entry to `TemplateRegistry.applications` array in `Services/TemplateRegistry.qml` +2. Create template file in `/Assets/MatugenTemplates/` +3. Add Settings toggle in `Commons/Settings.qml` (templates.yourapp) +4. Template will automatically work for both wallpaper and predefined paths + +**Discord client handling:** +All Discord clients use the same template (`vesktop.css`) but different output paths. +Auto-detected by ProgramCheckerService, enabled via single `Settings.data.templates.discord` toggle. + ## Git Hooks Uses `lefthook` for git hooks (see lefthook.yml) @@ -352,7 +413,9 @@ See [development guidelines](https://docs.noctalia.dev/development/guideline) - Service initialization: `shell.qml` (Component.onCompleted) - Panel registration: `shell.qml` (panelComponents array) - Theme system: `Services/AppThemeService.qml` -- Color generation: `Services/MatugenTemplates.qml` +- Template configurations: `Services/TemplateRegistry.qml` +- Palette generation: `Services/ColorPaletteGenerator.qml` +- Template processing: `Services/TemplateProcessor.qml` ### Testing - Test on target compositors: Niri, Hyprland, Sway diff --git a/Commons/Color.qml b/Commons/Color.qml index 91fde33c..1e358344 100644 --- a/Commons/Color.qml +++ b/Commons/Color.qml @@ -4,7 +4,6 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons -import qs.Services /* diff --git a/Commons/Icons.qml b/Commons/Icons.qml index 919782d6..acd7e1b7 100644 --- a/Commons/Icons.qml +++ b/Commons/Icons.qml @@ -10,9 +10,9 @@ Singleton { // Expose the font family name for easy access readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : "" - readonly property string defaultIcon: TablerIcons.defaultIcon - readonly property var icons: TablerIcons.icons - readonly property var aliases: TablerIcons.aliases + readonly property string defaultIcon: IconsTabler.defaultIcon + readonly property var icons: IconsTabler.icons + readonly property var aliases: IconsTabler.aliases readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.ttf" // Current active font loader diff --git a/Commons/TablerIcons.qml b/Commons/IconsTabler.qml similarity index 100% rename from Commons/TablerIcons.qml rename to Commons/IconsTabler.qml diff --git a/Commons/KeyboardLayout.qml b/Commons/KeyboardLayout.qml deleted file mode 100644 index 38d08aa1..00000000 --- a/Commons/KeyboardLayout.qml +++ /dev/null @@ -1,205 +0,0 @@ -pragma Singleton - -import QtQuick - -QtObject { - id: root - - // Comprehensive language name to ISO code mapping - property var languageMap: { - "english"// English variants - : "us", - "american": "us", - "united states": "us", - "us english": "us", - "british": "gb", - "uk": "ua", - "united kingdom"// FIXED: Ukrainian language code should map to Ukraine - : "gb", - "english (uk)": "gb", - "canadian": "ca", - "canada": "ca", - "canadian english": "ca", - "australian": "au", - "australia": "au", - "swedish"// Nordic countries - : "se", - "svenska": "se", - "sweden": "se", - "norwegian": "no", - "norsk": "no", - "norway": "no", - "danish": "dk", - "dansk": "dk", - "denmark": "dk", - "finnish": "fi", - "suomi": "fi", - "finland": "fi", - "icelandic": "is", - "íslenska": "is", - "iceland": "is", - "german"// Western/Central European Germanic - : "de", - "deutsch": "de", - "germany": "de", - "austrian": "at", - "austria": "at", - "österreich": "at", - "swiss": "ch", - "switzerland": "ch", - "schweiz": "ch", - "suisse": "ch", - "dutch": "nl", - "nederlands": "nl", - "netherlands": "nl", - "holland": "nl", - "belgian": "be", - "belgium": "be", - "belgië": "be", - "belgique": "be", - "french"// Romance languages (Western/Southern Europe) - : "fr", - "français": "fr", - "france": "fr", - "canadian french": "ca", - "spanish": "es", - "español": "es", - "spain": "es", - "castilian": "es", - "italian": "it", - "italiano": "it", - "italy": "it", - "portuguese": "pt", - "português": "pt", - "portugal": "pt", - "catalan": "ad", - "català": "ad", - "andorra": "ad", - "romanian"// Eastern European Romance - : "ro", - "română": "ro", - "romania": "ro", - "russian"// Slavic languages (Eastern Europe) - : "ru", - "русский": "ru", - "russia": "ru", - "polish": "pl", - "polski": "pl", - "poland": "pl", - "czech": "cz", - "čeština": "cz", - "czech republic": "cz", - "slovak": "sk", - "slovenčina": "sk", - "slovakia": "sk", - "uk": "ua", - "ukrainian"// Ukrainian language code - : "ua", - "українська": "ua", - "ukraine": "ua", - "bulgarian": "bg", - "български": "bg", - "bulgaria": "bg", - "serbian": "rs", - "srpski": "rs", - "serbia": "rs", - "croatian": "hr", - "hrvatski": "hr", - "croatia": "hr", - "slovenian": "si", - "slovenščina": "si", - "slovenia": "si", - "bosnian": "ba", - "bosanski": "ba", - "bosnia": "ba", - "macedonian": "mk", - "македонски": "mk", - "macedonia": "mk", - "irish"// Celtic languages (Western Europe) - : "ie", - "gaeilge": "ie", - "ireland": "ie", - "welsh": "gb", - "cymraeg": "gb", - "wales": "gb", - "scottish": "gb", - "gàidhlig": "gb", - "scotland": "gb", - "estonian"// Baltic languages (Northern Europe) - : "ee", - "eesti": "ee", - "estonia": "ee", - "latvian": "lv", - "latviešu": "lv", - "latvia": "lv", - "lithuanian": "lt", - "lietuvių": "lt", - "lithuania": "lt", - "hungarian"// Other European languages - : "hu", - "magyar": "hu", - "hungary": "hu", - "greek": "gr", - "ελληνικά": "gr", - "greece": "gr", - "albanian": "al", - "shqip": "al", - "albania": "al", - "maltese": "mt", - "malti": "mt", - "malta": "mt", - "turkish"// West/Southwest Asian languages - : "tr", - "türkçe": "tr", - "turkey": "tr", - "arabic": "ar", - "العربية": "ar", - "arab": "ar", - "hebrew": "il", - "עברית": "il", - "israel": "il", - "brazilian"// South American languages - : "br", - "brazilian portuguese": "br", - "brasil": "br", - "brazil": "br", - "japanese"// East Asian languages - : "jp", - "日本語": "jp", - "japan": "jp", - "korean": "kr", - "한국어": "kr", - "korea": "kr", - "south korea": "kr", - "chinese": "cn", - "中文": "cn", - "china": "cn", - "simplified chinese": "cn", - "traditional chinese": "tw", - "taiwan": "tw", - "繁體中文": "tw", - "thai"// Southeast Asian languages - : "th", - "ไทย": "th", - "thailand": "th", - "vietnamese": "vn", - "tiếng việt": "vn", - "vietnam": "vn", - "hindi"// South Asian languages - : "in", - "हिन्दी": "in", - "india": "in", - "afrikaans"// African languages - : "za", - "south africa": "za", - "south african": "za", - "qwerty"// Layout variants - : "us", - "dvorak": "us", - "colemak": "us", - "workman": "us", - "azerty": "fr", - "norman": "fr", - "qwertz": "de" - } -} diff --git a/Commons/Settings.qml b/Commons/Settings.qml index e720a97f..81063cf2 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -4,7 +4,7 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.UI import "../Helpers/QtObj2JS.js" as QtObj2JS Singleton { @@ -14,7 +14,7 @@ Singleton { readonly property alias data: adapter property bool isLoaded: false property bool directoriesCreated: false - property int settingsVersion: 20 + property int settingsVersion: 21 property bool isDebug: Quickshell.env("NOCTALIA_DEBUG") === "1" // Define our app directories @@ -342,7 +342,6 @@ Singleton { // notifications property JsonObject notifications: JsonObject { property bool enabled: true - property bool doNotDisturb: false property list monitors: [] property string location: "top_right" property bool overlayLayer: true @@ -366,8 +365,9 @@ Singleton { property JsonObject audio: JsonObject { property int volumeStep: 5 property bool volumeOverdrive: false - property int cavaFrameRate: 60 + property int cavaFrameRate: 30 property string visualizerType: "linear" + property string visualizerQuality: "high" property list mprisBlacklist: [] property string preferredPlayer: "" } @@ -402,17 +402,19 @@ Singleton { property bool wezterm: false property bool fuzzel: false property bool discord: false - property bool discord_vesktop: false - property bool discord_webcord: false - property bool discord_armcord: false - property bool discord_equibop: false - property bool discord_lightcord: false - property bool discord_dorion: false property bool pywalfox: false property bool vicinae: false property bool walker: false property bool code: false + property bool spicetify: false property bool enableUserTemplates: false + + property bool discord_vesktop: false // To be deleted soon + property bool discord_webcord: false // To be deleted soon + property bool discord_armcord: false // To be deleted soon + property bool discord_equibop: false // To be deleted soon + property bool discord_lightcord: false // To be deleted soon + property bool discord_dorion: false // To be deleted soon } // night light @@ -617,6 +619,28 @@ Singleton { Logger.w("Settings", "Added a ControlCenter widget to the right section") } } + + // ----------------- + // 5th. Migrate Discord templates (version 20 → 21) + // Consolidate individual discord_* properties into unified discord property + if (adapter.settingsVersion < 21) { + var anyDiscordEnabled = false + + // Check if any Discord client was enabled + const discordClients = ["discord_vesktop", "discord_webcord", "discord_armcord", "discord_equibop", "discord_lightcord", "discord_dorion"] + + for (var i = 0; i < discordClients.length; i++) { + if (adapter.templates[discordClients[i]]) { + anyDiscordEnabled = true + break + } + } + + // Set unified discord property + adapter.templates.discord = anyDiscordEnabled + + Logger.i("Settings", "Migrated Discord templates to unified 'discord' property (enabled:", anyDiscordEnabled + ")") + } } // ----------------------------------------------------- diff --git a/Commons/Style.qml b/Commons/Style.qml index bb06ff05..b2999d7b 100644 --- a/Commons/Style.qml +++ b/Commons/Style.qml @@ -2,9 +2,7 @@ pragma Singleton import QtQuick import Quickshell -import Quickshell.Io - -import qs.Services +import qs.Services.Power Singleton { id: root diff --git a/Commons/ThemeIcons.qml b/Commons/ThemeIcons.qml index aa8c4aea..addede93 100644 --- a/Commons/ThemeIcons.qml +++ b/Commons/ThemeIcons.qml @@ -2,7 +2,6 @@ pragma Singleton import QtQuick import Quickshell -import qs.Services Singleton { id: root diff --git a/Commons/Time.qml b/Commons/Time.qml index c346d1e0..b8245808 100644 --- a/Commons/Time.qml +++ b/Commons/Time.qml @@ -3,7 +3,6 @@ pragma Singleton import Quickshell import QtQuick import qs.Commons -import qs.Services Singleton { id: root diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 8a0f43f3..4cf8446b 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -2,8 +2,8 @@ import QtQuick import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Services -import qs.Widgets +import qs.Services.Compositor +import qs.Services.UI Variants { id: backgroundVariants @@ -21,6 +21,7 @@ Variants { // Internal state management property string transitionType: "fade" property real transitionProgress: 0 + property bool isStartupTransition: true readonly property real edgeSmoothness: Settings.data.wallpaper.transitionEdgeSmoothness readonly property var allTransitions: WallpaperService.allTransitions @@ -77,7 +78,11 @@ Variants { Connections { target: CompositorService function onDisplayScalesChanged() { - setWallpaperInitial() + // Recalculate image sizes without interrupting startup transition + if (isStartupTransition) { + return + } + recalculateImageSizes() } } @@ -110,7 +115,7 @@ Variants { property bool dimensionsCalculated: false source: "" - smooth: true + smooth: false mipmap: false visible: false cache: false @@ -139,7 +144,7 @@ Variants { property bool dimensionsCalculated: false source: "" - smooth: true + smooth: false mipmap: false visible: false cache: false @@ -332,10 +337,10 @@ Variants { var dim = Qt.size(0, 0) if (screenWidth >= screenHeight) { const w = Math.min(screenWidth, wpWidth) - dim = Qt.size(w, w / imageAspectRatio) + dim = Qt.size(Math.round(w), Math.round(w / imageAspectRatio)) } else { const h = Math.min(screenHeight, wpHeight) - dim = Qt.size(h * imageAspectRatio, h) + dim = Qt.size(Math.round(h * imageAspectRatio), Math.round(h)) } Logger.d("Background", `Wallpaper resized on ${modelData.name} ${screenWidth}x${screenHeight} @ ${compositorScale}x`, "src:", wpWidth, wpHeight, "dst:", dim.width, dim.height) @@ -344,11 +349,23 @@ Variants { // ------------------------------------------------------ function recalculateImageSizes() { + // Re-evaluate and apply optimal sourceSize for both images when ready if (currentWallpaper.status === Image.Ready) { - currentWallpaper.calculateSourceSize() + const optimal = calculateOptimalWallpaperSize(currentWallpaper.implicitWidth, currentWallpaper.implicitHeight) + if (optimal !== undefined && optimal !== false) { + currentWallpaper.sourceSize = optimal + } else { + currentWallpaper.sourceSize = undefined + } } + if (nextWallpaper.status === Image.Ready) { - nextWallpaper.calculateSourceSize() + const optimal2 = calculateOptimalWallpaperSize(nextWallpaper.implicitWidth, nextWallpaper.implicitHeight) + if (optimal2 !== undefined && optimal2 !== false) { + nextWallpaper.sourceSize = optimal2 + } else { + nextWallpaper.sourceSize = undefined + } } } @@ -360,7 +377,10 @@ Variants { return } - setWallpaperImmediate(WallpaperService.getWallpaper(modelData.name)) + const wallpaperPath = WallpaperService.getWallpaper(modelData.name) + + futureWallpaper = wallpaperPath + performStartupTransition() } // ------------------------------------------------------ @@ -453,6 +473,52 @@ Variants { break } } + + // ------------------------------------------------------ + // Dedicated function for startup animation + function performStartupTransition() { + // Get the transitionType from the settings + transitionType = Settings.data.wallpaper.transitionType + + if (transitionType == "random") { + var index = Math.floor(Math.random() * allTransitions.length) + transitionType = allTransitions[index] + } + + // Ensure the transition type really exists + if (transitionType !== "none" && !allTransitions.includes(transitionType)) { + transitionType = "fade" + } + + // Apply transitionType so the shader loader picks the correct shader + this.transitionType = transitionType + + switch (transitionType) { + case "none": + setWallpaperImmediate(futureWallpaper) + break + case "wipe": + wipeDirection = Math.random() * 4 + setWallpaperWithTransition(futureWallpaper) + break + case "disc": + // Force center origin for elegant startup animation + discCenterX = 0.5 + discCenterY = 0.5 + setWallpaperWithTransition(futureWallpaper) + break + case "stripes": + stripesCount = Math.round(Math.random() * 20 + 4) + stripesAngle = Math.random() * 360 + setWallpaperWithTransition(futureWallpaper) + break + default: + setWallpaperWithTransition(futureWallpaper) + break + } + // Mark startup transition complete + isStartupTransition = false + } } } } diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 394276a3..6dcbd3bc 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -1,10 +1,10 @@ import QtQuick -import QtQuick.Effects import Quickshell +import QtQuick.Effects import Quickshell.Wayland import qs.Commons -import qs.Services -import qs.Widgets +import qs.Services.Compositor +import qs.Services.UI Loader { active: CompositorService.isNiri && Settings.data.wallpaper.enabled && Settings.data.wallpaper.overviewEnabled diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index d858da2c..76d0b5a1 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -5,7 +5,7 @@ import Quickshell import Quickshell.Wayland import Quickshell.Services.UPower import qs.Commons -import qs.Services +import qs.Services.UI import qs.Widgets import qs.Modules.Notification import qs.Modules.Bar.Extras @@ -27,11 +27,8 @@ Item { readonly property string barPosition: Settings.data.bar.position || "top" readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" readonly property bool barFloating: Settings.data.bar.floating || false - readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 - readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 - - // Attachment overlap to fix hairline gap with fractional scaling - readonly property real attachmentOverlap: 1 + readonly property real barMarginH: 0 //barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + readonly property real barMarginV: 0 //barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 // Fill the parent (the Loader) anchors.fill: parent @@ -68,29 +65,28 @@ Item { id: bar // Position and size the bar based on orientation and floating margins - // Extend the bar by attachmentOverlap to eliminate hairline gap x: { var baseX = (root.barPosition === "right") ? (parent.width - Style.barHeight - root.barMarginH) : root.barMarginH if (root.barPosition === "right") - return baseX - root.attachmentOverlap // Extend left towards panels + return baseX // Extend left towards panels return baseX } y: { var baseY = (root.barPosition === "bottom") ? (parent.height - Style.barHeight - root.barMarginV) : root.barMarginV if (root.barPosition === "bottom") - return baseY - root.attachmentOverlap // Extend up towards panels + return baseY // Extend up towards panels return baseY } width: { var baseWidth = root.barIsVertical ? Style.barHeight : (parent.width - root.barMarginH * 2) if (!root.barIsVertical) return baseWidth // Horizontal bars extend via height, not width - return baseWidth + root.attachmentOverlap + 1 + return baseWidth + 1 } height: { var baseHeight = root.barIsVertical ? (parent.height - root.barMarginV * 2) : Style.barHeight if (!root.barIsVertical) - return baseHeight + root.attachmentOverlap + return baseHeight return baseHeight // Vertical bars extend via width, not height } diff --git a/Modules/Bar/Extras/BarPill.qml b/Modules/Bar/Extras/BarPill.qml index aef33339..a8c097cf 100644 --- a/Modules/Bar/Extras/BarPill.qml +++ b/Modules/Bar/Extras/BarPill.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import Quickshell import qs.Commons -import qs.Services import qs.Widgets Item { @@ -18,6 +17,7 @@ Item { property bool forceClose: false property bool oppositeDirection: false property bool hovered: false + property bool rotateText: false readonly property string barPosition: Settings.data.bar.position readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" @@ -53,6 +53,7 @@ Item { oppositeDirection: root.oppositeDirection hovered: root.hovered density: root.density + rotateText: root.rotateText onShown: root.shown() onHidden: root.hidden() onEntered: root.entered() diff --git a/Modules/Bar/Extras/BarPillHorizontal.qml b/Modules/Bar/Extras/BarPillHorizontal.qml index c9d84869..ed74b74b 100644 --- a/Modules/Bar/Extras/BarPillHorizontal.qml +++ b/Modules/Bar/Extras/BarPillHorizontal.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Controls import Quickshell import qs.Commons -import qs.Services +import qs.Services.UI import qs.Widgets Item { diff --git a/Modules/Bar/Extras/BarPillVertical.qml b/Modules/Bar/Extras/BarPillVertical.qml index 492ec735..5718c619 100644 --- a/Modules/Bar/Extras/BarPillVertical.qml +++ b/Modules/Bar/Extras/BarPillVertical.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Controls import Quickshell import qs.Commons -import qs.Services +import qs.Services.UI import qs.Widgets Item { @@ -18,6 +18,7 @@ Item { property bool forceClose: false property bool oppositeDirection: false property bool hovered: false + property bool rotateText: false // Bar position detection for pill direction readonly property string barPosition: Settings.data.bar.position @@ -48,8 +49,8 @@ Item { readonly property int pillHeight: buttonSize readonly property int pillPaddingVertical: 3 * 2 // Very precise adjustment don't replace by Style.margin readonly property int pillOverlap: Math.round(buttonSize * 0.5) - readonly property int maxPillWidth: buttonSize - readonly property int maxPillHeight: Math.max(1, Math.round(textItem.implicitHeight + pillPaddingVertical * 4)) + readonly property int maxPillWidth: rotateText ? Math.max(buttonSize, Math.round(textItem.implicitHeight + pillPaddingVertical * 2)) : buttonSize + readonly property int maxPillHeight: rotateText ? Math.max(1, Math.round(textItem.implicitWidth + pillPaddingVertical * 2 + Math.round(iconCircle.height / 4))) : Math.max(1, Math.round(textItem.implicitHeight + pillPaddingVertical * 4)) readonly property real iconSize: { switch (root.density) { @@ -109,14 +110,8 @@ Item { id: textItem anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: { - 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 += oppositeDirection ? -Style.marginXXS : Style.marginXXS - } - return offset - } + anchors.verticalCenterOffset: rotateText ? Math.round(iconCircle.height / 4) : getVerticalCenterOffset() + rotation: rotateText ? -90 : 0 text: root.text + root.suffix family: Settings.data.ui.fontFixed pointSize: textSize @@ -126,8 +121,15 @@ Item { verticalAlignment: Text.AlignVCenter color: forceOpen ? Color.mOnSurface : Color.mPrimary visible: revealed - } + function getVerticalCenterOffset() { + var offset = openDownward ? Math.round(pillPaddingVertical * 0.75) : -Math.round(pillPaddingVertical * 0.75) + if (forceOpen) { + offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS + } + return offset + } + } Behavior on width { enabled: showAnim.running || hideAnim.running NumberAnimation { diff --git a/Modules/Bar/Extras/BarWidgetLoader.qml b/Modules/Bar/Extras/BarWidgetLoader.qml index fb90c645..71f21414 100644 --- a/Modules/Bar/Extras/BarWidgetLoader.qml +++ b/Modules/Bar/Extras/BarWidgetLoader.qml @@ -1,6 +1,6 @@ import QtQuick import Quickshell -import qs.Services +import qs.Services.UI import qs.Commons Item { diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 8772c5b4..bd8fc4f5 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -1,11 +1,10 @@ import QtQuick -import QtQuick.Controls import QtQuick.Layouts import Quickshell -import Quickshell.Wayland import Quickshell.Widgets import qs.Commons -import qs.Services +import qs.Services.Compositor +import qs.Services.UI import qs.Widgets Item { diff --git a/Modules/Bar/Widgets/AudioVisualizer.qml b/Modules/Bar/Widgets/AudioVisualizer.qml index a83c9e52..cecc6952 100644 --- a/Modules/Bar/Widgets/AudioVisualizer.qml +++ b/Modules/Bar/Widgets/AudioVisualizer.qml @@ -1,9 +1,10 @@ import QtQuick import Quickshell import qs.Commons -import qs.Widgets.AudioSpectrum -import qs.Services +import qs.Services.UI +import qs.Services.Media import qs.Widgets +import qs.Widgets.AudioSpectrum Item { id: root diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index 7eb2a714..08488fe5 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -3,7 +3,8 @@ import Quickshell import Quickshell.Services.UPower import QtQuick.Layouts import qs.Commons -import qs.Services +import qs.Services.UI +import qs.Services.Hardware import qs.Widgets import qs.Modules.Bar.Extras diff --git a/Modules/Bar/Widgets/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml index 84ca7f41..2ce3fea6 100644 --- a/Modules/Bar/Widgets/Bluetooth.qml +++ b/Modules/Bar/Widgets/Bluetooth.qml @@ -1,8 +1,9 @@ import QtQuick import Quickshell import qs.Commons -import qs.Services import qs.Modules.Bar.Extras +import qs.Services.Networking +import qs.Services.UI Item { id: root diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index 615ec16e..e426c4aa 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -3,7 +3,8 @@ import Quickshell import qs.Commons import qs.Modules.Bar.Extras import qs.Modules.Panels.Settings -import qs.Services +import qs.Services.Hardware +import qs.Services.UI import qs.Widgets Item { diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index 8a31bdc3..4eb93dba 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.UI import qs.Widgets Rectangle { diff --git a/Modules/Bar/Widgets/ControlCenter.qml b/Modules/Bar/Widgets/ControlCenter.qml index ae009ee1..11daae13 100644 --- a/Modules/Bar/Widgets/ControlCenter.qml +++ b/Modules/Bar/Widgets/ControlCenter.qml @@ -4,7 +4,8 @@ import Quickshell.Widgets import QtQuick.Effects import qs.Commons import qs.Widgets -import qs.Services +import qs.Services.UI +import qs.Services.System NIconButton { id: root @@ -31,6 +32,11 @@ NIconButton { readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon readonly property bool useDistroLogo: (widgetSettings.useDistroLogo !== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo readonly property string customIconPath: widgetSettings.customIconPath || "" + readonly property bool colorizeDistroLogo: { + if (widgetSettings.colorizeDistroLogo !== undefined) + return widgetSettings.colorizeDistroLogo + return widgetMetadata.colorizeDistroLogo !== undefined ? widgetMetadata.colorizeDistroLogo : false + } // If we have a custom path or distro logo, don't use the theme icon. icon: (customIconPath === "" && !useDistroLogo) ? customIcon : "" @@ -48,12 +54,13 @@ NIconButton { var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen) if (Settings.data.controlCenter.position === "close_to_bar_button") { // Willopen the panel next to the bar button. - controlCenterPanel.toggle(this) + controlCenterPanel?.toggle(this) } else { - controlCenterPanel.toggle() + controlCenterPanel?.toggle() } } onRightClicked: PanelService.getPanel("settingsPanel", screen)?.toggle() + onMiddleClicked: PanelService.getPanel("launcherPanel", screen)?.toggle() IconImage { id: customOrDistroLogo @@ -70,5 +77,12 @@ NIconButton { visible: source !== "" smooth: true asynchronous: true + layer.enabled: useDistroLogo && colorizeDistroLogo + layer.effect: ShaderEffect { + property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant + property real colorizeMode: 1.0 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } } } diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index ab8c3982..3e67fb0c 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -3,7 +3,7 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.UI import qs.Widgets import qs.Modules.Panels.Settings import qs.Modules.Bar.Extras @@ -30,7 +30,8 @@ Item { return {} } - // Use settings or defaults from BarWidgetRegistry + readonly property bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon readonly property string leftClickExec: widgetSettings.leftClickExec || widgetMetadata.leftClickExec readonly property string rightClickExec: widgetSettings.rightClickExec || widgetMetadata.rightClickExec @@ -40,8 +41,11 @@ Item { readonly property int textIntervalMs: widgetSettings.textIntervalMs !== undefined ? widgetSettings.textIntervalMs : (widgetMetadata.textIntervalMs || 3000) readonly property string textCollapse: widgetSettings.textCollapse !== undefined ? widgetSettings.textCollapse : (widgetMetadata.textCollapse || "") readonly property bool parseJson: widgetSettings.parseJson !== undefined ? widgetSettings.parseJson : (widgetMetadata.parseJson || false) + readonly property bool hideTextInVerticalBar: widgetSettings.hideTextInVerticalBar !== undefined ? widgetSettings.hideTextInVerticalBar : (widgetMetadata.hideTextInVerticalBar || false) readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec) + readonly property bool shouldShowText: !isVerticalBar || !hideTextInVerticalBar + implicitWidth: pill.width implicitHeight: pill.height @@ -50,25 +54,37 @@ Item { oppositeDirection: BarService.getPillDirection(root) icon: _dynamicIcon !== "" ? _dynamicIcon : customIcon - text: _dynamicText + text: shouldShowText ? _dynamicText : "" density: Settings.data.bar.density + rotateText: isVerticalBar && !hideTextInVerticalBar autoHide: false forceOpen: _dynamicText !== "" tooltipText: { - if (!hasExec) { - return "Custom button, configure in settings." - } else { - var lines = [] + var tooltipLines = [] + + if (hasExec) { if (leftClickExec !== "") { - lines.push(`Left click: ${leftClickExec}.`) + tooltipLines.push(`Left click: ${leftClickExec}.`) } if (rightClickExec !== "") { - lines.push(`Right click: ${rightClickExec}.`) + tooltipLines.push(`Right click: ${rightClickExec}.`) } if (middleClickExec !== "") { - lines.push(`Middle click: ${middleClickExec}.`) + tooltipLines.push(`Middle click: ${middleClickExec}.`) } - return lines.join("\n") + } + + if (_dynamicTooltip !== "") { + if (tooltipLines.length > 0) { + tooltipLines.push("") + } + tooltipLines.push(_dynamicTooltip) + } + + if (tooltipLines.length === 0) { + return "Custom button, configure in settings." + } else { + return tooltipLines.join("\n") } } @@ -80,13 +96,14 @@ Item { // Internal state for dynamic text property string _dynamicText: "" property string _dynamicIcon: "" + property string _dynamicTooltip: "" // Periodically run the text command (if set) Timer { id: refreshTimer interval: Math.max(250, textIntervalMs) repeat: true - running: !textStream && textCommand && textCommand.length > 0 + running: shouldShowText && !textStream && textCommand && textCommand.length > 0 triggeredOnStart: true onTriggered: root.runTextCommand() } @@ -95,7 +112,7 @@ Item { Timer { id: restartTimer interval: 1000 - running: textStream && !textProc.running + running: shouldShowText && textStream && !textProc.running onTriggered: root.runTextCommand() } @@ -123,38 +140,49 @@ Item { function parseDynamicContent(content) { var contentStr = String(content || "").trim() - if (contentStr.indexOf("\n") !== -1) { - contentStr = contentStr.split("\n")[0] - } if (parseJson) { + var lineToParse = contentStr + + if (!textStream && contentStr.includes('\n')) { + const lines = contentStr.split('\n').filter(line => line.trim() !== '') + if (lines.length > 0) { + lineToParse = lines[lines.length - 1] + } + } + try { - var parsed = JSON.parse(contentStr) - var text = parsed.text || "" + const parsed = JSON.parse(lineToParse) + const text = parsed.text || "" + const icon = parsed.icon || "" + const tooltip = parsed.tooltip || "" if (checkCollapse(text)) { _dynamicText = "" _dynamicIcon = "" + _dynamicTooltip = "" return } _dynamicText = text - _dynamicIcon = parsed.icon || "" + _dynamicIcon = icon + _dynamicTooltip = tooltip return } catch (e) { - - // Not a valid JSON, treat as plain text + Logger.w("CustomButton", `Failed to parse JSON. Content: "${lineToParse}"`) } } if (checkCollapse(contentStr)) { _dynamicText = "" _dynamicIcon = "" + _dynamicTooltip = "" return } _dynamicText = contentStr _dynamicIcon = "" + _dynamicTooltip = "" } function checkCollapse(text) { diff --git a/Modules/Bar/Widgets/DarkMode.qml b/Modules/Bar/Widgets/DarkMode.qml index d9a2cb25..170b3f5f 100644 --- a/Modules/Bar/Widgets/DarkMode.qml +++ b/Modules/Bar/Widgets/DarkMode.qml @@ -1,7 +1,7 @@ import Quickshell import qs.Commons import qs.Widgets -import qs.Services +import qs.Services.UI NIconButton { id: root diff --git a/Modules/Bar/Widgets/KeepAwake.qml b/Modules/Bar/Widgets/KeepAwake.qml index c0546884..46f975d6 100644 --- a/Modules/Bar/Widgets/KeepAwake.qml +++ b/Modules/Bar/Widgets/KeepAwake.qml @@ -2,7 +2,8 @@ import QtQuick import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.Power +import qs.Services.UI import qs.Widgets NIconButton { diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml index 41ffd027..e00fd2d6 100644 --- a/Modules/Bar/Widgets/KeyboardLayout.qml +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -4,7 +4,8 @@ import Quickshell import Quickshell.Wayland import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.UI +import qs.Services.Keyboard import qs.Widgets import qs.Modules.Bar.Extras diff --git a/Modules/Bar/Widgets/LockKeys.qml b/Modules/Bar/Widgets/LockKeys.qml index 3fd5b8fb..a13dbd9b 100644 --- a/Modules/Bar/Widgets/LockKeys.qml +++ b/Modules/Bar/Widgets/LockKeys.qml @@ -4,7 +4,8 @@ import Quickshell import Quickshell.Io import qs.Commons import qs.Modules.Panels.Settings -import qs.Services +import qs.Services.Keyboard +import qs.Services.UI import qs.Widgets //import qs.Modules.Bar.Extras diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index 9d626284..0867514d 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -4,7 +4,8 @@ import QtQuick.Layouts import Quickshell import qs.Widgets.AudioSpectrum import qs.Commons -import qs.Services +import qs.Services.Media +import qs.Services.UI import qs.Widgets Item { diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index 23946dfb..d384cfb5 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -3,10 +3,11 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire import qs.Commons -import qs.Modules.Panels.Settings -import qs.Services -import qs.Widgets import qs.Modules.Bar.Extras +import qs.Modules.Panels.Settings +import qs.Services.Media +import qs.Services.UI +import qs.Widgets Item { id: root diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 5524a5bd..c04d713d 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -5,7 +5,8 @@ import Quickshell import Quickshell.Wayland import qs.Commons import qs.Modules.Panels.Settings -import qs.Services +import qs.Services.System +import qs.Services.UI import qs.Widgets NIconButton { diff --git a/Modules/Bar/Widgets/NoctaliaPerformance.qml b/Modules/Bar/Widgets/NoctaliaPerformance.qml new file mode 100644 index 00000000..4263604a --- /dev/null +++ b/Modules/Bar/Widgets/NoctaliaPerformance.qml @@ -0,0 +1,31 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Modules.Panels.Settings +import qs.Services.Power +import qs.Services.UI +import qs.Widgets + +NIconButton { + id: root + + property ShellScreen screen + + density: Settings.data.bar.density + baseSize: Style.capsuleHeight + applyUiScale: false + colorBg: PowerProfileService.noctaliaPerformanceMode ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) + colorFg: PowerProfileService.noctaliaPerformanceMode ? Color.mOnPrimary : Color.mOnSurface + colorBorder: Color.transparent + colorBorderHover: Color.transparent + + icon: PowerProfileService.noctaliaPerformanceMode ? "rocket" : "rocket-off" + tooltipText: PowerProfileService.noctaliaPerformanceMode ? I18n.tr("tooltips.noctalia-performance-enabled") : I18n.tr("tooltips.noctalia-performance-disabled") + tooltipDirection: BarService.getTooltipDirection() + onClicked: { + PowerProfileService.toggleNoctaliaPerformance() + } +} diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 55f41d9b..1dd21717 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -4,7 +4,8 @@ import QtQuick.Controls import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Services +import qs.Services.UI +import qs.Services.System import qs.Widgets NIconButton { @@ -47,8 +48,8 @@ NIconButton { baseSize: Style.capsuleHeight applyUiScale: false density: Settings.data.bar.density - icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell" - tooltipText: Settings.data.notifications.doNotDisturb ? I18n.tr("tooltips.open-notification-history-disable-dnd") : I18n.tr("tooltips.open-notification-history-enable-dnd") + icon: NotificationService.doNotDisturb ? "bell-off" : "bell" + tooltipText: NotificationService.doNotDisturb ? I18n.tr("tooltips.open-notification-history-disable-dnd") : I18n.tr("tooltips.open-notification-history-enable-dnd") tooltipDirection: BarService.getTooltipDirection() colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) colorFg: Color.mOnSurface @@ -60,7 +61,7 @@ NIconButton { panel?.toggle(this) } - onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb + onRightClicked: NotificationService.doNotDisturb = !NotificationService.doNotDisturb Loader { anchors.right: parent.right diff --git a/Modules/Bar/Widgets/PowerProfile.qml b/Modules/Bar/Widgets/PowerProfile.qml index da96fcf1..724d7a4d 100644 --- a/Modules/Bar/Widgets/PowerProfile.qml +++ b/Modules/Bar/Widgets/PowerProfile.qml @@ -3,7 +3,8 @@ import QtQuick.Layouts import Quickshell import Quickshell.Services.UPower import qs.Commons -import qs.Services +import qs.Services.Power +import qs.Services.UI import qs.Widgets NIconButton { diff --git a/Modules/Bar/Widgets/ScreenRecorder.qml b/Modules/Bar/Widgets/ScreenRecorder.qml index 4e528c2f..085db1e3 100644 --- a/Modules/Bar/Widgets/ScreenRecorder.qml +++ b/Modules/Bar/Widgets/ScreenRecorder.qml @@ -1,6 +1,8 @@ import Quickshell import qs.Commons -import qs.Services +import qs.Services.UI +import qs.Services.Media +import qs.Services.System import qs.Widgets // Screen Recording Indicator @@ -22,7 +24,7 @@ NIconButton { function handleClick() { if (!ScreenRecorderService.isAvailable) { - ToastService.showError(I18n.tr("toast.recording.not-installed"), I18n.tr("toast.recording.not-installed-desc"), 7000) + ToastService.showError(I18n.tr("toast.recording.not-installed"), I18n.tr("toast.recording.not-installed-desc")) return } ScreenRecorderService.toggleRecording() diff --git a/Modules/Bar/Widgets/SessionMenu.qml b/Modules/Bar/Widgets/SessionMenu.qml index d1b79998..64192d32 100644 --- a/Modules/Bar/Widgets/SessionMenu.qml +++ b/Modules/Bar/Widgets/SessionMenu.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.UI import qs.Widgets NIconButton { diff --git a/Modules/Bar/Widgets/Spacer.qml b/Modules/Bar/Widgets/Spacer.qml index 3af0e792..92bc33f3 100644 --- a/Modules/Bar/Widgets/Spacer.qml +++ b/Modules/Bar/Widgets/Spacer.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.UI import qs.Widgets Item { diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index 0adec65d..caab8fda 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -3,7 +3,8 @@ import QtQuick.Layouts import Quickshell import qs.Commons import qs.Modules.Panels.Settings -import qs.Services +import qs.Services.UI +import qs.Services.System import qs.Widgets Rectangle { diff --git a/Modules/Bar/Widgets/Taskbar.qml b/Modules/Bar/Widgets/Taskbar.qml index 959dc2df..30b41e72 100644 --- a/Modules/Bar/Widgets/Taskbar.qml +++ b/Modules/Bar/Widgets/Taskbar.qml @@ -5,7 +5,8 @@ import Quickshell import Quickshell.Widgets import Quickshell.Wayland import qs.Commons -import qs.Services +import qs.Services.Compositor +import qs.Services.UI import qs.Widgets Rectangle { diff --git a/Modules/Bar/Widgets/TaskbarGrouped.qml b/Modules/Bar/Widgets/TaskbarGrouped.qml index 73895274..ccf0ef41 100644 --- a/Modules/Bar/Widgets/TaskbarGrouped.qml +++ b/Modules/Bar/Widgets/TaskbarGrouped.qml @@ -5,7 +5,8 @@ import Quickshell import Quickshell.Wayland import Quickshell.Widgets import qs.Commons -import qs.Services +import qs.Services.Compositor +import qs.Services.UI import qs.Widgets Item { diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 11347224..7f1a3921 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -7,7 +7,7 @@ import Quickshell.Services.SystemTray import Quickshell.Widgets import qs.Commons import qs.Modules.Bar.Extras -import qs.Services +import qs.Services.UI import qs.Widgets Rectangle { @@ -198,6 +198,36 @@ Rectangle { spacing: Style.marginXS flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight + // Drawer opener (before items if opposite direction) + NIconButton { + id: chevronIconBefore + visible: root.drawerEnabled && dropdownItems.length > 0 && BarService.getPillDirection(root) + tooltipText: I18n.tr("tooltips.open-tray-dropdown") + tooltipDirection: BarService.getTooltipDirection() + density: Settings.data.bar.density + baseSize: Style.capsuleHeight + applyUiScale: false + colorBg: Settings.data.colorSchemes.darkMode ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary + colorFg: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mOnPrimary + colorBorder: Color.transparent + colorBorderHover: Color.transparent + icon: { + switch (barPosition) { + case "bottom": + return "caret-up" + case "left": + return "caret-right" + case "right": + return "caret-left" + case "top": + default: + return "caret-down" + } + } + onClicked: toggleDrawer(this) + onRightClicked: toggleDrawer(this) + } + Repeater { id: repeater model: root.filteredItems @@ -311,10 +341,10 @@ Rectangle { } } - // Drawer opener + // Drawer opener (after items if normal direction) NIconButton { - id: chevronIcon - visible: root.drawerEnabled && dropdownItems.length > 0 + id: chevronIconAfter + visible: root.drawerEnabled && dropdownItems.length > 0 && !BarService.getPillDirection(root) tooltipText: I18n.tr("tooltips.open-tray-dropdown") tooltipDirection: BarService.getTooltipDirection() density: Settings.data.bar.density @@ -337,12 +367,8 @@ Rectangle { return "caret-down" } } - onClicked: { - toggleDrawer(this) - } - onRightClicked: { - toggleDrawer(this) - } + onClicked: toggleDrawer(this) + onRightClicked: toggleDrawer(this) } } } diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 5a178e37..9fb72180 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -3,10 +3,11 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire import qs.Commons -import qs.Modules.Panels.Settings -import qs.Services -import qs.Widgets import qs.Modules.Bar.Extras +import qs.Modules.Panels.Settings +import qs.Services.UI +import qs.Services.Media +import qs.Widgets Item { id: root diff --git a/Modules/Bar/Widgets/WallpaperSelector.qml b/Modules/Bar/Widgets/WallpaperSelector.qml index 322b54c0..1dbbdff4 100644 --- a/Modules/Bar/Widgets/WallpaperSelector.qml +++ b/Modules/Bar/Widgets/WallpaperSelector.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.UI import qs.Widgets NIconButton { diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 77000e83..ad2d1ede 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -1,7 +1,8 @@ import QtQuick import Quickshell import qs.Commons -import qs.Services +import qs.Services.Networking +import qs.Services.UI import qs.Modules.Bar.Extras Item { diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 0ceec88e..f30af56f 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -6,7 +6,8 @@ import QtQuick.Effects import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.Compositor +import qs.Services.UI import qs.Widgets Item { diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 78ba4c38..fc6c1614 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -6,7 +6,7 @@ import Quickshell import Quickshell.Wayland import Quickshell.Widgets import qs.Commons -import qs.Services +import qs.Services.UI import qs.Widgets Loader { @@ -303,6 +303,10 @@ Loader { border.width: Style.borderS border.color: Qt.alpha(Color.mOutline, Settings.data.dock.backgroundOpacity) + // Enable layer caching to reduce GPU usage from continuous animations + // (pulse animations on active indicators run infinitely) + layer.enabled: true + MouseArea { id: dockMouseArea anchors.fill: parent @@ -562,22 +566,6 @@ Loader { radius: Style.radiusXS anchors.top: parent.bottom anchors.horizontalCenter: parent.horizontalCenter - - // Pulse animation for active indicator - SequentialAnimation on opacity { - running: isActive - loops: Animation.Infinite - NumberAnimation { - to: 0.6 - duration: Style.animationSlowest - easing.type: Easing.InOutQuad - } - NumberAnimation { - to: 1.0 - duration: Style.animationSlowest - easing.type: Easing.InOutQuad - } - } } } } diff --git a/Modules/Dock/DockMenu.qml b/Modules/Dock/DockMenu.qml index 17fcbd29..ec2cb15f 100644 --- a/Modules/Dock/DockMenu.qml +++ b/Modules/Dock/DockMenu.qml @@ -5,7 +5,6 @@ import Quickshell import Quickshell.Wayland import Quickshell.Widgets import qs.Commons -import qs.Services import qs.Widgets PopupWindow { diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index f329afe1..ceb832f5 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -9,7 +9,11 @@ import Quickshell.Services.UPower import Quickshell.Io import Quickshell.Widgets import qs.Commons -import qs.Services +import qs.Services.Hardware +import qs.Services.Location +import qs.Services.Media +import qs.Services.Compositor +import qs.Services.UI import qs.Widgets import qs.Widgets.AudioSpectrum diff --git a/Modules/MainScreen/AllScreens.qml b/Modules/MainScreen/AllScreens.qml index 5204404c..3211a332 100644 --- a/Modules/MainScreen/AllScreens.qml +++ b/Modules/MainScreen/AllScreens.qml @@ -3,7 +3,7 @@ import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Services +import qs.Services.UI import qs.Modules.MainScreen // ------------------------------ @@ -15,8 +15,10 @@ Variants { required property ShellScreen modelData property bool shouldBeActive: { - if (!modelData || !modelData.name) - Logger.d("Shell", "MainScreen activated for", modelData?.name) + if (!modelData || !modelData.name) { + return false + } + Logger.d("Shell", "MainScreen activated for", modelData?.name) return true } @@ -39,6 +41,27 @@ Variants { } } + // Bar content in separate windows to prevent fullscreen redraws + Loader { + active: { + if (!parent.windowLoaded || !parent.shouldBeActive || !BarService.isVisible) + return false + + // Check if bar is configured for this screen + var monitors = Settings.data.bar.monitors || [] + return monitors.length === 0 || monitors.includes(modelData?.name) + } + asynchronous: false + + sourceComponent: BarContentWindow { + screen: modelData + } + + onLoaded: { + Logger.d("Shell", "BarContentWindow created for", modelData?.name) + } + } + // BarExclusionZone - created after MainScreen has fully loaded // Disabled when bar is hidden or not configured for this screen Loader { diff --git a/Modules/MainScreen/Backgrounds/AllBackgrounds.qml b/Modules/MainScreen/Backgrounds/AllBackgrounds.qml index 75487bc2..acb6c53a 100644 --- a/Modules/MainScreen/Backgrounds/AllBackgrounds.qml +++ b/Modules/MainScreen/Backgrounds/AllBackgrounds.qml @@ -25,125 +25,149 @@ Item { anchors.fill: parent - // The unified Shape container - Shape { - id: backgroundsShape + // Wrapper with layer caching for better shadow performance + Item { anchors.fill: parent - // Use curve renderer for smooth corners - preferredRendererType: Shape.CurveRenderer + // Enable layer caching to prevent continuous re-rendering + // This caches the Shape to a GPU texture, reducing GPU tessellation overhead + layer.enabled: true - // CRITICAL: Shape must not block mouse input! - // ShapePaths inside will render, but the Shape container itself should be transparent to input - enabled: false // Disable mouse input on the Shape itself + // The unified Shape container + Shape { + id: backgroundsShape + anchors.fill: parent - Component.onCompleted: { - Logger.d("AllBackgrounds", "AllBackgrounds initialized") - Logger.d("AllBackgrounds", " bar:", root.bar) - Logger.d("AllBackgrounds", " windowRoot:", root.windowRoot) - } + // Use curve renderer for smooth corners (GPU-accelerated) + preferredRendererType: Shape.CurveRenderer + + enabled: false // Disable mouse input on the Shape itself + + Component.onCompleted: { + Logger.d("AllBackgrounds", "AllBackgrounds initialized") + Logger.d("AllBackgrounds", " bar:", root.bar) + Logger.d("AllBackgrounds", " windowRoot:", root.windowRoot) + } - /** + /** * Bar */ - BarBackground { - bar: root.bar - shapeContainer: backgroundsShape - } + BarBackground { + bar: root.bar + shapeContainer: backgroundsShape + windowRoot: root.windowRoot + backgroundColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) + } - /** + /** * Panels */ - // Audio - PanelBackground { - panel: root.windowRoot.audioPanel - shapeContainer: backgroundsShape + // Audio + PanelBackground { + panel: root.windowRoot.audioPanel + shapeContainer: backgroundsShape + backgroundColor: Color.mSurface + } + + // Battery + PanelBackground { + panel: root.windowRoot.batteryPanel + shapeContainer: backgroundsShape + backgroundColor: Color.mSurface + } + + // Bluetooth + PanelBackground { + panel: root.windowRoot.bluetoothPanel + shapeContainer: backgroundsShape + backgroundColor: Color.mSurface + } + + // Calendar + PanelBackground { + panel: root.windowRoot.calendarPanel + shapeContainer: backgroundsShape + backgroundColor: Color.mSurface + } + + // Control Center + PanelBackground { + panel: root.windowRoot.controlCenterPanel + shapeContainer: backgroundsShape + backgroundColor: Color.mSurface + } + + // Launcher + PanelBackground { + panel: root.windowRoot.launcherPanel + shapeContainer: backgroundsShape + backgroundColor: Qt.alpha(Color.mSurface, Settings.data.appLauncher.backgroundOpacity) + } + + // Notification History + PanelBackground { + panel: root.windowRoot.notificationHistoryPanel + shapeContainer: backgroundsShape + backgroundColor: Color.mSurface + } + + // Session Menu + PanelBackground { + panel: root.windowRoot.sessionMenuPanel + shapeContainer: backgroundsShape + backgroundColor: Color.mSurface + } + + // Settings + PanelBackground { + panel: root.windowRoot.settingsPanel + shapeContainer: backgroundsShape + backgroundColor: Color.mSurface + } + + // Setup Wizard + PanelBackground { + panel: root.windowRoot.setupWizardPanel + shapeContainer: backgroundsShape + backgroundColor: Color.mSurface + } + + // TrayDrawer + PanelBackground { + panel: root.windowRoot.trayDrawerPanel + shapeContainer: backgroundsShape + backgroundColor: Color.mSurface + } + + // TrayMenu + PanelBackground { + panel: root.windowRoot.trayMenuPanel + shapeContainer: backgroundsShape + backgroundColor: Color.mSurface + } + + // Wallpaper + PanelBackground { + panel: root.windowRoot.wallpaperPanel + shapeContainer: backgroundsShape + backgroundColor: Color.mSurface + } + + // WiFi + PanelBackground { + panel: root.windowRoot.wifiPanel + shapeContainer: backgroundsShape + backgroundColor: Color.mSurface + } } - // Battery - PanelBackground { - panel: root.windowRoot.batteryPanel - shapeContainer: backgroundsShape + // Apply shadow to the cached layer + NDropShadows { + anchors.fill: parent + source: backgroundsShape } - - // Bluetooth - PanelBackground { - panel: root.windowRoot.bluetoothPanel - shapeContainer: backgroundsShape - } - - // Calendar - PanelBackground { - panel: root.windowRoot.calendarPanel - shapeContainer: backgroundsShape - } - - // Control Center - PanelBackground { - panel: root.windowRoot.controlCenterPanel - shapeContainer: backgroundsShape - } - - // Launcher - PanelBackground { - panel: root.windowRoot.launcherPanel - shapeContainer: backgroundsShape - } - - // Notification History - PanelBackground { - panel: root.windowRoot.notificationHistoryPanel - shapeContainer: backgroundsShape - } - - // Session Menu - PanelBackground { - panel: root.windowRoot.sessionMenuPanel - shapeContainer: backgroundsShape - } - - // Settings - PanelBackground { - panel: root.windowRoot.settingsPanel - shapeContainer: backgroundsShape - } - - // Setup Wizard - PanelBackground { - panel: root.windowRoot.setupWizardPanel - shapeContainer: backgroundsShape - } - - // TrayDrawer - PanelBackground { - panel: root.windowRoot.trayDrawerPanel - shapeContainer: backgroundsShape - } - - // TrayMenu - PanelBackground { - panel: root.windowRoot.trayMenuPanel - shapeContainer: backgroundsShape - } - - // Wallpaper - PanelBackground { - panel: root.windowRoot.wallpaperPanel - shapeContainer: backgroundsShape - } - - // WiFi - PanelBackground { - panel: root.windowRoot.wifiPanel - shapeContainer: backgroundsShape - } - } - - NDropShadows { - anchors.fill: parent - source: backgroundsShape } } diff --git a/Modules/MainScreen/Backgrounds/BarBackground.qml b/Modules/MainScreen/Backgrounds/BarBackground.qml index 9399ab12..11fd0404 100644 --- a/Modules/MainScreen/Backgrounds/BarBackground.qml +++ b/Modules/MainScreen/Backgrounds/BarBackground.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Shapes import qs.Commons +import qs.Services.UI import qs.Modules.MainScreen.Backgrounds @@ -25,6 +26,26 @@ ShapePath { // Required reference to AllBackgrounds shapeContainer required property var shapeContainer + // Required reference to windowRoot for screen access + required property var windowRoot + + required property color backgroundColor + + // Check if bar should be visible on this screen + readonly property bool shouldShow: { + // Check global bar visibility + if (!BarService.isVisible) + return false + + // Check screen-specific configuration + var monitors = Settings.data.bar.monitors || [] + var screenName = windowRoot?.screen?.name || "" + + // If no monitors specified, show on all screens + // If monitors specified, only show if this screen is in the list + return monitors.length === 0 || monitors.includes(screenName) + } + // Corner radius (from Style) readonly property real radius: Style.radiusL @@ -32,9 +53,13 @@ ShapePath { // we can use bar.x and bar.y directly (they're already in screen coordinates) readonly property point barMappedPos: bar ? Qt.point(bar.x, bar.y) : Qt.point(0, 0) + // Effective dimensions - 0 when bar shouldn't show (similar to panel behavior) + readonly property real barWidth: (bar && shouldShow) ? bar.width : 0 + readonly property real barHeight: (bar && shouldShow) ? bar.height : 0 + // Flatten corners if bar is too small (handle null bar) - readonly property bool shouldFlatten: bar ? ShapeCornerHelper.shouldFlatten(bar.width, bar.height, radius) : false - readonly property real effectiveRadius: shouldFlatten ? (bar ? ShapeCornerHelper.getFlattenedRadius(Math.min(bar.width, bar.height), radius) : 0) : radius + readonly property bool shouldFlatten: bar ? ShapeCornerHelper.shouldFlatten(barWidth, barHeight, radius) : false + readonly property real effectiveRadius: shouldFlatten ? (bar ? ShapeCornerHelper.getFlattenedRadius(Math.min(barWidth, barHeight), radius) : 0) : radius // Helper function for getting corner radius based on state function getCornerRadius(cornerState) { @@ -64,7 +89,7 @@ ShapePath { // ShapePath configuration strokeWidth: -1 // No stroke, fill only - fillColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) + fillColor: backgroundColor // Starting position (top-left corner, after the arc) // Use mapped coordinates relative to the Shape container @@ -84,7 +109,7 @@ ShapePath { // Top edge (moving right) PathLine { - relativeX: (bar ? bar.width : 0) - root.tlRadius * root.tlMultX - root.trRadius * root.trMultX + relativeX: root.barWidth - root.tlRadius * root.tlMultX - root.trRadius * root.trMultX relativeY: 0 } @@ -100,7 +125,7 @@ ShapePath { // Right edge (moving down) PathLine { relativeX: 0 - relativeY: (bar ? bar.height : 0) - root.trRadius * root.trMultY - root.brRadius * root.brMultY + relativeY: root.barHeight - root.trRadius * root.trMultY - root.brRadius * root.brMultY } // Bottom-right corner arc @@ -114,7 +139,7 @@ ShapePath { // Bottom edge (moving left) PathLine { - relativeX: -((bar ? bar.width : 0) - root.brRadius * root.brMultX - root.blRadius * root.blMultX) + relativeX: -(root.barWidth - root.brRadius * root.brMultX - root.blRadius * root.blMultX) relativeY: 0 } @@ -130,7 +155,7 @@ ShapePath { // Left edge (moving up) - closes the path back to start PathLine { relativeX: 0 - relativeY: -((bar ? bar.height : 0) - root.blRadius * root.blMultY - root.tlRadius * root.tlMultY) + relativeY: -(root.barHeight - root.blRadius * root.blMultY - root.tlRadius * root.tlMultY) } // Top-left corner arc (back to start) diff --git a/Modules/MainScreen/Backgrounds/PanelBackground.qml b/Modules/MainScreen/Backgrounds/PanelBackground.qml index e3858823..e91bface 100644 --- a/Modules/MainScreen/Backgrounds/PanelBackground.qml +++ b/Modules/MainScreen/Backgrounds/PanelBackground.qml @@ -28,6 +28,8 @@ ShapePath { // Corner radius (from Style) readonly property real radius: Style.radiusL + required property color backgroundColor + // Get the actual panelBackground Item from SmartPanel // Only access panelRegion if panel exists and is visible readonly property var panelBg: (panel && panel.visible) ? panel.panelRegion : null @@ -68,21 +70,6 @@ ShapePath { readonly property real blMultY: panelBg ? ShapeCornerHelper.getMultY(panelBg.bottomLeftCornerState) : 1 readonly property real blRadius: panelBg ? getCornerRadius(panelBg.bottomLeftCornerState) : 0 - // DEBUG: Log panel state changes - onPanelChanged: { - Logger.d("PanelBackground", "=== panel changed:", panel) - Logger.d("PanelBackground", " panel.visible:", panel ? panel.visible : "null") - Logger.d("PanelBackground", " panel.panelRegion:", panel ? panel.panelRegion : "null") - } - - onPanelBgChanged: { - Logger.d("PanelBackground", "=== panelBg changed:", panelBg) - if (panelBg) { - Logger.d("PanelBackground", " Geometry:", panelX, panelY, panelWidth, panelHeight) - Logger.d("PanelBackground", " startX:", startX, "startY:", startY) - } - } - // ShapePath configuration strokeWidth: -1 // No stroke, fill only @@ -90,14 +77,7 @@ ShapePath { startX: panelX + tlRadius * tlMultX startY: panelY - fillColor: Color.mSurface - - // Smooth color animation - Behavior on fillColor { - ColorAnimation { - duration: Style.animationFast - } - } + fillColor: backgroundColor // ========== PATH DEFINITION ========== // Draws a rectangle with potentially inverted corners diff --git a/Modules/MainScreen/BarContentWindow.qml b/Modules/MainScreen/BarContentWindow.qml new file mode 100644 index 00000000..66958b0e --- /dev/null +++ b/Modules/MainScreen/BarContentWindow.qml @@ -0,0 +1,65 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services.UI +import qs.Modules.Bar + + +/** + * BarContentWindow - Separate transparent PanelWindow for bar content + * + * This window contains only the bar widgets (content), while the background + * is rendered in MainScreen's unified Shape system. This separation prevents + * fullscreen redraws when bar widgets redraw. + * + * This component should be instantiated once per screen by AllScreens.qml + */ +PanelWindow { + id: barWindow + + // Note: screen property is inherited from PanelWindow and should be set by parent + color: Color.transparent // Transparent - background is in MainScreen below + + Component.onCompleted: { + Logger.d("BarContentWindow", "Bar content window created for screen:", barWindow.screen?.name) + } + + // Wayland layer configuration + WlrLayershell.namespace: "noctalia-bar-content-" + (barWindow.screen?.name || "unknown") + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.exclusionMode: ExclusionMode.Ignore // Don't reserve space - BarExclusionZone in MainScreen handles that + + // Position and size to match bar location + readonly property string barPosition: Settings.data.bar.position || "top" + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property bool barFloating: Settings.data.bar.floating || false + readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 + + // Anchor to the bar's edge + anchors { + top: barPosition === "top" || barIsVertical + bottom: barPosition === "bottom" || barIsVertical + left: barPosition === "left" || !barIsVertical + right: barPosition === "right" || !barIsVertical + } + + // Handle floating margins + margins { + top: barPosition === "top" || barIsVertical ? barMarginV : 0 + bottom: barPosition === "bottom" || barIsVertical ? barMarginV : 0 + left: barPosition === "left" || !barIsVertical ? barMarginH : 0 + right: barPosition === "right" || !barIsVertical ? barMarginH : 0 + } + + // Set a tight window size + implicitWidth: barIsVertical ? Style.barHeight : barWindow.screen.width + implicitHeight: barIsVertical ? barWindow.screen.height : Style.barHeight + + // Bar content - just the widgets, no background + Bar { + anchors.fill: parent + screen: barWindow.screen + } +} diff --git a/Modules/MainScreen/MainScreen.qml b/Modules/MainScreen/MainScreen.qml index 5a59cbfc..6586c59c 100644 --- a/Modules/MainScreen/MainScreen.qml +++ b/Modules/MainScreen/MainScreen.qml @@ -3,7 +3,7 @@ import QtQuick.Effects import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Services +import qs.Services.UI import "Backgrounds" as Backgrounds // All panels @@ -49,17 +49,6 @@ PanelWindow { Logger.d("MainScreen", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y) } - // Debug: Log mask region changes - onMaskChanged: { - Logger.d("MainScreen", "Mask changed!") - Logger.d("MainScreen", " Bar region:", barLoader.item?.barRegion) - Logger.d("MainScreen", " Panel count:", panelsRepeater.count) - for (var i = 0; i < panelsRepeater.count; i++) { - var panelItem = panelsRepeater.itemAt(i)?.item - Logger.d("MainScreen", " Panel", i, "- open:", panelItem?.isPanelOpen, "- region:", panelItem?.panelRegion) - } - } - // Wayland // Always use Exclusive keyboard focus when a panel is open // This ensures all keyboard shortcuts work reliably (Escape, etc.) @@ -112,12 +101,11 @@ PanelWindow { // Bar region - subtract bar area from mask Region { id: barMaskRegion - property var barRegion: barLoader.item?.barRegion - x: barRegion?.x ?? 0 - y: barRegion?.y ?? 0 - width: barRegion?.width ?? 0 - height: barRegion?.height ?? 0 + x: barPlaceholder.x + y: barPlaceholder.y + width: barPlaceholder.width + height: barPlaceholder.height intersection: Intersection.Subtract } @@ -144,7 +132,7 @@ PanelWindow { Backgrounds.AllBackgrounds { id: unifiedBackgrounds anchors.fill: parent - bar: barLoader.item?.barItem || null + bar: barPlaceholder.barItem || null windowRoot: root z: 0 // Behind all content } @@ -174,7 +162,6 @@ PanelWindow { Component.onCompleted: { objectName = "audioPanel-" + (screen?.name || "unknown") PanelService.registerPanel(audioPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } @@ -186,7 +173,6 @@ PanelWindow { Component.onCompleted: { objectName = "batteryPanel-" + (screen?.name || "unknown") PanelService.registerPanel(batteryPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } @@ -198,7 +184,6 @@ PanelWindow { Component.onCompleted: { objectName = "bluetoothPanel-" + (screen?.name || "unknown") PanelService.registerPanel(bluetoothPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } @@ -210,7 +195,6 @@ PanelWindow { Component.onCompleted: { objectName = "controlCenterPanel-" + (screen?.name || "unknown") PanelService.registerPanel(controlCenterPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } @@ -222,7 +206,6 @@ PanelWindow { Component.onCompleted: { objectName = "calendarPanel-" + (screen?.name || "unknown") PanelService.registerPanel(calendarPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } @@ -234,7 +217,6 @@ PanelWindow { Component.onCompleted: { objectName = "launcherPanel-" + (screen?.name || "unknown") PanelService.registerPanel(launcherPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } @@ -246,7 +228,6 @@ PanelWindow { Component.onCompleted: { objectName = "notificationHistoryPanel-" + (screen?.name || "unknown") PanelService.registerPanel(notificationHistoryPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } @@ -258,7 +239,6 @@ PanelWindow { Component.onCompleted: { objectName = "sessionMenuPanel-" + (screen?.name || "unknown") PanelService.registerPanel(sessionMenuPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } @@ -270,7 +250,6 @@ PanelWindow { Component.onCompleted: { objectName = "settingsPanel-" + (screen?.name || "unknown") PanelService.registerPanel(settingsPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } @@ -282,7 +261,6 @@ PanelWindow { Component.onCompleted: { objectName = "setupWizardPanel-" + (screen?.name || "unknown") PanelService.registerPanel(setupWizardPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } @@ -294,7 +272,6 @@ PanelWindow { Component.onCompleted: { objectName = "trayDrawerPanel-" + (screen?.name || "unknown") PanelService.registerPanel(trayDrawerPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } @@ -306,7 +283,6 @@ PanelWindow { Component.onCompleted: { objectName = "trayMenuPanel-" + (screen?.name || "unknown") PanelService.registerPanel(trayMenuPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } @@ -318,7 +294,6 @@ PanelWindow { Component.onCompleted: { objectName = "wallpaperPanel-" + (screen?.name || "unknown") PanelService.registerPanel(wallpaperPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } @@ -330,33 +305,111 @@ PanelWindow { Component.onCompleted: { objectName = "wifiPanel-" + (screen?.name || "unknown") PanelService.registerPanel(wifiPanel) - Logger.d("MainScreen", "Panel registered:", objectName, "on screen:", screen?.name) } } - // Bar (always on top - rendered last in tree, so naturally on top) - Loader { - id: barLoader - asynchronous: false - sourceComponent: Bar {} - // Keep bar loaded but hide it when BarService.isVisible is false - // This allows panels to remain accessible via IPC - visible: BarService.isVisible + // Bar placeholder - just for background positioning (actual bar content is in BarContentWindow) + Item { + id: barPlaceholder - // Fill parent to provide dimensions for Bar to reference - anchors.fill: parent + // Expose self as barItem for AllBackgrounds compatibility + readonly property var barItem: barPlaceholder + // Screen reference property ShellScreen screen: root.screen - onLoaded: { - Logger.d("MainScreen", "Bar loaded:", item !== null) - if (item) { - Logger.d("MainScreen", "Bar screen", item.screen?.name, "size:", item.width, "x", item.height) - // Bind screen to bar component (use binding for reactivity) - item.screen = Qt.binding(function () { - return barLoader.screen - }) + // Bar positioning properties (match Bar.qml logic) + readonly property string barPosition: Settings.data.bar.position || "top" + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property bool barFloating: Settings.data.bar.floating || false + readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 + + // Expose bar dimensions directly on this Item for BarBackground + // Use screen dimensions directly + x: { + if (barPosition === "right") + return screen.width - Style.barHeight - barMarginH + return barMarginH + } + y: { + if (barPosition === "bottom") + return screen.height - Style.barHeight - barMarginV + return barMarginV + } + width: { + if (barIsVertical) { + return Style.barHeight + 1 } + return screen.width - barMarginH * 2 + } + height: { + if (barIsVertical) { + return screen.height - barMarginV * 2 + } + return Style.barHeight + } + + // Corner states (same as Bar.qml) + readonly property int topLeftCornerState: { + if (barFloating) + return 0 + if (barPosition === "top") + return -1 + if (barPosition === "left") + return -1 + if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")) { + return barIsVertical ? 1 : 2 + } + return -1 + } + + readonly property int topRightCornerState: { + if (barFloating) + return 0 + if (barPosition === "top") + return -1 + if (barPosition === "right") + return -1 + if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")) { + return barIsVertical ? 1 : 2 + } + return -1 + } + + readonly property int bottomLeftCornerState: { + if (barFloating) + return 0 + if (barPosition === "bottom") + return -1 + if (barPosition === "left") + return -1 + if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")) { + return barIsVertical ? 1 : 2 + } + return -1 + } + + readonly property int bottomRightCornerState: { + if (barFloating) + return 0 + if (barPosition === "bottom") + return -1 + if (barPosition === "right") + return -1 + if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")) { + return barIsVertical ? 1 : 2 + } + return -1 + } + + Component.onCompleted: { + Logger.d("MainScreen", "===== Bar placeholder loaded =====") + Logger.d("MainScreen", " Screen:", screen?.name, "Size:", screen?.width, "x", screen?.height) + Logger.d("MainScreen", " Bar position:", barPosition, "| isVertical:", barIsVertical) + Logger.d("MainScreen", " Bar dimensions: x=" + x, "y=" + y, "width=" + width, "height=" + height) + Logger.d("MainScreen", " Style.barHeight =", Style.barHeight) + Logger.d("MainScreen", " Margins: H=" + barMarginH, "V=" + barMarginV, "| Floating:", barFloating) } } @@ -501,6 +554,26 @@ PanelWindow { } } + Shortcut { + sequence: "Ctrl+N" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onCtrlNPressed) { + PanelService.openedPanel.onCtrlNPressed() + } + } + } + + Shortcut { + sequence: "Ctrl+P" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onCtrlPPressed) { + PanelService.openedPanel.onCtrlPPressed() + } + } + } + Shortcut { sequence: "Left" enabled: root.isPanelOpen diff --git a/Modules/MainScreen/ScreenCorners.qml b/Modules/MainScreen/ScreenCorners.qml index 367e6269..89dec642 100644 --- a/Modules/MainScreen/ScreenCorners.qml +++ b/Modules/MainScreen/ScreenCorners.qml @@ -1,7 +1,6 @@ import QtQuick import QtQuick.Shapes import qs.Commons -import qs.Services /** @@ -10,207 +9,221 @@ import qs.Services * Renders concave corners at the screen edges to create a rounded screen effect. * Self-contained Shape component (no shadows). */ -Shape { +Item { id: root anchors.fill: parent - preferredRendererType: Shape.CurveRenderer - enabled: false // Disable mouse input - ShapePath { - id: cornersPath + // Wrapper with layer caching to reduce GPU tessellation overhead + Item { + anchors.fill: parent - // Corner configuration - readonly property color cornerColor: Settings.data.general.forceBlackScreenCorners ? Color.black : Color.mSurface - readonly property real cornerRadius: Style.screenRadius - readonly property real cornerSize: Style.screenRadius + // Cache the Shape to a texture to prevent continuous re-tessellation + layer.enabled: true - // Determine margins based on bar position - readonly property real topMargin: 0 - readonly property real bottomMargin: 0 - readonly property real leftMargin: 0 - readonly property real rightMargin: 0 + Shape { + id: cornersShape - // Screen dimensions - readonly property real screenWidth: root.width - readonly property real screenHeight: root.height + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + enabled: false // Disable mouse input - // Only show screen corners if enabled and appropriate conditions are met - readonly property bool shouldShow: Settings.data.general.showScreenCorners + ShapePath { + id: cornersPath - // ShapePath configuration - strokeWidth: -1 // No stroke, fill only - fillColor: shouldShow ? cornerColor : Color.transparent + // Corner configuration + readonly property color cornerColor: Settings.data.general.forceBlackScreenCorners ? Color.black : Color.mSurface + readonly property real cornerRadius: Style.screenRadius + readonly property real cornerSize: Style.screenRadius - // Smooth color animation - Behavior on fillColor { - ColorAnimation { - duration: Style.animationFast + // Determine margins based on bar position + readonly property real topMargin: 0 + readonly property real bottomMargin: 0 + readonly property real leftMargin: 0 + readonly property real rightMargin: 0 + + // Screen dimensions + readonly property real screenWidth: cornersShape.width + readonly property real screenHeight: cornersShape.height + + // Only show screen corners if enabled and appropriate conditions are met + readonly property bool shouldShow: Settings.data.general.showScreenCorners + + // ShapePath configuration + strokeWidth: -1 // No stroke, fill only + fillColor: shouldShow ? cornerColor : Color.transparent + + // Smooth color animation + Behavior on fillColor { + ColorAnimation { + duration: Style.animationFast + } + } + + // ========== PATH DEFINITION ========== + // Draws 4 separate corner squares at screen edges + // Each corner square has a concave arc on the inner diagonal + + // ========== TOP-LEFT CORNER ========== + // Arc is at the bottom-right of this square (inner diagonal) + // Start at top-left screen corner + startX: leftMargin + startY: topMargin + + // Top edge (moving right) + PathLine { + relativeX: cornersPath.cornerSize + relativeY: 0 + } + + // Right edge (moving down toward arc) + PathLine { + relativeX: 0 + relativeY: cornersPath.cornerSize - cornersPath.cornerRadius + } + + // Concave arc (bottom-right corner of square, curving inward toward screen center) + PathArc { + relativeX: -cornersPath.cornerRadius + relativeY: cornersPath.cornerRadius + radiusX: cornersPath.cornerRadius + radiusY: cornersPath.cornerRadius + direction: PathArc.Counterclockwise + } + + // Bottom edge (moving left) + PathLine { + relativeX: -(cornersPath.cornerSize - cornersPath.cornerRadius) + relativeY: 0 + } + + // Left edge (moving up) - closes back to start + PathLine { + relativeX: 0 + relativeY: -cornersPath.cornerSize + } + + // ========== TOP-RIGHT CORNER ========== + // Arc is at the bottom-left of this square (inner diagonal) + PathMove { + x: cornersPath.screenWidth - cornersPath.rightMargin - cornersPath.cornerSize + y: cornersPath.topMargin + } + + // Top edge (moving right) + PathLine { + relativeX: cornersPath.cornerSize + relativeY: 0 + } + + // Right edge (moving down) + PathLine { + relativeX: 0 + relativeY: cornersPath.cornerSize + } + + // Bottom edge (moving left toward arc) + PathLine { + relativeX: -(cornersPath.cornerSize - cornersPath.cornerRadius) + relativeY: 0 + } + + // Concave arc (bottom-left corner of square, curving inward toward screen center) + PathArc { + relativeX: -cornersPath.cornerRadius + relativeY: -cornersPath.cornerRadius + radiusX: cornersPath.cornerRadius + radiusY: cornersPath.cornerRadius + direction: PathArc.Counterclockwise + } + + // Left edge (moving up) - closes back to start + PathLine { + relativeX: 0 + relativeY: -(cornersPath.cornerSize - cornersPath.cornerRadius) + } + + // ========== BOTTOM-LEFT CORNER ========== + // Arc is at the top-right of this square (inner diagonal) + PathMove { + x: cornersPath.leftMargin + y: cornersPath.screenHeight - cornersPath.bottomMargin - cornersPath.cornerSize + } + + // Top edge (moving right toward arc) + PathLine { + relativeX: cornersPath.cornerSize - cornersPath.cornerRadius + relativeY: 0 + } + + // Concave arc (top-right corner of square, curving inward toward screen center) + PathArc { + relativeX: cornersPath.cornerRadius + relativeY: cornersPath.cornerRadius + radiusX: cornersPath.cornerRadius + radiusY: cornersPath.cornerRadius + direction: PathArc.Counterclockwise + } + + // Right edge (moving down) + PathLine { + relativeX: 0 + relativeY: cornersPath.cornerSize - cornersPath.cornerRadius + } + + // Bottom edge (moving left) + PathLine { + relativeX: -cornersPath.cornerSize + relativeY: 0 + } + + // Left edge (moving up) - closes back to start + PathLine { + relativeX: 0 + relativeY: -cornersPath.cornerSize + } + + // ========== BOTTOM-RIGHT CORNER ========== + // Arc is at the top-left of this square (inner diagonal) + // Start at bottom-right of square (different from other corners!) + PathMove { + x: cornersPath.screenWidth - cornersPath.rightMargin + y: cornersPath.screenHeight - cornersPath.bottomMargin + } + + // Bottom edge (moving left) + PathLine { + relativeX: -cornersPath.cornerSize + relativeY: 0 + } + + // Left edge (moving up toward arc) + PathLine { + relativeX: 0 + relativeY: -(cornersPath.cornerSize - cornersPath.cornerRadius) + } + + // Concave arc (top-left corner of square, curving inward toward screen center) + PathArc { + relativeX: cornersPath.cornerRadius + relativeY: -cornersPath.cornerRadius + radiusX: cornersPath.cornerRadius + radiusY: cornersPath.cornerRadius + direction: PathArc.Counterclockwise + } + + // Top edge (moving right) + PathLine { + relativeX: cornersPath.cornerSize - cornersPath.cornerRadius + relativeY: 0 + } + + // Right edge (moving down) - closes back to start + PathLine { + relativeX: 0 + relativeY: cornersPath.cornerSize + } } } - - // ========== PATH DEFINITION ========== - // Draws 4 separate corner squares at screen edges - // Each corner square has a concave arc on the inner diagonal - - // ========== TOP-LEFT CORNER ========== - // Arc is at the bottom-right of this square (inner diagonal) - // Start at top-left screen corner - startX: leftMargin - startY: topMargin - - // Top edge (moving right) - PathLine { - relativeX: cornersPath.cornerSize - relativeY: 0 - } - - // Right edge (moving down toward arc) - PathLine { - relativeX: 0 - relativeY: cornersPath.cornerSize - cornersPath.cornerRadius - } - - // Concave arc (bottom-right corner of square, curving inward toward screen center) - PathArc { - relativeX: -cornersPath.cornerRadius - relativeY: cornersPath.cornerRadius - radiusX: cornersPath.cornerRadius - radiusY: cornersPath.cornerRadius - direction: PathArc.Counterclockwise - } - - // Bottom edge (moving left) - PathLine { - relativeX: -(cornersPath.cornerSize - cornersPath.cornerRadius) - relativeY: 0 - } - - // Left edge (moving up) - closes back to start - PathLine { - relativeX: 0 - relativeY: -cornersPath.cornerSize - } - - // ========== TOP-RIGHT CORNER ========== - // Arc is at the bottom-left of this square (inner diagonal) - PathMove { - x: cornersPath.screenWidth - cornersPath.rightMargin - cornersPath.cornerSize - y: cornersPath.topMargin - } - - // Top edge (moving right) - PathLine { - relativeX: cornersPath.cornerSize - relativeY: 0 - } - - // Right edge (moving down) - PathLine { - relativeX: 0 - relativeY: cornersPath.cornerSize - } - - // Bottom edge (moving left toward arc) - PathLine { - relativeX: -(cornersPath.cornerSize - cornersPath.cornerRadius) - relativeY: 0 - } - - // Concave arc (bottom-left corner of square, curving inward toward screen center) - PathArc { - relativeX: -cornersPath.cornerRadius - relativeY: -cornersPath.cornerRadius - radiusX: cornersPath.cornerRadius - radiusY: cornersPath.cornerRadius - direction: PathArc.Counterclockwise - } - - // Left edge (moving up) - closes back to start - PathLine { - relativeX: 0 - relativeY: -(cornersPath.cornerSize - cornersPath.cornerRadius) - } - - // ========== BOTTOM-LEFT CORNER ========== - // Arc is at the top-right of this square (inner diagonal) - PathMove { - x: cornersPath.leftMargin - y: cornersPath.screenHeight - cornersPath.bottomMargin - cornersPath.cornerSize - } - - // Top edge (moving right toward arc) - PathLine { - relativeX: cornersPath.cornerSize - cornersPath.cornerRadius - relativeY: 0 - } - - // Concave arc (top-right corner of square, curving inward toward screen center) - PathArc { - relativeX: cornersPath.cornerRadius - relativeY: cornersPath.cornerRadius - radiusX: cornersPath.cornerRadius - radiusY: cornersPath.cornerRadius - direction: PathArc.Counterclockwise - } - - // Right edge (moving down) - PathLine { - relativeX: 0 - relativeY: cornersPath.cornerSize - cornersPath.cornerRadius - } - - // Bottom edge (moving left) - PathLine { - relativeX: -cornersPath.cornerSize - relativeY: 0 - } - - // Left edge (moving up) - closes back to start - PathLine { - relativeX: 0 - relativeY: -cornersPath.cornerSize - } - - // ========== BOTTOM-RIGHT CORNER ========== - // Arc is at the top-left of this square (inner diagonal) - // Start at bottom-right of square (different from other corners!) - PathMove { - x: cornersPath.screenWidth - cornersPath.rightMargin - y: cornersPath.screenHeight - cornersPath.bottomMargin - } - - // Bottom edge (moving left) - PathLine { - relativeX: -cornersPath.cornerSize - relativeY: 0 - } - - // Left edge (moving up toward arc) - PathLine { - relativeX: 0 - relativeY: -(cornersPath.cornerSize - cornersPath.cornerRadius) - } - - // Concave arc (top-left corner of square, curving inward toward screen center) - PathArc { - relativeX: cornersPath.cornerRadius - relativeY: -cornersPath.cornerRadius - radiusX: cornersPath.cornerRadius - radiusY: cornersPath.cornerRadius - direction: PathArc.Counterclockwise - } - - // Top edge (moving right) - PathLine { - relativeX: cornersPath.cornerSize - cornersPath.cornerRadius - relativeY: 0 - } - - // Right edge (moving down) - closes back to start - PathLine { - relativeX: 0 - relativeY: cornersPath.cornerSize - } } } diff --git a/Modules/MainScreen/SmartPanel.qml b/Modules/MainScreen/SmartPanel.qml index e4bda7a2..1d06371f 100644 --- a/Modules/MainScreen/SmartPanel.qml +++ b/Modules/MainScreen/SmartPanel.qml @@ -1,7 +1,7 @@ import QtQuick import Quickshell import qs.Commons -import qs.Services +import qs.Services.UI /** @@ -528,6 +528,7 @@ Item { Logger.d("SmartPanel", "Position calculated:", calculatedX, calculatedY) Logger.d("SmartPanel", " Panel size:", panelWidth, "x", panelHeight) + Logger.d("SmartPanel", " Parent size:", root.width, "x", root.height) } // Watch for changes in content-driven sizes and update position @@ -744,12 +745,14 @@ Item { if (!isPanelVisible) { return true } + // PRIORITY 1: Bar attachment (always takes precedence) // Attached to bar at top if (willTouchTopBar) { return true } + // PRIORITY 2: Screen edge attachment (only if not touching bar) // Attached to screen top edge (not bar) - if (willTouchTopEdge && !willTouchTopBar) { + if (willTouchTopEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar) { return true } // If panel is not attached to any edge, animate from top by default @@ -762,12 +765,14 @@ Item { if (!isPanelVisible) { return false } + // PRIORITY 1: Bar attachment (always takes precedence) // Attached to bar at bottom if (willTouchBottomBar) { return true } + // PRIORITY 2: Screen edge attachment (only if not touching bar) // Attached to screen bottom edge (not bar) - if (willTouchBottomEdge && !willTouchBottomBar) { + if (willTouchBottomEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar) { return true } return false @@ -776,22 +781,25 @@ Item { if (!isPanelVisible) { return false } - // Don't animate from left if also touching top/bottom edge (priority: vertical over horizontal) - // Check directly instead of relying on other computed properties to avoid race conditions - var touchingTopBar = isPanelVisible && panelContent.allowAttachToBar && root.barPosition === "top" && !root.barIsVertical && Math.abs(panelBackground.targetY - (root.barMarginV + Style.barHeight)) <= 1 - var touchingBottomBar = isPanelVisible && panelContent.allowAttachToBar && root.barPosition === "bottom" && !root.barIsVertical && Math.abs(panelBackground.targetY - (root.height - root.barMarginV - Style.barHeight - panelBackground.targetHeight)) <= 1 - var touchingTopEdge = isPanelVisible && panelContent.allowAttach && panelBackground.targetY <= 1 - var touchingBottomEdge = isPanelVisible && panelContent.allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1) - - if (touchingTopEdge || touchingBottomEdge || touchingTopBar || touchingBottomBar) { + // PRIORITY 1: Bar attachment (always takes precedence) + // If touching any horizontal bar, don't animate from left + if (willTouchTopBar || willTouchBottomBar) { return false } // Attached to bar at left if (willTouchLeftBar) { return true } + // PRIORITY 2: Screen edge attachment (only if not touching any bar) + // Don't animate from left if also touching top/bottom edge (priority: vertical over horizontal) + var touchingTopEdge = isPanelVisible && panelContent.allowAttach && panelBackground.targetY <= 1 + var touchingBottomEdge = isPanelVisible && panelContent.allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1) + + if (touchingTopEdge || touchingBottomEdge) { + return false + } // Attached to screen left edge (not bar) - if (willTouchLeftEdge && !willTouchLeftBar) { + if (willTouchLeftEdge && !willTouchLeftBar && !willTouchTopBar && !willTouchBottomBar && !willTouchRightBar) { return true } return false @@ -800,22 +808,25 @@ Item { if (!isPanelVisible) { return false } - // Don't animate from right if also touching top/bottom edge (priority: vertical over horizontal) - // Check directly instead of relying on other computed properties to avoid race conditions - var touchingTopBar = isPanelVisible && panelContent.allowAttachToBar && root.barPosition === "top" && !root.barIsVertical && Math.abs(panelBackground.targetY - (root.barMarginV + Style.barHeight)) <= 1 - var touchingBottomBar = isPanelVisible && panelContent.allowAttachToBar && root.barPosition === "bottom" && !root.barIsVertical && Math.abs(panelBackground.targetY - (root.height - root.barMarginV - Style.barHeight - panelBackground.targetHeight)) <= 1 - var touchingTopEdge = isPanelVisible && panelContent.allowAttach && panelBackground.targetY <= 1 - var touchingBottomEdge = isPanelVisible && panelContent.allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1) - - if (touchingTopEdge || touchingBottomEdge || touchingTopBar || touchingBottomBar) { + // PRIORITY 1: Bar attachment (always takes precedence) + // If touching any horizontal bar, don't animate from right + if (willTouchTopBar || willTouchBottomBar) { return false } // Attached to bar at right if (willTouchRightBar) { return true } + // PRIORITY 2: Screen edge attachment (only if not touching any bar) + // Don't animate from right if also touching top/bottom edge (priority: vertical over horizontal) + var touchingTopEdge = isPanelVisible && panelContent.allowAttach && panelBackground.targetY <= 1 + var touchingBottomEdge = isPanelVisible && panelContent.allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - 1) + + if (touchingTopEdge || touchingBottomEdge) { + return false + } // Attached to screen right edge (not bar) - if (willTouchRightEdge && !willTouchRightBar) { + if (willTouchRightEdge && !willTouchLeftBar && !willTouchTopBar && !willTouchBottomBar && !willTouchRightBar) { return true } return false diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index c8ffce5d..f09ef643 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -6,7 +6,7 @@ import Quickshell.Widgets import Quickshell.Wayland import Quickshell.Services.Notifications import qs.Commons -import qs.Services +import qs.Services.System import qs.Widgets // Simple notification popup - displays multiple notifications @@ -61,7 +61,7 @@ Variants { readonly property string barPos: Settings.data.bar.position readonly property bool isFloating: Settings.data.bar.floating - readonly property int notifWidth: Math.round(420 * Style.uiScaleRatio) + readonly property int notifWidth: Math.round(400 * Style.uiScaleRatio) // Calculate bar offsets for each edge separately readonly property int barOffsetTop: { @@ -99,10 +99,10 @@ Variants { anchors.right: isRight // Margins for PanelWindow - only apply bar offset for the specific edge where the bar is - margins.top: isTop ? Style.marginM + barOffsetTop : 0 - margins.bottom: isBottom ? Style.marginM + barOffsetBottom : 0 - margins.left: isLeft ? Style.marginM + barOffsetLeft : 0 - margins.right: isRight ? Style.marginM + barOffsetRight : 0 + margins.top: isTop ? barOffsetTop : 0 + margins.bottom: isBottom ? barOffsetBottom : 0 + margins.left: isLeft ? barOffsetLeft : 0 + margins.right: isRight ? barOffsetRight : 0 implicitWidth: notifWidth implicitHeight: notificationStack.implicitHeight + Style.marginL @@ -251,6 +251,7 @@ Variants { NDropShadows { anchors.fill: cardBackground source: cardBackground + autoPaddingEnabled: true } // Hover handling diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index 835965e6..248f6ac8 100644 --- a/Modules/OSD/OSD.qml +++ b/Modules/OSD/OSD.qml @@ -5,8 +5,10 @@ import QtQuick.Effects import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Services import qs.Widgets +import qs.Services.Hardware +import qs.Services.Media +import qs.Services.System // Unified OSD component // Loader activates only when showing OSD, deactivates when hidden to save resources @@ -137,10 +139,10 @@ Variants { readonly property bool isRight: (location.indexOf("_right") >= 0) || (location === "right") readonly property bool isCentered: (location === "top" || location === "bottom") readonly property bool verticalMode: (location === "left" || location === "right") - readonly property int hWidth: Math.round(420 * Style.uiScaleRatio) - readonly property int hHeight: Math.round(84 * Style.uiScaleRatio) - readonly property int vWidth: Math.round(92 * Style.uiScaleRatio) - readonly property int vHeight: Math.round(380 * Style.uiScaleRatio) + readonly property int hWidth: Math.round(320 * Style.uiScaleRatio) + readonly property int hHeight: Math.round(72 * Style.uiScaleRatio) + readonly property int vWidth: Math.round(72 * Style.uiScaleRatio) + readonly property int vHeight: Math.round(280 * Style.uiScaleRatio) // Ensure an even width to keep the vertical bar perfectly centered readonly property int barThickness: { @@ -208,6 +210,10 @@ Variants { WlrLayershell.layer: (Settings.data.osd && Settings.data.osd.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top exclusionMode: PanelWindow.ExclusionMode.Ignore + // Rectangle { + // anchors.fill: parent + // color: "#4400FF00" + // } Item { id: osdItem anchors.fill: parent @@ -253,7 +259,7 @@ Variants { Rectangle { id: background anchors.fill: parent - anchors.margins: Style.marginM * 2 + anchors.margins: Style.marginM * 1.5 radius: Style.radiusL color: Color.mSurface border.color: Color.mOutline @@ -263,17 +269,17 @@ Variants { } } - // MultiEffect applied to background only NDropShadows { anchors.fill: background source: background + autoPaddingEnabled: true } // Content loader on top of the background (not affected by MultiEffect) Loader { id: contentLoader - anchors.fill: parent - anchors.margins: Style.marginL * 2 + anchors.fill: background + anchors.margins: Style.marginM active: true sourceComponent: panel.verticalMode ? verticalContent : horizontalContent } diff --git a/Modules/Panels/Audio/AudioPanel.qml b/Modules/Panels/Audio/AudioPanel.qml index b2b53f2f..6d857791 100644 --- a/Modules/Panels/Audio/AudioPanel.qml +++ b/Modules/Panels/Audio/AudioPanel.qml @@ -4,9 +4,9 @@ import QtQuick.Controls import Quickshell import Quickshell.Services.Pipewire import qs.Commons -import qs.Services import qs.Widgets import qs.Modules.MainScreen +import qs.Services.Media SmartPanel { id: root diff --git a/Modules/Panels/Battery/BatteryPanel.qml b/Modules/Panels/Battery/BatteryPanel.qml index 50d31791..f9544e26 100644 --- a/Modules/Panels/Battery/BatteryPanel.qml +++ b/Modules/Panels/Battery/BatteryPanel.qml @@ -4,7 +4,7 @@ import QtQuick.Controls import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Services +import qs.Services.Hardware import qs.Widgets import qs.Modules.MainScreen diff --git a/Modules/Panels/Bluetooth/BluetoothDevicesList.qml b/Modules/Panels/Bluetooth/BluetoothDevicesList.qml index 3491a80b..147f1042 100644 --- a/Modules/Panels/Bluetooth/BluetoothDevicesList.qml +++ b/Modules/Panels/Bluetooth/BluetoothDevicesList.qml @@ -5,7 +5,7 @@ import Quickshell import Quickshell.Bluetooth import Quickshell.Wayland import qs.Commons -import qs.Services +import qs.Services.Networking import qs.Widgets NBox { diff --git a/Modules/Panels/Bluetooth/BluetoothPanel.qml b/Modules/Panels/Bluetooth/BluetoothPanel.qml index db1402ff..d5661867 100644 --- a/Modules/Panels/Bluetooth/BluetoothPanel.qml +++ b/Modules/Panels/Bluetooth/BluetoothPanel.qml @@ -3,22 +3,22 @@ import QtQuick.Layouts import QtQuick.Controls import Quickshell import Quickshell.Bluetooth -import Quickshell.Wayland import qs.Commons -import qs.Services +import qs.Services.UI +import qs.Services.Networking import qs.Widgets import qs.Modules.MainScreen SmartPanel { id: root - preferredWidth: Math.round(340 * Style.uiScaleRatio) - preferredHeight: 500 * Style.uiScaleRatio + preferredWidth: Math.round(400 * Style.uiScaleRatio) + preferredHeight: Math.round(500 * Style.uiScaleRatio) panelContent: Rectangle { color: Color.transparent - property real contentPreferredHeight: Math.min(preferredHeight, Math.max(240, mainColumn.implicitHeight + Style.marginL * 2)) + property real contentPreferredHeight: !(BluetoothService.adapter && BluetoothService.adapter.enabled) ? Math.min(preferredHeight, Math.max(280 * Style.uiScaleRatio, mainColumn.implicitHeight + Style.marginL * 2)) : (mainColumn.implicitHeight + Style.marginL * 2) ColumnLayout { id: mainColumn @@ -29,13 +29,12 @@ SmartPanel { // Header NBox { Layout.fillWidth: true - implicitHeight: headerRow.implicitHeight + (Style.marginM * 2) + Layout.preferredHeight: headerRow.implicitHeight + Style.marginM * 2 RowLayout { id: headerRow anchors.fill: parent - anchors.leftMargin: Style.marginM - anchors.rightMargin: Style.marginM + anchors.margins: Style.marginM spacing: Style.marginM NIcon { @@ -90,9 +89,13 @@ SmartPanel { // Center the content within this rectangle ColumnLayout { - anchors.centerIn: parent + anchors.fill: parent spacing: Style.marginM + Item { + Layout.fillHeight: true + } + NIcon { icon: "bluetooth-off" pointSize: 48 @@ -111,7 +114,13 @@ SmartPanel { text: I18n.tr("bluetooth.panel.enable-message") pointSize: Style.fontSizeS color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + Item { + Layout.fillHeight: true } } } diff --git a/Modules/Panels/Calendar/CalendarPanel.qml b/Modules/Panels/Calendar/CalendarPanel.qml index f28158fc..a01f7836 100644 --- a/Modules/Panels/Calendar/CalendarPanel.qml +++ b/Modules/Panels/Calendar/CalendarPanel.qml @@ -4,10 +4,11 @@ import QtQuick.Layouts import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Services -import qs.Widgets import qs.Modules.MainScreen import qs.Modules.Panels.ControlCenter.Cards +import qs.Services.Location +import qs.Services.UI +import qs.Widgets SmartPanel { id: root diff --git a/Modules/Panels/ControlCenter/Cards/AudioCard.qml b/Modules/Panels/ControlCenter/Cards/AudioCard.qml index 3370b71f..47c35711 100644 --- a/Modules/Panels/ControlCenter/Cards/AudioCard.qml +++ b/Modules/Panels/ControlCenter/Cards/AudioCard.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.Media import qs.Widgets // Audio controls card: output and input volume controls diff --git a/Modules/Panels/ControlCenter/Cards/MediaCard.qml b/Modules/Panels/ControlCenter/Cards/MediaCard.qml index 9879d84c..7b922fbb 100644 --- a/Modules/Panels/ControlCenter/Cards/MediaCard.qml +++ b/Modules/Panels/ControlCenter/Cards/MediaCard.qml @@ -3,10 +3,11 @@ import QtQuick.Controls import QtQuick.Layouts import QtQuick.Effects import Quickshell -import qs.Widgets.AudioSpectrum import qs.Commons -import qs.Services +import qs.Services.Media +import qs.Services.UI import qs.Widgets +import qs.Widgets.AudioSpectrum NBox { id: root @@ -182,61 +183,28 @@ NBox { } } - ColumnLayout { + // Content container that adjusts for player selector + Item { anchors.fill: parent - anchors.margins: Style.marginM + anchors.topMargin: playerSelectorButton.visible ? (playerSelectorButton.height + Style.marginXS + Style.marginM) : Style.marginM + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + anchors.bottomMargin: Style.marginM - // No media player detected - use Loader for performance - Loader { - id: fallbackLoader - Layout.fillWidth: true - Layout.fillHeight: true - active: !root.hasActivePlayer - - sourceComponent: ColumnLayout { - spacing: Style.marginS - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - } - - Item { - Layout.fillWidth: true - - ColumnLayout { - anchors.centerIn: parent - spacing: Style.marginL - - Item { - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: Style.fontSizeXXXL * 4 - Layout.preferredHeight: Style.fontSizeXXXL * 4 - - // Center icon (static, no animation to avoid GPU cost) - NIcon { - anchors.centerIn: parent - icon: "disc" - pointSize: Style.fontSizeXXXL * 3 - color: Color.mOnSurfaceVariant - opacity: 1.0 - } - } - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - } - } + // No media player detected - centered disc icon + NIcon { + anchors.centerIn: parent + visible: !root.hasActivePlayer + icon: "disc" + pointSize: Style.fontSizeXXXL * 3 + color: Color.mOnSurfaceVariant + opacity: 1.0 } // MediaPlayer Main Content - use Loader for performance Loader { id: mainLoader - Layout.fillWidth: true - Layout.fillHeight: true + anchors.fill: parent active: root.hasActivePlayer sourceComponent: ColumnLayout { diff --git a/Modules/Panels/ControlCenter/Cards/ProfileCard.qml b/Modules/Panels/ControlCenter/Cards/ProfileCard.qml index a8e04484..2a75e700 100644 --- a/Modules/Panels/ControlCenter/Cards/ProfileCard.qml +++ b/Modules/Panels/ControlCenter/Cards/ProfileCard.qml @@ -5,10 +5,10 @@ import Quickshell import Quickshell.Io import Quickshell.Widgets import qs.Commons -import qs.Services -import qs.Widgets import qs.Modules.Panels.ControlCenter.Cards import qs.Modules.Panels.Settings +import qs.Services.UI +import qs.Widgets // Header card with avatar, user and quick actions NBox { diff --git a/Modules/Panels/ControlCenter/Cards/ShortcutsCard.qml b/Modules/Panels/ControlCenter/Cards/ShortcutsCard.qml index 5e7c7ad6..91fba4b9 100644 --- a/Modules/Panels/ControlCenter/Cards/ShortcutsCard.qml +++ b/Modules/Panels/ControlCenter/Cards/ShortcutsCard.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services import qs.Widgets import qs.Modules.Panels.ControlCenter import qs.Modules.Panels.ControlCenter.Cards diff --git a/Modules/Panels/ControlCenter/Cards/SystemMonitorCard.qml b/Modules/Panels/ControlCenter/Cards/SystemMonitorCard.qml index fc94aa97..2aa62105 100644 --- a/Modules/Panels/ControlCenter/Cards/SystemMonitorCard.qml +++ b/Modules/Panels/ControlCenter/Cards/SystemMonitorCard.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.System import qs.Widgets // Unified system card: monitors CPU, temp, memory, disk diff --git a/Modules/Panels/ControlCenter/Cards/WeatherCard.qml b/Modules/Panels/ControlCenter/Cards/WeatherCard.qml index b9bd0344..52fcd7bc 100644 --- a/Modules/Panels/ControlCenter/Cards/WeatherCard.qml +++ b/Modules/Panels/ControlCenter/Cards/WeatherCard.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.Location import qs.Widgets // Weather overview card (placeholder data) diff --git a/Modules/Panels/ControlCenter/ControlCenterPanel.qml b/Modules/Panels/ControlCenter/ControlCenterPanel.qml index 510d781f..07be9664 100644 --- a/Modules/Panels/ControlCenter/ControlCenterPanel.qml +++ b/Modules/Panels/ControlCenter/ControlCenterPanel.qml @@ -3,10 +3,11 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services -import qs.Widgets import qs.Modules.MainScreen import qs.Modules.Panels.ControlCenter.Cards +import qs.Services.Media +import qs.Services.UI +import qs.Widgets SmartPanel { id: root diff --git a/Modules/Panels/ControlCenter/ControlCenterWidgetLoader.qml b/Modules/Panels/ControlCenter/ControlCenterWidgetLoader.qml index ebfb05f7..0fb3cd8b 100644 --- a/Modules/Panels/ControlCenter/ControlCenterWidgetLoader.qml +++ b/Modules/Panels/ControlCenter/ControlCenterWidgetLoader.qml @@ -1,6 +1,6 @@ import QtQuick import Quickshell -import qs.Services +import qs.Services.UI import qs.Commons Item { diff --git a/Modules/Panels/ControlCenter/Widgets/Bluetooth.qml b/Modules/Panels/ControlCenter/Widgets/Bluetooth.qml index 5378156d..d3ab79ad 100644 --- a/Modules/Panels/ControlCenter/Widgets/Bluetooth.qml +++ b/Modules/Panels/ControlCenter/Widgets/Bluetooth.qml @@ -1,7 +1,8 @@ import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.Networking +import qs.Services.UI import qs.Widgets NIconButtonHot { diff --git a/Modules/Panels/ControlCenter/Widgets/CustomButton.qml b/Modules/Panels/ControlCenter/Widgets/CustomButton.qml index 84c89c62..bad214ad 100644 --- a/Modules/Panels/ControlCenter/Widgets/CustomButton.qml +++ b/Modules/Panels/ControlCenter/Widgets/CustomButton.qml @@ -2,7 +2,6 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons -import qs.Services import qs.Widgets Item { diff --git a/Modules/Panels/ControlCenter/Widgets/KeepAwake.qml b/Modules/Panels/ControlCenter/Widgets/KeepAwake.qml index 96d60733..5b459e7e 100644 --- a/Modules/Panels/ControlCenter/Widgets/KeepAwake.qml +++ b/Modules/Panels/ControlCenter/Widgets/KeepAwake.qml @@ -1,7 +1,7 @@ import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.Power import qs.Widgets NIconButtonHot { diff --git a/Modules/Panels/ControlCenter/Widgets/NightLight.qml b/Modules/Panels/ControlCenter/Widgets/NightLight.qml index c59126ad..c45f197a 100644 --- a/Modules/Panels/ControlCenter/Widgets/NightLight.qml +++ b/Modules/Panels/ControlCenter/Widgets/NightLight.qml @@ -2,7 +2,8 @@ import QtQuick.Layouts import Quickshell import qs.Commons import qs.Modules.Panels.Settings -import qs.Services +import qs.Services.System +import qs.Services.UI import qs.Widgets NIconButtonHot { diff --git a/Modules/Panels/ControlCenter/Widgets/Notifications.qml b/Modules/Panels/ControlCenter/Widgets/Notifications.qml index 35f5cdf7..1877130e 100644 --- a/Modules/Panels/ControlCenter/Widgets/Notifications.qml +++ b/Modules/Panels/ControlCenter/Widgets/Notifications.qml @@ -1,15 +1,16 @@ import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.System +import qs.Services.UI import qs.Widgets NIconButtonHot { property ShellScreen screen - icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell" - hot: Settings.data.notifications.doNotDisturb + icon: NotificationService.doNotDisturb ? "bell-off" : "bell" + hot: NotificationService.doNotDisturb tooltipText: I18n.tr("quickSettings.notifications.tooltip.action") onClicked: PanelService.getPanel("notificationHistoryPanel", screen)?.toggle(this) - onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb + onRightClicked: NotificationService.doNotDisturb = !NotificationService.doNotDisturb } diff --git a/Modules/Panels/ControlCenter/Widgets/PowerProfile.qml b/Modules/Panels/ControlCenter/Widgets/PowerProfile.qml index ca5f497f..1a211c16 100644 --- a/Modules/Panels/ControlCenter/Widgets/PowerProfile.qml +++ b/Modules/Panels/ControlCenter/Widgets/PowerProfile.qml @@ -2,7 +2,7 @@ import QtQuick.Layouts import Quickshell import Quickshell.Services.UPower import qs.Commons -import qs.Services +import qs.Services.Power import qs.Widgets // Performance @@ -15,7 +15,5 @@ NIconButtonHot { icon: PowerProfileService.getIcon() hot: !PowerProfileService.isDefault() tooltipText: I18n.tr("quickSettings.powerProfile.tooltip.action") - onClicked: { - PowerProfileService.cycleProfile() - } + onClicked: PowerProfileService.cycleProfile() } diff --git a/Modules/Panels/ControlCenter/Widgets/ScreenRecorder.qml b/Modules/Panels/ControlCenter/Widgets/ScreenRecorder.qml index 85fbacd9..0cbfec0d 100644 --- a/Modules/Panels/ControlCenter/Widgets/ScreenRecorder.qml +++ b/Modules/Panels/ControlCenter/Widgets/ScreenRecorder.qml @@ -1,7 +1,9 @@ import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.Media +import qs.Services.System +import qs.Services.UI import qs.Widgets NIconButtonHot { @@ -14,8 +16,7 @@ NIconButtonHot { onClicked: { ScreenRecorderService.toggleRecording() if (!ScreenRecorderService.isRecording) { - var panel = PanelService.getPanel("controlCenterPanel", screen) - panel?.close() + PanelService.getPanel("controlCenterPanel", screen)?.close } } } diff --git a/Modules/Panels/ControlCenter/Widgets/WallpaperSelector.qml b/Modules/Panels/ControlCenter/Widgets/WallpaperSelector.qml index daa941ee..6d6e840d 100644 --- a/Modules/Panels/ControlCenter/Widgets/WallpaperSelector.qml +++ b/Modules/Panels/ControlCenter/Widgets/WallpaperSelector.qml @@ -1,7 +1,7 @@ import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.UI import qs.Widgets NIconButtonHot { diff --git a/Modules/Panels/ControlCenter/Widgets/WiFi.qml b/Modules/Panels/ControlCenter/Widgets/WiFi.qml index a24f43e1..e40047f1 100644 --- a/Modules/Panels/ControlCenter/Widgets/WiFi.qml +++ b/Modules/Panels/ControlCenter/Widgets/WiFi.qml @@ -1,7 +1,8 @@ import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.Networking +import qs.Services.UI import qs.Widgets NIconButtonHot { diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index 43e1f886..9747b3ee 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -4,17 +4,18 @@ import QtQuick.Layouts import Quickshell import Quickshell.Widgets import qs.Commons -import qs.Services -import qs.Widgets import qs.Modules.MainScreen +import qs.Services.Keyboard +import qs.Widgets + import "Plugins" SmartPanel { id: root // Panel configuration - preferredWidth: 500 * Style.uiScaleRatio - preferredHeight: 600 * Style.uiScaleRatio + preferredWidth: Math.round(500 * Style.uiScaleRatio) + preferredHeight: Math.round(600 * Style.uiScaleRatio) preferredWidthRatio: 0.3 preferredHeightRatio: 0.5 @@ -96,6 +97,14 @@ SmartPanel { selectPreviousWrapped() } + function onCtrlNPressed() { + selectNextWrapped() + } + + function onCtrlPPressed() { + selectPreviousWrapped() + } + // Public API for plugins function setSearchText(text) { searchText = text diff --git a/Modules/Panels/Launcher/Plugins/ApplicationsPlugin.qml b/Modules/Panels/Launcher/Plugins/ApplicationsPlugin.qml index 8add1460..3d3a1e60 100644 --- a/Modules/Panels/Launcher/Plugins/ApplicationsPlugin.qml +++ b/Modules/Panels/Launcher/Plugins/ApplicationsPlugin.qml @@ -2,7 +2,6 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons -import qs.Services import "../../../../Helpers/FuzzySort.js" as Fuzzysort Item { diff --git a/Modules/Panels/Launcher/Plugins/CalculatorPlugin.qml b/Modules/Panels/Launcher/Plugins/CalculatorPlugin.qml index 9eca7fc7..e8aaf5e3 100644 --- a/Modules/Panels/Launcher/Plugins/CalculatorPlugin.qml +++ b/Modules/Panels/Launcher/Plugins/CalculatorPlugin.qml @@ -1,5 +1,4 @@ import QtQuick -import qs.Services import qs.Commons import "../../../../Helpers/AdvancedMath.js" as AdvancedMath diff --git a/Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml b/Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml index 0d5837bd..e6922b87 100644 --- a/Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml +++ b/Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml @@ -1,7 +1,7 @@ import QtQuick import Quickshell import qs.Commons -import qs.Services +import qs.Services.Keyboard Item { id: root diff --git a/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml b/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml index 78c6fbbf..d0833879 100644 --- a/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml +++ b/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml @@ -5,7 +5,7 @@ import Quickshell import Quickshell.Wayland import Quickshell.Services.Notifications import qs.Commons -import qs.Services +import qs.Services.System import qs.Widgets import qs.Modules.MainScreen @@ -56,10 +56,10 @@ SmartPanel { } NIconButton { - icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell" - tooltipText: Settings.data.notifications.doNotDisturb ? I18n.tr("tooltips.do-not-disturb-enabled") : I18n.tr("tooltips.do-not-disturb-disabled") + icon: NotificationService.doNotDisturb ? "bell-off" : "bell" + tooltipText: NotificationService.doNotDisturb ? I18n.tr("tooltips.do-not-disturb-enabled") : I18n.tr("tooltips.do-not-disturb-disabled") baseSize: Style.baseWidgetSize * 0.8 - onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb + onClicked: NotificationService.doNotDisturb = !NotificationService.doNotDisturb } NIconButton { @@ -124,237 +124,197 @@ SmartPanel { } // Notification list - NListView { - id: notificationList + NScrollView { + id: scrollView Layout.fillWidth: true Layout.fillHeight: true - horizontalPolicy: ScrollBar.AlwaysOff - verticalPolicy: ScrollBar.AsNeeded - - model: NotificationService.historyList - spacing: Style.marginM + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded clip: true - boundsBehavior: Flickable.StopAtBounds visible: NotificationService.historyList.count > 0 // Track which notification is expanded property string expandedId: "" - delegate: Item { - property string notificationId: model.id - property bool isExpanded: notificationList.expandedId === notificationId + contentWidth: availableWidth - // Cache the content height to break binding loops - property real contentHeight: notificationLayout.implicitHeight + Column { + width: scrollView.width + spacing: Style.marginM - // Cache truncation state to avoid binding to truncated property during polish - property bool hasTextTruncated: false + Repeater { + model: NotificationService.historyList - width: notificationList.width - height: contentHeight + (Style.marginM * 2) - - Rectangle { - anchors.fill: parent - radius: Style.radiusM - color: Color.mSurfaceVariant - border.color: Qt.alpha(Color.mOutline, Style.opacityMedium) - border.width: Style.borderS - - // Smooth color transition on hover - Behavior on color { - enabled: !Settings.data.general.animationDisabled - ColorAnimation { - duration: Style.animationFast - } - } - } - - // Click to expand/collapse - MouseArea { - anchors.fill: parent - // Don't capture clicks on the delete button - anchors.rightMargin: 48 - enabled: hasTextTruncated - onClicked: { - if (notificationList.expandedId === notificationId) { - notificationList.expandedId = "" - } else { - notificationList.expandedId = notificationId - } - } - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - } - - // Update truncation state asynchronously - Connections { - target: summaryText - function onTruncatedChanged() { - hasTextTruncated = summaryText.truncated || bodyText.truncated - } - } - - Connections { - target: bodyText - function onTruncatedChanged() { - hasTextTruncated = summaryText.truncated || bodyText.truncated - } - } - - Component.onCompleted: { - hasTextTruncated = summaryText.truncated || bodyText.truncated - } - - Item { - id: notificationLayoutWrapper - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Style.marginM - height: notificationLayout.implicitHeight - - RowLayout { - id: notificationLayout + delegate: Item { + id: notificationDelegate width: parent.width - spacing: Style.marginM + height: contentColumn.height + (Style.marginM * 2) - // Icon - properly centered vertically - NImageCircled { - Layout.preferredWidth: Math.round(40 * Style.uiScaleRatio) - Layout.preferredHeight: Math.round(40 * Style.uiScaleRatio) - Layout.alignment: Qt.AlignVCenter - imagePath: model.cachedImage || model.originalImage || "" - borderColor: Color.transparent - borderWidth: 0 - fallbackIcon: "bell" - fallbackIconSize: 24 + property string notificationId: model.id + property bool isExpanded: scrollView.expandedId === notificationId + property bool canExpand: summaryText.truncated || bodyText.truncated + + Rectangle { + anchors.fill: parent + radius: Style.radiusM + color: Color.mSurfaceVariant + border.color: Qt.alpha(Color.mOutline, Style.opacityMedium) + border.width: Style.borderS + + Behavior on color { + enabled: !Settings.data.general.animationDisabled + ColorAnimation { + duration: Style.animationFast + } + } } - // Notification content column - ColumnLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: Style.marginXS - Layout.rightMargin: -(Style.marginM + Style.baseWidgetSize * 0.6) + // Click to expand/collapse + MouseArea { + anchors.fill: parent + anchors.rightMargin: Style.baseWidgetSize + enabled: notificationDelegate.canExpand + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + if (scrollView.expandedId === notificationId) { + scrollView.expandedId = "" + } else { + scrollView.expandedId = notificationId + } + } + } - // Header row with app name and timestamp - RowLayout { - Layout.fillWidth: true - spacing: Style.marginS + Column { + id: contentColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginM + spacing: Style.marginM - // Urgency indicator - Rectangle { - Layout.preferredWidth: 6 - Layout.preferredHeight: 6 - Layout.alignment: Qt.AlignVCenter - radius: 3 - visible: model.urgency !== 1 - color: { - if (model.urgency === 2) - return Color.mError - else if (model.urgency === 0) - return Color.mOnSurfaceVariant - else - return Color.transparent + Row { + width: parent.width + spacing: Style.marginM + + // Icon + NImageCircled { + width: Math.round(40 * Style.uiScaleRatio) + height: Math.round(40 * Style.uiScaleRatio) + anchors.verticalCenter: parent.verticalCenter + imagePath: model.cachedImage || model.originalImage || "" + borderColor: Color.transparent + borderWidth: 0 + fallbackIcon: "bell" + fallbackIconSize: 24 + } + + // Content + Column { + width: parent.width - Math.round(40 * Style.uiScaleRatio) - Style.marginM - Style.baseWidgetSize + spacing: Style.marginXS + + // Header row with app name and timestamp + Row { + width: parent.width + spacing: Style.marginS + + // Urgency indicator + Rectangle { + width: 6 + height: 6 + anchors.verticalCenter: parent.verticalCenter + radius: 3 + visible: model.urgency !== 1 + color: { + if (model.urgency === 2) + return Color.mError + else if (model.urgency === 0) + return Color.mOnSurfaceVariant + else + return Color.transparent + } + } + + NText { + text: model.appName || "Unknown App" + pointSize: Style.fontSizeXS + color: Color.mSecondary + } + + NText { + text: Time.formatRelativeTime(model.timestamp) + pointSize: Style.fontSizeXS + color: Color.mSecondary + } + } + + // Summary + NText { + id: summaryText + width: parent.width + text: model.summary || I18n.tr("general.no-summary") + pointSize: Style.fontSizeM + font.weight: Font.Medium + color: Color.mOnSurface + textFormat: Text.PlainText + wrapMode: Text.Wrap + maximumLineCount: notificationDelegate.isExpanded ? 999 : 2 + elide: Text.ElideRight + } + + // Body + NText { + id: bodyText + width: parent.width + text: model.body || "" + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + textFormat: Text.PlainText + wrapMode: Text.Wrap + maximumLineCount: notificationDelegate.isExpanded ? 999 : 3 + elide: Text.ElideRight + visible: text.length > 0 + } + + // Expand indicator + Row { + width: parent.width + visible: !notificationDelegate.isExpanded && notificationDelegate.canExpand + spacing: Style.marginXS + + Item { + width: parent.width - expandText.width - expandIcon.width - Style.marginXS + height: 1 + } + + NText { + id: expandText + text: I18n.tr("notifications.panel.click-to-expand") || "Click to expand" + pointSize: Style.fontSizeXS + color: Color.mPrimary + font.weight: Font.Medium + } + + NIcon { + id: expandIcon + icon: "chevron-down" + pointSize: Style.fontSizeS + color: Color.mPrimary + } } } - NText { - text: model.appName || "Unknown App" - pointSize: Style.fontSizeXS - color: Color.mSecondary + // Delete button + NIconButton { + icon: "trash" + tooltipText: I18n.tr("tooltips.delete-notification") + baseSize: Style.baseWidgetSize * 0.7 + anchors.verticalCenter: parent.verticalCenter + + onClicked: { + NotificationService.removeFromHistory(notificationId) + } } - - NText { - text: Time.formatRelativeTime(model.timestamp) - pointSize: Style.fontSizeXS - color: Color.mSecondary - } - - Item { - Layout.fillWidth: true - } - } - - // Summary - NText { - id: summaryText - text: model.summary || I18n.tr("general.no-summary") - pointSize: Style.fontSizeM - font.weight: Font.Medium - color: Color.mOnSurface - textFormat: Text.PlainText - wrapMode: Text.Wrap - Layout.fillWidth: true - maximumLineCount: isExpanded ? 999 : 2 - elide: Text.ElideRight - - // Smooth transition without triggering layout recalculation - Behavior on maximumLineCount { - enabled: false // Disable animation on this to avoid polish loops - } - } - - // Body - NText { - id: bodyText - text: model.body || "" - pointSize: Style.fontSizeS - color: Color.mOnSurfaceVariant - textFormat: Text.PlainText - wrapMode: Text.Wrap - Layout.fillWidth: true - maximumLineCount: isExpanded ? 999 : 3 - elide: Text.ElideRight - visible: text.length > 0 - - // Smooth transition without triggering layout recalculation - Behavior on maximumLineCount { - enabled: false // Disable animation on this to avoid polish loops - } - } - - // Spacer for expand indicator - Item { - Layout.fillWidth: true - Layout.preferredHeight: Style.marginS - visible: !isExpanded && hasTextTruncated - } - - // Expand indicator - RowLayout { - Layout.fillWidth: true - visible: !isExpanded && hasTextTruncated - spacing: Style.marginXS - - Item { - Layout.fillWidth: true - } - - NText { - text: I18n.tr("notifications.panel.click-to-expand") || "Click to expand" - pointSize: Style.fontSizeXS - color: Color.mPrimary - font.weight: Font.Medium - } - - NIcon { - icon: "chevron-down" - pointSize: Style.fontSizeS - color: Color.mPrimary - } - } - } - - // Delete button - NIconButton { - icon: "trash" - tooltipText: I18n.tr("tooltips.delete-notification") - baseSize: Style.baseWidgetSize * 0.7 - Layout.alignment: Qt.AlignTop - - onClicked: { - // Remove from history using the service API - NotificationService.removeFromHistory(notificationId) } } } diff --git a/Modules/Panels/SessionMenu/SessionMenu.qml b/Modules/Panels/SessionMenu/SessionMenu.qml index 4fdcdeea..942ae471 100644 --- a/Modules/Panels/SessionMenu/SessionMenu.qml +++ b/Modules/Panels/SessionMenu/SessionMenu.qml @@ -7,7 +7,8 @@ import Quickshell.Io import Quickshell.Widgets import Quickshell.Wayland import qs.Commons -import qs.Services +import qs.Services.Compositor +import qs.Services.UI import qs.Widgets import qs.Modules.MainScreen diff --git a/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml b/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml index c6f866fc..e77eb945 100644 --- a/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml +++ b/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml @@ -4,7 +4,7 @@ import QtQuick.Effects import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services +import qs.Services.UI // Widget Settings Dialog Component Popup { @@ -25,19 +25,12 @@ Popup { anchors.centerIn: parent onOpened: { - // Mark this popup has opened in the PanelService - PanelService.willOpenPopup(root) - // Load settings when popup opens with data if (widgetData && widgetId) { loadWidgetSettings() } } - onClosed: { - PanelService.willClosePopup(root) - } - background: Rectangle { id: bgRect diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml index 514733dd..140fc511 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/AudioVisualizerSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/AudioVisualizerSettings.qml index 936260a6..dea5809d 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/AudioVisualizerSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/AudioVisualizerSettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/BatterySettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/BatterySettings.qml index 8377958d..7a5f046c 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/BatterySettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/BatterySettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/BrightnessSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/BrightnessSettings.qml index bc572776..ecf4869c 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/BrightnessSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/BrightnessSettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/ClockSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/ClockSettings.qml index a214f508..3e650121 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/ClockSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/ClockSettings.qml @@ -3,7 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services +import qs.Services.System ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/ControlCenterSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/ControlCenterSettings.qml index 66c30c11..1b423d9c 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/ControlCenterSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/ControlCenterSettings.qml @@ -4,7 +4,6 @@ import QtQuick.Layouts import Quickshell import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root @@ -18,12 +17,14 @@ ColumnLayout { property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo property string valueCustomIconPath: widgetData.customIconPath !== undefined ? widgetData.customIconPath : "" + property bool valueColorizeDistroLogo: widgetData.colorizeDistroLogo !== undefined ? widgetData.colorizeDistroLogo : (widgetMetadata.colorizeDistroLogo !== undefined ? widgetMetadata.colorizeDistroLogo : false) function saveSettings() { var settings = Object.assign({}, widgetData || {}) settings.icon = valueIcon settings.useDistroLogo = valueUseDistroLogo settings.customIconPath = valueCustomIconPath + settings.colorizeDistroLogo = valueColorizeDistroLogo return settings } @@ -40,6 +41,16 @@ ColumnLayout { } } + NToggle { + visible: valueUseDistroLogo + label: I18n.tr("bar.widget-settings.control-center.colorize-distro-logo.label") + description: I18n.tr("bar.widget-settings.control-center.colorize-distro-logo.description") + checked: valueColorizeDistroLogo + onToggled: function (checked) { + valueColorizeDistroLogo = checked + } + } + RowLayout { spacing: Style.marginM diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/CustomButtonSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/CustomButtonSettings.qml index 129613ab..715e4ef9 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/CustomButtonSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/CustomButtonSettings.qml @@ -4,7 +4,6 @@ import QtQuick.Layouts import QtQuick.Window import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root @@ -16,6 +15,7 @@ ColumnLayout { property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon property bool valueTextStream: widgetData.textStream !== undefined ? widgetData.textStream : widgetMetadata.textStream property bool valueParseJson: widgetData.parseJson !== undefined ? widgetData.parseJson : widgetMetadata.parseJson + property bool valueHideTextInVerticalBar: widgetData.hideTextInVerticalBar !== undefined ? widgetData.hideTextInVerticalBar : widgetMetadata.hideTextInVerticalBar function saveSettings() { var settings = Object.assign({}, widgetData || {}) @@ -27,6 +27,7 @@ ColumnLayout { settings.textCollapse = textCollapseInput.text settings.textStream = valueTextStream settings.parseJson = valueParseJson + settings.hideTextInVerticalBar = valueHideTextInVerticalBar settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10) return settings } @@ -95,6 +96,13 @@ ColumnLayout { label: I18n.tr("bar.widget-settings.custom-button.dynamic-text") } + NToggle { + label: I18n.tr("bar.widget-settings.custom-button.hide-vertical.label", "Hide text in vertical bar") + description: I18n.tr("bar.widget-settings.custom-button.hide-vertical.description", "If enabled, the text from the command output will not be shown when the bar is in a vertical layout (left or right).") + checked: valueHideTextInVerticalBar + onToggled: checked => valueHideTextInVerticalBar = checked + } + NToggle { id: textStreamInput label: I18n.tr("bar.widget-settings.custom-button.text-stream.label") diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/KeyboardLayoutSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/KeyboardLayoutSettings.qml index 86ada35a..5b2bc230 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/KeyboardLayoutSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/KeyboardLayoutSettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/LockKeysSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/LockKeysSettings.qml index 012879cc..dd9b12ef 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/LockKeysSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/LockKeysSettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/MediaMiniSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/MediaMiniSettings.qml index 5f8c039d..808fe3dc 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/MediaMiniSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/MediaMiniSettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/MicrophoneSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/MicrophoneSettings.qml index e62ac1da..3ff2ee58 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/MicrophoneSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/MicrophoneSettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/NotificationHistorySettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/NotificationHistorySettings.qml index 61c4e10d..ba6475af 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/NotificationHistorySettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/NotificationHistorySettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/SpacerSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/SpacerSettings.qml index 9679c619..423f0d65 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/SpacerSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/SpacerSettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml index 323bacfd..9ca62f78 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/TaskbarSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/TaskbarSettings.qml index 9ea560e8..825d32d5 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/TaskbarSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/TaskbarSettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/VolumeSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/VolumeSettings.qml index 3b131d38..09032661 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/VolumeSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/VolumeSettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/WorkspaceSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/WorkspaceSettings.qml index 4beecd41..83d0bbad 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/WorkspaceSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/WorkspaceSettings.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services ColumnLayout { id: root diff --git a/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml b/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml index 4b9c9fb1..8ebea72e 100644 --- a/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml +++ b/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml @@ -3,7 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services +import qs.Services.UI // Widget Settings Dialog Component Popup { @@ -23,16 +23,11 @@ Popup { anchors.centerIn: parent onOpened: { - PanelService.willOpenPopup(root) if (widgetData && widgetId) { loadWidgetSettings() } } - onClosed: { - PanelService.willClosePopup(root) - } - background: Rectangle { color: Color.mSurface radius: Style.radiusL diff --git a/Modules/Panels/Settings/SettingsPanel.qml b/Modules/Panels/Settings/SettingsPanel.qml index f1694ab0..9d024e7d 100644 --- a/Modules/Panels/Settings/SettingsPanel.qml +++ b/Modules/Panels/Settings/SettingsPanel.qml @@ -2,21 +2,23 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell -import Quickshell.Wayland import qs.Modules.Panels.Settings.Tabs import qs.Commons -import qs.Services -import qs.Widgets import qs.Modules.MainScreen +import qs.Services.System +import qs.Widgets SmartPanel { id: root preferredWidth: Math.round(820 * Style.uiScaleRatio) - preferredHeight: Math.round(900 * Style.uiScaleRatio) + preferredHeight: Math.round(910 * Style.uiScaleRatio) readonly property bool attachToBar: Settings.data.ui.settingsPanelAttachToBar readonly property string barPosition: Settings.data.bar.position + readonly property bool barFloating: Settings.data.bar.floating + readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 forceAttachToBar: attachToBar panelAnchorHorizontalCenter: attachToBar ? (barPosition === "top" || barPosition === "bottom") : true @@ -32,6 +34,30 @@ SmartPanel { } } + onBarPositionChanged: { + if (isPanelOpen) { + Qt.callLater(root.setPosition) + } + } + + onBarFloatingChanged: { + if (isPanelOpen) { + Qt.callLater(root.setPosition) + } + } + + onBarMarginHChanged: { + if (isPanelOpen) { + Qt.callLater(root.setPosition) + } + } + + onBarMarginVChanged: { + if (isPanelOpen) { + Qt.callLater(root.setPosition) + } + } + // Tabs enumeration, order is NOT relevant enum Tab { About, diff --git a/Modules/Panels/Settings/Tabs/AboutTab.qml b/Modules/Panels/Settings/Tabs/AboutTab.qml index 3bd64a45..b3463bc3 100644 --- a/Modules/Panels/Settings/Tabs/AboutTab.qml +++ b/Modules/Panels/Settings/Tabs/AboutTab.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.Noctalia import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/AudioTab.qml b/Modules/Panels/Settings/Tabs/AudioTab.qml index 2133f5a1..d47fe949 100644 --- a/Modules/Panels/Settings/Tabs/AudioTab.qml +++ b/Modules/Panels/Settings/Tabs/AudioTab.qml @@ -3,7 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell.Services.Pipewire import qs.Commons -import qs.Services +import qs.Services.Media import qs.Widgets ColumnLayout { @@ -331,7 +331,6 @@ ColumnLayout { } // AudioService Visualizer section NComboBox { - id: audioVisualizerCombo label: I18n.tr("settings.audio.media.visualizer-type.label") description: I18n.tr("settings.audio.media.visualizer-type.description") model: [{ @@ -351,6 +350,20 @@ ColumnLayout { onSelected: key => Settings.data.audio.visualizerType = key } + NComboBox { + label: I18n.tr("settings.audio.media.visualizer-quality.label") + description: I18n.tr("settings.audio.media.visualizer-quality.description") + model: [{ + "key": "low", + "name": I18n.tr("options.visualizer-quality.low") + }, { + "key": "high", + "name": I18n.tr("options.visualizer-quality.high") + }] + currentKey: Settings.data.audio.visualizerQuality + onSelected: key => Settings.data.audio.visualizerQuality = key + } + NComboBox { label: I18n.tr("settings.audio.media.frame-rate.label") description: I18n.tr("settings.audio.media.frame-rate.description") diff --git a/Modules/Panels/Settings/Tabs/BarTab.qml b/Modules/Panels/Settings/Tabs/BarTab.qml index b01e1f88..5e0c2fad 100644 --- a/Modules/Panels/Settings/Tabs/BarTab.qml +++ b/Modules/Panels/Settings/Tabs/BarTab.qml @@ -3,7 +3,8 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.Compositor +import qs.Services.UI import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/ColorSchemeTab.qml b/Modules/Panels/Settings/Tabs/ColorSchemeTab.qml index f12decfa..d11e6e68 100644 --- a/Modules/Panels/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Panels/Settings/Tabs/ColorSchemeTab.qml @@ -4,7 +4,9 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.Theming +import qs.Services.System +import qs.Services.UI import qs.Widgets ColumnLayout { @@ -111,9 +113,9 @@ ColumnLayout { if (exitCode === 0) { Settings.data.colorSchemes.useWallpaperColors = true AppThemeService.generate() - ToastService.showNotice(I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.enabled"), "settings-color-scheme") + ToastService.showNotice(I18n.tr("toast.wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.enabled"), "settings-color-scheme") } else { - ToastService.showWarning(I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.not-installed")) + ToastService.showWarning(I18n.tr("oast.wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.not-installed")) } } @@ -128,7 +130,6 @@ ColumnLayout { Repeater { model: ColorSchemeService.schemes - delegate: Item { FileView { path: modelData @@ -248,7 +249,7 @@ ColumnLayout { matugenCheck.running = true } else { Settings.data.colorSchemes.useWallpaperColors = false - ToastService.showNotice(I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.disabled"), "settings-color-scheme") + ToastService.showNotice(I18n.tr("toast.wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.disabled"), "settings-color-scheme") if (Settings.data.colorSchemes.predefinedScheme) { ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme) @@ -468,6 +469,7 @@ ColumnLayout { ColumnLayout { Layout.fillWidth: true spacing: Style.marginL + visible: Settings.data.colorSchemes.useWallpaperColors || Settings.data.colorSchemes.generateTemplatesForPredefined NHeader { label: I18n.tr("settings.color-scheme.templates.section.label") @@ -755,8 +757,25 @@ ColumnLayout { } } } - } + NCheckbox { + label: "Spicetify" + description: ProgramCheckerService.spicetifyAvailable ? I18n.tr("settings.color-scheme.templates.programs.spicetify.description", { + "filepath": "~/.config/spicetify/Themes/Comfy/color.ini" + }) : I18n.tr("settings.color-scheme.templates.programs.spicetify.description-missing", { + "app": "spicetify" + }) + checked: Settings.data.templates.spicetify + enabled: ProgramCheckerService.spicetifyAvailable + opacity: ProgramCheckerService.spicetifyAvailable ? 1.0 : 0.6 + onToggled: checked => { + if (ProgramCheckerService.spicetifyAvailable) { + Settings.data.templates.spicetify = checked + AppThemeService.generate() + } + } + } + } // Miscellaneous NCollapsible { Layout.fillWidth: true @@ -771,7 +790,7 @@ ColumnLayout { onToggled: checked => { Settings.data.templates.enableUserTemplates = checked if (checked) { - MatugenTemplates.writeUserTemplatesToml() + TemplateRegistry.writeUserTemplatesToml() } AppThemeService.generate() } diff --git a/Modules/Panels/Settings/Tabs/ControlCenterTab.qml b/Modules/Panels/Settings/Tabs/ControlCenterTab.qml index 8e96428f..989f56e7 100644 --- a/Modules/Panels/Settings/Tabs/ControlCenterTab.qml +++ b/Modules/Panels/Settings/Tabs/ControlCenterTab.qml @@ -3,7 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.UI import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/DisplayTab.qml b/Modules/Panels/Settings/Tabs/DisplayTab.qml index ff863964..d9530e28 100644 --- a/Modules/Panels/Settings/Tabs/DisplayTab.qml +++ b/Modules/Panels/Settings/Tabs/DisplayTab.qml @@ -4,7 +4,9 @@ import QtQuick.Controls import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.Compositor +import qs.Services.Location +import qs.Services.Hardware import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/DockTab.qml b/Modules/Panels/Settings/Tabs/DockTab.qml index e9784bea..8afe2e59 100644 --- a/Modules/Panels/Settings/Tabs/DockTab.qml +++ b/Modules/Panels/Settings/Tabs/DockTab.qml @@ -3,7 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.Compositor import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/GeneralTab.qml b/Modules/Panels/Settings/Tabs/GeneralTab.qml index 80b282e9..c3bdfa99 100644 --- a/Modules/Panels/Settings/Tabs/GeneralTab.qml +++ b/Modules/Panels/Settings/Tabs/GeneralTab.qml @@ -3,7 +3,8 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services +import qs.Services.System +import qs.Services.UI import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/HooksTab.qml b/Modules/Panels/Settings/Tabs/HooksTab.qml index 0b664cd3..b129bd7c 100644 --- a/Modules/Panels/Settings/Tabs/HooksTab.qml +++ b/Modules/Panels/Settings/Tabs/HooksTab.qml @@ -2,7 +2,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services +import qs.Services.Control +import qs.Services.UI import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/LauncherTab.qml b/Modules/Panels/Settings/Tabs/LauncherTab.qml index 642d1ee8..e4cc4631 100644 --- a/Modules/Panels/Settings/Tabs/LauncherTab.qml +++ b/Modules/Panels/Settings/Tabs/LauncherTab.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services +import qs.Services.System import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/LocationTab.qml b/Modules/Panels/Settings/Tabs/LocationTab.qml index c2f264ce..b91edeb6 100644 --- a/Modules/Panels/Settings/Tabs/LocationTab.qml +++ b/Modules/Panels/Settings/Tabs/LocationTab.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services +import qs.Services.Location import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/LockScreenTab.qml b/Modules/Panels/Settings/Tabs/LockScreenTab.qml index 140ed960..46b9497d 100644 --- a/Modules/Panels/Settings/Tabs/LockScreenTab.qml +++ b/Modules/Panels/Settings/Tabs/LockScreenTab.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services import qs.Widgets ColumnLayout { @@ -13,7 +12,7 @@ ColumnLayout { 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 + onToggled: checked => Settings.data.general.lockOnSuspend = checked } NToggle { diff --git a/Modules/Panels/Settings/Tabs/NetworkTab.qml b/Modules/Panels/Settings/Tabs/NetworkTab.qml index 958a2842..9ff4c6d2 100644 --- a/Modules/Panels/Settings/Tabs/NetworkTab.qml +++ b/Modules/Panels/Settings/Tabs/NetworkTab.qml @@ -2,9 +2,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell -import Quickshell.Bluetooth import qs.Commons -import qs.Services +import qs.Services.Networking import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/NotificationsTab.qml b/Modules/Panels/Settings/Tabs/NotificationsTab.qml index b37f1d62..189e7038 100644 --- a/Modules/Panels/Settings/Tabs/NotificationsTab.qml +++ b/Modules/Panels/Settings/Tabs/NotificationsTab.qml @@ -3,7 +3,7 @@ import QtQuick.Layouts import QtQuick.Controls import Quickshell import qs.Commons -import qs.Services +import qs.Services.Compositor import qs.Widgets ColumnLayout { @@ -42,8 +42,8 @@ ColumnLayout { NToggle { label: I18n.tr("settings.notifications.settings.do-not-disturb.label") description: I18n.tr("settings.notifications.settings.do-not-disturb.description") - checked: Settings.data.notifications.doNotDisturb - onToggled: checked => Settings.data.notifications.doNotDisturb = checked + checked: NotificationService.doNotDisturb + onToggled: checked => NotificationService.doNotDisturb = checked } NComboBox { diff --git a/Modules/Panels/Settings/Tabs/OsdTab.qml b/Modules/Panels/Settings/Tabs/OsdTab.qml index 906f8883..3474ec87 100644 --- a/Modules/Panels/Settings/Tabs/OsdTab.qml +++ b/Modules/Panels/Settings/Tabs/OsdTab.qml @@ -3,7 +3,7 @@ import QtQuick.Layouts import QtQuick.Controls import Quickshell import qs.Commons -import qs.Services +import qs.Services.Compositor import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/ScreenRecorderTab.qml b/Modules/Panels/Settings/Tabs/ScreenRecorderTab.qml index d5e45091..580af116 100644 --- a/Modules/Panels/Settings/Tabs/ScreenRecorderTab.qml +++ b/Modules/Panels/Settings/Tabs/ScreenRecorderTab.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/UserInterfaceTab.qml b/Modules/Panels/Settings/Tabs/UserInterfaceTab.qml index 7ccf511c..abe077e4 100644 --- a/Modules/Panels/Settings/Tabs/UserInterfaceTab.qml +++ b/Modules/Panels/Settings/Tabs/UserInterfaceTab.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/WallpaperTab.qml b/Modules/Panels/Settings/Tabs/WallpaperTab.qml index 6fb57733..ec3995a0 100644 --- a/Modules/Panels/Settings/Tabs/WallpaperTab.qml +++ b/Modules/Panels/Settings/Tabs/WallpaperTab.qml @@ -4,7 +4,8 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.Compositor +import qs.Services.UI import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/SetupWizard/SetupAppearanceStep.qml b/Modules/Panels/SetupWizard/SetupAppearanceStep.qml index 7a2df8c0..56621419 100644 --- a/Modules/Panels/SetupWizard/SetupAppearanceStep.qml +++ b/Modules/Panels/SetupWizard/SetupAppearanceStep.qml @@ -3,7 +3,8 @@ import QtQuick.Layouts import QtQuick.Controls import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.System +import qs.Services.Theming import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/SetupWizard/SetupCustomizeStep.qml b/Modules/Panels/SetupWizard/SetupCustomizeStep.qml index 52da1576..2526933d 100644 --- a/Modules/Panels/SetupWizard/SetupCustomizeStep.qml +++ b/Modules/Panels/SetupWizard/SetupCustomizeStep.qml @@ -3,7 +3,6 @@ import QtQuick.Layouts import QtQuick.Controls import Quickshell import qs.Commons -import qs.Services import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/SetupWizard/SetupDockStep.qml b/Modules/Panels/SetupWizard/SetupDockStep.qml index 79164869..3b3e3168 100644 --- a/Modules/Panels/SetupWizard/SetupDockStep.qml +++ b/Modules/Panels/SetupWizard/SetupDockStep.qml @@ -3,7 +3,7 @@ import QtQuick.Layouts import QtQuick.Controls import Quickshell import qs.Commons -import qs.Services +import qs.Services.Compositor import qs.Widgets ColumnLayout { diff --git a/Modules/Panels/SetupWizard/SetupWallpaperStep.qml b/Modules/Panels/SetupWizard/SetupWallpaperStep.qml index d1d0df06..33b1edef 100644 --- a/Modules/Panels/SetupWizard/SetupWallpaperStep.qml +++ b/Modules/Panels/SetupWizard/SetupWallpaperStep.qml @@ -5,7 +5,7 @@ import QtQuick.Effects import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.UI import qs.Widgets import "../../../Helpers/FuzzySort.js" as FuzzySort diff --git a/Modules/Panels/SetupWizard/SetupWizard.qml b/Modules/Panels/SetupWizard/SetupWizard.qml index 74dd1fb3..e6955fb3 100644 --- a/Modules/Panels/SetupWizard/SetupWizard.qml +++ b/Modules/Panels/SetupWizard/SetupWizard.qml @@ -4,7 +4,6 @@ import QtQuick.Controls import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Services import qs.Widgets import qs.Modules.MainScreen @@ -183,7 +182,12 @@ SmartPanel { } } - // Step 2: UI Configuration + // Step 2: Appearance - Dark mode and color source + SetupAppearanceStep { + id: step3 + } + + // Step 3: UI Configuration SetupCustomizeStep { id: step2 selectedScaleRatio: root.selectedScaleRatio @@ -198,15 +202,10 @@ SmartPanel { } } - // Step 3: Dock Setup + // Step 4: Dock Setup SetupDockStep { id: stepDock } - - // Step 4: Appearance - Dark mode and color source - SetupAppearanceStep { - id: step3 - } } } @@ -234,15 +233,15 @@ SmartPanel { }, { "icon": "image", "label": "Wallpaper" + }, { + "icon": "palette", + "label": "Appearance" }, { "icon": "settings", "label": "Customize" }, { "icon": "device-desktop", "label": "Dock" - }, { - "icon": "palette", - "label": "Appearance" }] delegate: RowLayout { spacing: Style.marginS diff --git a/Modules/Panels/Tray/TrayDrawerPanel.qml b/Modules/Panels/Tray/TrayDrawerPanel.qml index 85cced89..20db163a 100644 --- a/Modules/Panels/Tray/TrayDrawerPanel.qml +++ b/Modules/Panels/Tray/TrayDrawerPanel.qml @@ -4,7 +4,7 @@ import Quickshell import Quickshell.Widgets import Quickshell.Services.SystemTray import qs.Commons -import qs.Services +import qs.Services.UI import qs.Widgets import qs.Modules.MainScreen diff --git a/Modules/Panels/Tray/TrayMenuPanel.qml b/Modules/Panels/Tray/TrayMenuPanel.qml index e842f161..2a3f31cb 100644 --- a/Modules/Panels/Tray/TrayMenuPanel.qml +++ b/Modules/Panels/Tray/TrayMenuPanel.qml @@ -3,9 +3,9 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services import qs.Widgets import qs.Modules.MainScreen +import qs.Services.UI SmartPanel { id: root @@ -17,7 +17,9 @@ SmartPanel { property int widgetIndex: -1 // Internal - readonly property int menuWidth: 280 + property int menuWidth: 280 + readonly property int minMenuWidth: 280 + readonly property int maxMenuWidth: 600 preferredWidth: menuWidth // Height is content-driven via panelContent @@ -52,8 +54,111 @@ SmartPanel { QsMenuOpener { id: opener menu: root.menu + onChildrenChanged: Qt.callLater(() => calculateMenuWidth(opener)) } + // Text metrics for measuring text widths + TextMetrics { + id: textMetrics + font.pointSize: Style.fontSizeS + } + + // Watch for submenu changes + onActiveSubMenuChanged: { + if (activeSubMenu && inPlaceSubmenu) { + Qt.callLater(() => { + // Find the subMenuOpener in the loaded component + if (inPlaceSubMenuLoader.item) { + const flickable = inPlaceSubMenuLoader.item.children[1] // Flickable + if (flickable && flickable.children.length > 0) { + const columnLayout = flickable.children[0] + if (columnLayout && columnLayout.children.length > 0) { + const subOpener = columnLayout.children[0] + if (subOpener) { + calculateMenuWidth(subOpener) + } + } + } + } + }) + } else if (!activeSubMenu) { + // Reset to main menu width when submenu closes + Qt.callLater(() => calculateMenuWidth(opener)) + } + } + + // Calculate required menu width based on menu entries + function calculateMenuWidth(menuOpener) { + let maxWidth = root.minMenuWidth + + // For submenus, also measure the "Back" button + if (menuOpener !== opener && content.inPlaceSubmenu) { + textMetrics.text = I18n.tr("settings.bar.tray.back") + const backWidth = (Style.marginM * 4) + Style.fontSizeS + Style.marginS + textMetrics.width + maxWidth = Math.max(maxWidth, backWidth) + } + + // Check all menu entries + if (menuOpener && menuOpener.children) { + try { + const entries = menuOpener.children.values ? [...menuOpener.children.values] : [] + + for (var i = 0; i < entries.length; i++) { + const entry = entries[i] + if (!entry || entry.isSeparator) + continue + + const text = entry.text || "" + if (text === "") + continue + + // Measure the text + textMetrics.text = text.replace(/[\n\r]+/g, ' ') + const textWidth = textMetrics.width + + // Calculate total width: + // - Outer inset: Style.marginM * 2 (parent width reduction) + // - RowLayout margins: Style.marginM * 2 (left + right) + // - Text width + // - Spacing: Style.marginS + // - Icon: Style.marginL (if present) + // - Submenu arrow: ~20px (if present) + let requiredWidth = (Style.marginM * 4) + textWidth + + if (entry.icon && entry.icon !== "") { + requiredWidth += Style.marginS + Style.marginL + } + + if (entry.hasChildren) { + requiredWidth += Style.marginS + 20 + } + + maxWidth = Math.max(maxWidth, requiredWidth) + } + } catch (e) { + + // Silently ignore errors during width calculation + } + } + + // Check pin/unpin button (only for main menu) + if (menuOpener === opener && root.trayItem !== null && root.widgetSection !== "" && root.widgetIndex >= 0) { + textMetrics.text = I18n.tr("settings.bar.tray.pin-application") + let pinWidth = (Style.marginM * 4) + textMetrics.width + Style.marginS + 20 + maxWidth = Math.max(maxWidth, pinWidth) + + textMetrics.text = I18n.tr("settings.bar.tray.unpin-application") + let unpinWidth = (Style.marginM * 4) + textMetrics.width + Style.marginS + 20 + maxWidth = Math.max(maxWidth, unpinWidth) + } + + // Clamp to min/max and apply + const finalWidth = Math.min(root.maxMenuWidth, Math.max(root.minMenuWidth, Math.ceil(maxWidth))) + root.menuWidth = finalWidth + } + + Component.onCompleted: Qt.callLater(() => calculateMenuWidth(opener)) + Component { id: subMenuComponent @@ -103,7 +208,7 @@ SmartPanel { id: subMenuFlickable anchors.fill: parent contentHeight: subMenuColumnLayout.implicitHeight - interactive: true + interactive: false z: 0 ColumnLayout { @@ -114,6 +219,7 @@ SmartPanel { QsMenuOpener { id: subMenuOpener menu: content.activeSubMenu + onChildrenChanged: Qt.callLater(() => content.calculateMenuWidth(subMenuOpener)) } // Back button (only shown when submenu is in-place) @@ -312,7 +418,7 @@ SmartPanel { Layout.preferredWidth: root.menuWidth Layout.fillHeight: true contentHeight: mainColumnLayout.implicitHeight - interactive: true + interactive: false visible: !(content.activeSubMenu !== null && content.inPlaceSubmenu) ColumnLayout { diff --git a/Modules/Panels/Wallpaper/WallpaperPanel.qml b/Modules/Panels/Wallpaper/WallpaperPanel.qml index 60e2cd41..d2fc547f 100644 --- a/Modules/Panels/Wallpaper/WallpaperPanel.qml +++ b/Modules/Panels/Wallpaper/WallpaperPanel.qml @@ -2,12 +2,11 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import Quickshell -import Quickshell.Wayland import qs.Commons -import qs.Modules.Panels.Settings -import qs.Services -import qs.Widgets import qs.Modules.MainScreen +import qs.Modules.Panels.Settings +import qs.Services.UI +import qs.Widgets import "../../../Helpers/FuzzySort.js" as FuzzySort SmartPanel { diff --git a/Modules/Panels/WiFi/WiFiPanel.qml b/Modules/Panels/WiFi/WiFiPanel.qml index accb5782..178e010f 100644 --- a/Modules/Panels/WiFi/WiFiPanel.qml +++ b/Modules/Panels/WiFi/WiFiPanel.qml @@ -2,16 +2,15 @@ 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.MainScreen +import qs.Services.Networking +import qs.Widgets SmartPanel { id: root - preferredWidth: Math.round(340 * Style.uiScaleRatio) + preferredWidth: Math.round(400 * Style.uiScaleRatio) preferredHeight: Math.round(500 * Style.uiScaleRatio) property string passwordSsid: "" @@ -150,7 +149,9 @@ SmartPanel { text: I18n.tr("wifi.panel.enable-message") pointSize: Style.fontSizeS color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap } Item { diff --git a/Modules/Toast/SimpleToast.qml b/Modules/Toast/SimpleToast.qml index 947dae17..9dbd62be 100644 --- a/Modules/Toast/SimpleToast.qml +++ b/Modules/Toast/SimpleToast.qml @@ -16,8 +16,8 @@ Item { signal hidden - width: 480 - height: Math.round(contentLayout.implicitHeight + Style.marginL * 4) + width: Math.round(420 * Style.uiScaleRatio + Style.marginM * 1.5 * 2) + height: Math.round(contentLayout.implicitHeight + Style.marginM * 3 * 2) visible: true opacity: 0 scale: initialScale @@ -26,7 +26,7 @@ Item { Rectangle { id: background anchors.fill: parent - anchors.margins: Style.marginL + anchors.margins: Style.marginM * 1.5 radius: Style.radiusL color: Color.mSurface @@ -47,6 +47,7 @@ Item { NDropShadows { anchors.fill: background source: background + autoPaddingEnabled: true } Behavior on opacity { diff --git a/Modules/Toast/ToastOverlay.qml b/Modules/Toast/ToastOverlay.qml index 3051108d..db5dbe56 100644 --- a/Modules/Toast/ToastOverlay.qml +++ b/Modules/Toast/ToastOverlay.qml @@ -3,7 +3,6 @@ import QtQuick.Controls import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Services import qs.Widgets Variants { diff --git a/Modules/Toast/ToastScreen.qml b/Modules/Toast/ToastScreen.qml index 70b1f38b..e782a3ce 100644 --- a/Modules/Toast/ToastScreen.qml +++ b/Modules/Toast/ToastScreen.qml @@ -1,10 +1,8 @@ import QtQuick -import QtQuick.Controls import Quickshell import Quickshell.Wayland import qs.Commons -import qs.Services -import qs.Widgets +import qs.Services.UI Item { id: root @@ -45,11 +43,11 @@ Item { function enqueueToast(toastData) { // Safe logging - fix the substring bug var descPreview = (toastData.description || "").substring(0, 100).replace(/\n/g, " ") - Logger.i("ToastScreen", "Queuing", toastData.type, ":", toastData.message, descPreview) + Logger.d("ToastScreen", "Queuing", toastData.type, ":", toastData.message, descPreview) // Bounded queue to prevent unbounded memory growth if (messageQueue.length >= maxQueueSize) { - Logger.i("ToastScreen", "Queue full, dropping oldest toast") + Logger.d("ToastScreen", "Queue full, dropping oldest toast") messageQueue.shift() } @@ -135,73 +133,68 @@ Item { screen: root.screen - readonly property string location: (Settings.data.notifications && Settings.data.notifications.location) ? Settings.data.notifications.location : "top_right" - readonly property bool isTop: (location === "top") || (location.length >= 3 && location.substring(0, 3) === "top") - readonly property bool isBottom: (location === "bottom") || (location.length >= 6 && location.substring(0, 6) === "bottom") - readonly property bool isLeft: location.indexOf("_left") >= 0 - readonly property bool isRight: location.indexOf("_right") >= 0 - readonly property bool isCentered: (location === "top" || location === "bottom") + // Parse location setting + readonly property string location: Settings.data.notifications?.location || "top_right" + readonly property bool isTop: location.startsWith("top") + readonly property bool isBottom: location.startsWith("bottom") + readonly property bool isLeft: location.endsWith("_left") + readonly property bool isRight: location.endsWith("_right") + readonly property bool isCentered: location === "top" || location === "bottom" - // Anchor selection based on location (window edges) + readonly property string barPos: Settings.data.bar.position + readonly property bool isFloating: Settings.data.bar.floating + + // Calculate bar offsets for each edge separately + readonly property int barOffsetTop: { + if (barPos !== "top") + return 0 + const floatMarginV = isFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 + return Style.barHeight + floatMarginV + } + + readonly property int barOffsetBottom: { + if (barPos !== "bottom") + return 0 + const floatMarginV = isFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 + return Style.barHeight + floatMarginV + } + + readonly property int barOffsetLeft: { + if (barPos !== "left") + return 0 + const floatMarginH = isFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + return floatMarginH + } + + readonly property int barOffsetRight: { + if (barPos !== "right") + return 0 + const floatMarginH = isFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + return floatMarginH + } + + // Anchoring anchors.top: isTop anchors.bottom: isBottom anchors.left: isLeft anchors.right: isRight - // Margins depending on bar position and chosen location - margins.top: { - if (!(anchors.top)) - return 0 - var base = Style.marginM - if (Settings.data.bar.position === "top") { - var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0 - return (Style.barHeight) + base + floatExtraV - } - return base - } + // Margins for PanelWindow - only apply bar offset for the specific edge where the bar is + margins.top: isTop ? barOffsetTop : 0 + margins.bottom: isBottom ? barOffsetBottom : 0 + margins.left: isLeft ? barOffsetLeft : 0 + margins.right: isRight ? barOffsetRight : 0 - margins.bottom: { - if (!(anchors.bottom)) - return 0 - var base = Style.marginM - if (Settings.data.bar.position === "bottom") { - var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0 - return (Style.barHeight) + base + floatExtraV - } - return base - } - - margins.left: { - if (!(anchors.left)) - return 0 - var base = Style.marginM - if (Settings.data.bar.position === "left") { - var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 - return (Style.barHeight) + base + floatExtraH - } - return base - } - - margins.right: { - if (!(anchors.right)) - return 0 - var base = Style.marginM - if (Settings.data.bar.position === "right") { - var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 - return (Style.barHeight) + base + floatExtraH - } - return base - } - - implicitWidth: Math.round(toastItem.width + Style.marginL * 2) - implicitHeight: Math.round(toastItem.height + Style.marginL * 2) + implicitWidth: Math.round(toastItem.width) + implicitHeight: Math.round(toastItem.height) color: Color.transparent WlrLayershell.layer: (Settings.data.notifications && Settings.data.notifications.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - exclusionMode: PanelWindow.ExclusionMode.Ignore + WlrLayershell.exclusionMode: ExclusionMode.Ignore + //mask: Region {} function showToast(message, description, icon, type, duration) { toastItem.show(message, description, icon, type, duration) } @@ -212,8 +205,6 @@ Item { SimpleToast { id: toastItem - - anchors.horizontalCenter: parent.horizontalCenter onHidden: root.onToastHidden() } } diff --git a/Modules/Tooltip/Tooltip.qml b/Modules/Tooltip/Tooltip.qml index f30306d3..584c1529 100644 --- a/Modules/Tooltip/Tooltip.qml +++ b/Modules/Tooltip/Tooltip.qml @@ -1,8 +1,7 @@ import QtQuick -import QtQuick.Layouts +// import QtQuick.Layouts import Quickshell import qs.Commons -import qs.Services import qs.Widgets PopupWindow { diff --git a/README.md b/README.md index f096654d..1b7bec4c 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ While all donations are greatly appreciated, they are completely voluntary. * MrDowntempo * Tempus Thales * Raine +* JustCurtis --- diff --git a/Services/AppThemeService.qml b/Services/AppThemeService.qml deleted file mode 100644 index a5460bcb..00000000 --- a/Services/AppThemeService.qml +++ /dev/null @@ -1,507 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Commons -import qs.Services -import "../Helpers/ColorsConvert.js" as ColorsConvert - -Singleton { - id: root - - readonly property string colorsApplyScript: Quickshell.shellDir + '/Bin/colors-apply.sh' - readonly property string dynamicConfigPath: Settings.cacheDir + "matugen.dynamic.toml" - readonly property var terminalPaths: ({ - "foot": "~/.config/foot/themes/noctalia", - "ghostty": "~/.config/ghostty/themes/noctalia", - "kitty": "~/.config/kitty/themes/noctalia.conf", - "alacritty": "~/.config/alacritty/themes/noctalia.toml", - "wezterm": "~/.config/wezterm/colors/Noctalia.toml" - }) - - readonly property var schemeNameMap: ({ - "Noctalia (default)": "Noctalia-default", - "Noctalia (legacy)": "Noctalia-legacy", - "Tokyo Night": "Tokyo-Night" - }) - readonly property var predefinedTemplateConfigs: ({ - "gtk": { - "input": "gtk.css", - "outputs": [{ - "path": "~/.config/gtk-3.0/gtk.css" - }, { - "path": "~/.config/gtk-4.0/gtk.css" - }], - "postProcess": mode => `gsettings set org.gnome.desktop.interface color-scheme prefer-${mode}\n` - }, - "qt": { - "input": "qtct.conf", - "outputs": [{ - "path": "~/.config/qt5ct/colors/noctalia.conf" - }, { - "path": "~/.config/qt6ct/colors/noctalia.conf" - }] - }, - "kcolorscheme": { - "input": "kcolorscheme.colors", - "outputs": [{ - "path": "~/.local/share/color-schemes/noctalia.colors" - }] - }, - "fuzzel": { - "input": "fuzzel.conf", - "outputs": [{ - "path": "~/.config/fuzzel/themes/noctalia" - }], - "postProcess": () => `${colorsApplyScript} fuzzel\n` - }, - "pywalfox": { - "input": "pywalfox.json", - "outputs": [{ - "path": "~/.cache/wal/colors.json" - }], - "postProcess": () => `${colorsApplyScript} pywalfox\n` - }, - "discord_vesktop": { - "input": "vesktop.css", - "outputs": [{ - "path": "~/.config/vesktop/themes/noctalia.theme.css" - }] - }, - "discord_webcord": { - "input": "vesktop.css", - "outputs": [{ - "path": "~/.config/webcord/themes/noctalia.theme.css" - }] - }, - "discord_armcord": { - "input": "vesktop.css", - "outputs": [{ - "path": "~/.config/armcord/themes/noctalia.theme.css" - }] - }, - "discord_equibop": { - "input": "vesktop.css", - "outputs": [{ - "path": "~/.config/equibop/themes/noctalia.theme.css" - }] - }, - "discord_lightcord": { - "input": "vesktop.css", - "outputs": [{ - "path": "~/.config/lightcord/themes/noctalia.theme.css" - }] - }, - "discord_dorion": { - "input": "vesktop.css", - "outputs": [{ - "path": "~/.config/dorion/themes/noctalia.theme.css" - }] - }, - "discord_vencord": { - "input": "vesktop.css", - "outputs": [{ - "path": "~/.config/discord/themes/noctalia.theme.css" - }] - }, - "vicinae": { - "input": "vicinae.toml", - "outputs": [{ - "path": "~/.local/share/vicinae/themes/matugen.toml" - }], - "postProcess": () => `cp -n ${Quickshell.shellDir}/Assets/noctalia.svg ~/.local/share/vicinae/themes/noctalia.svg && ${colorsApplyScript} vicinae\n` - }, - "walker": { - "input": "walker.css", - "outputs": [{ - "path": "~/.config/walker/themes/noctalia/style.css" - }], - "postProcess": () => `${colorsApplyScript} walker\n`, - "strict": true - }, - "code": { - "input": "code.json", - "outputs": [{ - "path": "~/.vscode/extensions/hyprluna.hyprluna-theme-1.0.2/themes/hyprluna.json" - }] - } - }) - - Connections { - target: WallpaperService - function onWallpaperChanged(screenName, path) { - if (screenName === Screen.name && Settings.data.colorSchemes.useWallpaperColors) { - generateFromWallpaper() - } - } - } - - Connections { - target: Settings.data.colorSchemes - function onDarkModeChanged() { - Logger.i("AppThemeService", "Detected dark mode change") - AppThemeService.generate() - } - } - - // -------------------------------------------------------------------------------- - function init() { - Logger.i("AppThemeService", "Service started") - } - - // -------------------------------------------------------------------------------- - function generate() { - if (Settings.data.colorSchemes.useWallpaperColors) { - generateFromWallpaper() - } else { - // Re-apply the scheme, this is the best way to regenerate all templates too. - ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme) - } - } - - // -------------------------------------------------------------------------------- - // Wallpaper Colors Generation - // -------------------------------------------------------------------------------- - function generateFromWallpaper() { - - // Logger.i("AppThemeService", "Generating from wallpaper on screen:", Screen.name) - const wp = WallpaperService.getWallpaper(Screen.name).replace(/'/g, "'\\''") - if (!wp) { - Logger.e("AppThemeService", "No wallpaper found") - return - } - - const content = MatugenTemplates.buildConfigToml() - if (!content) - return - - const mode = Settings.data.colorSchemes.darkMode ? "dark" : "light" - const script = buildMatugenScript(content, wp, mode) - - generateProcess.command = ["bash", "-lc", script] - generateProcess.running = true - } - - function buildMatugenScript(content, wallpaper, mode) { - const delimiter = "MATUGEN_CONFIG_EOF_" + Math.random().toString(36).substr(2, 9) - const pathEsc = dynamicConfigPath.replace(/'/g, "'\\''") - - let script = `cat > '${pathEsc}' << '${delimiter}'\n${content}\n${delimiter}\n` - script += `matugen image '${wallpaper}' --config '${pathEsc}' --mode ${mode} --type ${Settings.data.colorSchemes.matugenSchemeType}` - script += buildUserTemplateCommand(wallpaper, mode) - - return script + "\n" - } - - // -------------------------------------------------------------------------------- - // Predefined Scheme Generation - // For predefined color schemes, we bypass matugen's generation which do not gives good results. - // Instead, we use 'sed' to apply a custom palette to the existing matugen templates. - // -------------------------------------------------------------------------------- - function generateFromPredefinedScheme(schemeData) { - Logger.i("AppThemeService", "Generating templates from predefined color scheme") - - handleTerminalThemes() - - const mode = Settings.data.colorSchemes.darkMode ? "dark" : "light" - const colors = schemeData[mode] - let script = processAllTemplates(colors, mode) - - // Add user templates if enabled - script += buildUserTemplateCommandForPredefined(schemeData, mode) - - generateProcess.command = ["bash", "-lc", script] - generateProcess.running = true - } - - function generatePalette(colors, isDarkMode, isStrict) { - const c = hex => ({ - "default": { - "hex": hex, - "hex_stripped": hex.replace(/^#/, "") - } - }) - - // Generate container colors - const primaryContainer = ColorsConvert.generateContainerColor(colors.mPrimary, isDarkMode) - const secondaryContainer = ColorsConvert.generateContainerColor(colors.mSecondary, isDarkMode) - const tertiaryContainer = ColorsConvert.generateContainerColor(colors.mTertiary, isDarkMode) - - // Generate "on" colors - const onPrimary = ColorsConvert.generateOnColor(colors.mPrimary, isDarkMode) - const onSecondary = ColorsConvert.generateOnColor(colors.mSecondary, isDarkMode) - const onTertiary = ColorsConvert.generateOnColor(colors.mTertiary, isDarkMode) - - const onPrimaryContainer = ColorsConvert.generateOnColor(primaryContainer, isDarkMode) - const onSecondaryContainer = ColorsConvert.generateOnColor(secondaryContainer, isDarkMode) - const onTertiaryContainer = ColorsConvert.generateOnColor(tertiaryContainer, isDarkMode) - - // Generate error colors (standard red-based) - const errorContainer = ColorsConvert.generateContainerColor(colors.mError, isDarkMode) - const onError = ColorsConvert.generateOnColor(colors.mError, isDarkMode) - const onErrorContainer = ColorsConvert.generateOnColor(errorContainer, isDarkMode) - - // Surface is same as background in Material Design 3 - const surface = colors.mSurface - const onSurface = isStrict ? colors.mOnSurface : ColorsConvert.generateOnColor(colors.mSurface, isDarkMode) - - // Generate surface variant (slightly different tone) - const surfaceVariant = isStrict ? colors.mSurfaceVariant : ColorsConvert.adjustLightness(colors.mSurface, isDarkMode ? 5 : -3) - const onSurfaceVariant = isStrict ? colors.mOnSurfaceVariant : ColorsConvert.generateOnColor(surfaceVariant, isDarkMode) - - // Generate surface containers (progressive elevation) - const surfaceContainerLowest = ColorsConvert.generateSurfaceVariant(surface, 0, isDarkMode) - const surfaceContainerLow = ColorsConvert.generateSurfaceVariant(surface, 1, isDarkMode) - const surfaceContainer = ColorsConvert.generateSurfaceVariant(surface, 2, isDarkMode) - const surfaceContainerHigh = ColorsConvert.generateSurfaceVariant(surface, 3, isDarkMode) - const surfaceContainerHighest = ColorsConvert.generateSurfaceVariant(surface, 4, isDarkMode) - - // Generate outline colors (for borders/dividers) - const outline = isStrict ? colors.mOutline : ColorsConvert.adjustLightnessAndSaturation(colors.mOnSurface, isDarkMode ? -30 : 30, isDarkMode ? -30 : 30) - const outlineVariant = ColorsConvert.adjustLightness(outline, isDarkMode ? -20 : 20) - - // Shadow is always pitch black - const shadow = "#000000" - - return { - "primary": c(colors.mPrimary), - "on_primary": c(onPrimary), - "primary_container": c(primaryContainer), - "on_primary_container": c(onPrimaryContainer), - "secondary": c(colors.mSecondary), - "on_secondary": c(onSecondary), - "secondary_container": c(secondaryContainer), - "on_secondary_container": c(onSecondaryContainer), - "tertiary": c(colors.mTertiary), - "on_tertiary": c(onTertiary), - "tertiary_container": c(tertiaryContainer), - "on_tertiary_container": c(onTertiaryContainer), - "error": c(colors.mError), - "on_error": c(onError), - "error_container": c(errorContainer), - "on_error_container": c(onErrorContainer), - "background": c(surface), - "on_background": c(onSurface), - "surface": c(surface), - "on_surface": c(onSurface), - "surface_variant": c(surfaceVariant), - "on_surface_variant": c(onSurfaceVariant), - "surface_container_lowest": c(surfaceContainerLowest), - "surface_container_low": c(surfaceContainerLow), - "surface_container": c(surfaceContainer), - "surface_container_high": c(surfaceContainerHigh), - "surface_container_highest": c(surfaceContainerHighest), - "outline": c(outline), - "outline_variant": c(outlineVariant), - "shadow": c(shadow) - } - } - - function processAllTemplates(colors, mode) { - let script = "" - const homeDir = Quickshell.env("HOME") - - Object.keys(predefinedTemplateConfigs).forEach(appName => { - if (Settings.data.templates[appName]) { - script += processTemplate(appName, colors, mode, homeDir) - } - }) - - return script - } - - function processTemplate(appName, colors, mode, homeDir) { - const config = predefinedTemplateConfigs[appName] - const templatePath = `${Quickshell.shellDir}/Assets/MatugenTemplates/${config.input}` - let script = "" - - const palette = generatePalette(colors, Settings.data.colorSchemes.darkMode, config.strict || false) - - config.outputs.forEach(output => { - - const outputPath = output.path.replace("~", homeDir) - const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/')) - - // For Discord clients, check if the base config directory exists - if (appName.startsWith("discord_")) { - const baseConfigDir = outputDir.replace("/themes", "") - script += `if [ -d "${baseConfigDir}" ]; then\n` - script += ` mkdir -p ${outputDir}\n` - script += ` cp '${templatePath}' '${outputPath}'\n` - script += ` ${replaceColorsInFile(outputPath, palette)}\n` - script += `else\n` - script += ` echo "Discord client ${appName} not found at ${baseConfigDir}, skipping theme generation"\n` - script += `fi\n` - } else { - script += `mkdir -p ${outputDir}\n` - script += `cp '${templatePath}' '${outputPath}'\n` - script += replaceColorsInFile(outputPath, palette) - } - }) - - if (config.postProcess) { - script += config.postProcess(mode) - } - - return script - } - - function replaceColorsInFile(filePath, colors) { - // This handle both ".hex" and ".hex_stripped" the EXACT same way. Our predefined color schemes are - // always RRGGBB without alpha so this is fine and keeps compatibility with matugen. - let script = "" - Object.keys(colors).forEach(colorKey => { - const colorValue = colors[colorKey].default.hex - const escapedColor = colorValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - script += `sed -i 's/{{colors\\.${colorKey}\\.default\\.hex\\(_stripped\\)\\?}}/${escapedColor}/g' '${filePath}'\n` - }) - return script - } - - // -------------------------------------------------------------------------------- - // Terminal Themes - // -------------------------------------------------------------------------------- - function handleTerminalThemes() { - const commands = [] - - Object.keys(terminalPaths).forEach(terminal => { - if (Settings.data.templates[terminal]) { - const outputPath = terminalPaths[terminal] - const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/')) - const templatePath = getTerminalColorsTemplate(terminal) - - commands.push(`mkdir -p ${outputDir}`) - commands.push(`cp -f ${templatePath} ${outputPath}`) - commands.push(`${colorsApplyScript} ${terminal}`) - } - }) - - if (commands.length > 0) { - copyProcess.command = ["bash", "-lc", commands.join('; ')] - copyProcess.running = true - } - } - - function getTerminalColorsTemplate(terminal) { - let colorScheme = Settings.data.colorSchemes.predefinedScheme - const mode = Settings.data.colorSchemes.darkMode ? 'dark' : 'light' - - colorScheme = schemeNameMap[colorScheme] || colorScheme - let extension = "" - if (terminal === 'kitty') { - extension = ".conf" - } else if (terminal === 'wezterm') { - extension = ".toml" - } - - return `${Quickshell.shellDir}/Assets/ColorScheme/${colorScheme}/terminal/${terminal}/${colorScheme}-${mode}${extension}` - } - - // -------------------------------------------------------------------------------- - // User Templates - // -------------------------------------------------------------------------------- - function buildUserTemplateCommand(input, mode) { - if (!Settings.data.templates.enableUserTemplates) { - return "" - } - - const userConfigPath = getUserConfigPath() - let script = "\n# Execute user config if it exists\n" - script += `if [ -f '${userConfigPath}' ]; then\n` - script += ` matugen image '${input}' --config '${userConfigPath}' --mode ${mode} --type ${Settings.data.colorSchemes.matugenSchemeType}\n` - script += "fi" - - return script - } - - function buildUserTemplateCommandForPredefined(schemeData, mode) { - if (!Settings.data.templates.enableUserTemplates) { - return "" - } - - const userConfigPath = getUserConfigPath() - const isDarkMode = Settings.data.colorSchemes.darkMode - const colors = schemeData[mode] - - // Generate the matugen palette JSON - const palette = generatePalette(colors, isDarkMode, false) - - // Create a temporary JSON file with the color palette - const tempJsonPath = Settings.cacheDir + "predefined-colors.json" - const homeDir = Quickshell.env("HOME") - const tempJsonPathEsc = tempJsonPath.replace(/'/g, "'\\''") - - let script = "\n# Execute user templates with predefined scheme colors\n" - script += `if [ -f '${userConfigPath}' ]; then\n` - - // Write the color palette to a temp JSON file - script += ` cat > '${tempJsonPathEsc}' << 'EOF'\n` - script += JSON.stringify({ - "colors": palette - }, null, 2) + "\n" - script += "EOF\n" - - // Use matugen json subcommand with the color palette - script += ` matugen json '${tempJsonPathEsc}' --config '${userConfigPath}' --mode ${mode}\n` - script += "fi" - - return script - } - - function getUserConfigPath() { - return (Settings.configDir + "user-templates.toml").replace(/'/g, "'\\''") - } - - // -------------------------------------------------------------------------------- - // Processes - // -------------------------------------------------------------------------------- - Process { - id: generateProcess - workingDirectory: Quickshell.shellDir - running: false - - onExited: function (exitCode) { - if (exitCode !== 0) { - // Capture error output from stderr first, then stdout, or use fallback message - const errorMsg = (stderr.text && stderr.text.trim() !== "") ? stderr.text.trim() : ((stdout.text && stdout.text.trim() !== "") ? stdout.text.trim() : I18n.tr("toast.matugen.failed-general")) - Logger.e("AppThemeService", "Matugen process failed with exit code:", exitCode) - if (stderr.text && stderr.text.trim() !== "") { - Logger.e("AppThemeService", "Matugen stderr:", stderr.text) - } - if (stdout.text && stdout.text.trim() !== "") { - Logger.e("AppThemeService", "Matugen stdout:", stdout.text) - } - ToastService.showError(I18n.tr("toast.matugen.failed"), errorMsg, 6000) - } - } - - stdout: StdioCollector { - onStreamFinished: { - if (this.text) { - Logger.d("AppThemeService", "GenerateProcess stdout:", this.text) - } - } - } - stderr: StdioCollector { - onStreamFinished: { - if (this.text) { - Logger.d("AppThemeService", "GenerateProcess stderr:", this.text) - } - } - } - } - - Process { - id: copyProcess - workingDirectory: Quickshell.shellDir - running: false - stderr: StdioCollector { - onStreamFinished: { - if (this.text) { - Logger.d("AppThemeService", "CopyProcess stderr:", this.text) - } - } - } - } -} diff --git a/Services/CavaService.qml b/Services/CavaService.qml deleted file mode 100644 index 16324bc7..00000000 --- a/Services/CavaService.qml +++ /dev/null @@ -1,87 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Commons - -Singleton { - id: root - - - /** - * Cava runs if: - * - Bar has an audio visualizer - * - LockScreen is opened - * - A control center is open - */ - property bool shouldRun: BarService.hasAudioVisualizer || PanelService.lockScreen?.active || (PanelService.openedPanel && PanelService.openedPanel.objectName.startsWith("controlCenterPanel")) - - property var values: Array(barsCount).fill(0) - property int barsCount: 48 - property var config: ({ - "general": { - "bars": barsCount, - "framerate": Settings.data.audio.cavaFrameRate, - "autosens": 1, - "sensitivity": 100, - "lower_cutoff_freq": 50, - "higher_cutoff_freq": 12000 - }, - "smoothing": { - "monstercat": 0, - "noise_reduction": 77 - }, - "output": { - "method": "raw", - "data_format": "ascii", - "ascii_max_range": 100, - "bit_format": "8bit", - "channels": "mono", - "mono_option": "average" - } - }) - - Process { - id: process - stdinEnabled: true - running: root.shouldRun - command: ["cava", "-p", "/dev/stdin"] - onRunningChanged: { - Logger.d("Cava", "Process running:", running) - } - onExited: { - Logger.i("Cava", "Process exited") - stdinEnabled = true - values = Array(barsCount).fill(0) - } - onStarted: { - Logger.i("Cava", "Process started") - for (const k in config) { - if (typeof config[k] !== "object") { - write(k + "=" + config[k] + "\n") - continue - } - write("[" + k + "]\n") - const obj = config[k] - for (const k2 in obj) { - write(k2 + "=" + obj[k2] + "\n") - } - } - stdinEnabled = false - values = Array(barsCount).fill(0) - } - stdout: SplitParser { - onRead: data => { - root.values = data.slice(0, -1).split(";").map(v => parseInt(v, 10) / 100) - } - } - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - Logger.w("Cava", "Error", text) - } - } - } - } -} diff --git a/Services/CompositorService.qml b/Services/Compositor/CompositorService.qml similarity index 99% rename from Services/CompositorService.qml rename to Services/Compositor/CompositorService.qml index cd4b16b1..0ce10ffa 100644 --- a/Services/CompositorService.qml +++ b/Services/Compositor/CompositorService.qml @@ -4,7 +4,7 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.UI Singleton { id: root diff --git a/Services/HyprlandService.qml b/Services/Compositor/HyprlandService.qml similarity index 100% rename from Services/HyprlandService.qml rename to Services/Compositor/HyprlandService.qml diff --git a/Services/NiriService.qml b/Services/Compositor/NiriService.qml similarity index 100% rename from Services/NiriService.qml rename to Services/Compositor/NiriService.qml diff --git a/Services/SwayService.qml b/Services/Compositor/SwayService.qml similarity index 100% rename from Services/SwayService.qml rename to Services/Compositor/SwayService.qml diff --git a/Services/HooksService.qml b/Services/Control/HooksService.qml similarity index 98% rename from Services/HooksService.qml rename to Services/Control/HooksService.qml index db9f5e17..646cfbfd 100644 --- a/Services/HooksService.qml +++ b/Services/Control/HooksService.qml @@ -3,7 +3,7 @@ pragma Singleton import QtQuick import Quickshell import qs.Commons -import qs.Services +import qs.Services.UI Singleton { id: root diff --git a/Services/IPCService.qml b/Services/Control/IPCService.qml similarity index 96% rename from Services/IPCService.qml rename to Services/Control/IPCService.qml index a6cac693..eea7eea0 100644 --- a/Services/IPCService.qml +++ b/Services/Control/IPCService.qml @@ -3,8 +3,15 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Widgets + import qs.Commons -import qs.Services +import qs.Services.Compositor +import qs.Services.Hardware +import qs.Services.Media +import qs.Services.Power +import qs.Services.System +import qs.Services.Theming +import qs.Services.UI Item { id: root @@ -55,7 +62,13 @@ Item { }) } function toggleDND() { - Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb + NotificationService.doNotDisturb = !NotificationService.doNotDisturb + } + function enableDND() { + NotificationService.doNotDisturb = true + } + function disableDND() { + NotificationService.doNotDisturb = false } function clear() { NotificationService.clearHistory() diff --git a/Services/BatteryService.qml b/Services/Hardware/BatteryService.qml similarity index 98% rename from Services/BatteryService.qml rename to Services/Hardware/BatteryService.qml index 14b831c9..a655be64 100644 --- a/Services/BatteryService.qml +++ b/Services/Hardware/BatteryService.qml @@ -4,7 +4,7 @@ import Quickshell import Quickshell.Io import Quickshell.Services.UPower import qs.Commons -import qs.Services +import qs.Services.UI Singleton { id: root @@ -123,7 +123,7 @@ Singleton { Settings.data.battery.chargingMode = BatteryService.chargingMode } else if (exitCode === 2) { ToastService.showWarning(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.initial-setup")) - PanelService.getPanel("batteryPanel", screen)?.toggle(this) + PanelService.getPanel("batteryPanel", null)?.toggle(this) BatteryService.runInstaller() } else { ToastService.showError(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.set-failed")) diff --git a/Services/BrightnessService.qml b/Services/Hardware/BrightnessService.qml similarity index 100% rename from Services/BrightnessService.qml rename to Services/Hardware/BrightnessService.qml diff --git a/Services/ClipboardService.qml b/Services/Keyboard/ClipboardService.qml similarity index 100% rename from Services/ClipboardService.qml rename to Services/Keyboard/ClipboardService.qml diff --git a/Services/KeyboardLayoutService.qml b/Services/Keyboard/KeyboardLayoutService.qml similarity index 58% rename from Services/KeyboardLayoutService.qml rename to Services/Keyboard/KeyboardLayoutService.qml index ed0b4f8b..a28ff993 100644 --- a/Services/KeyboardLayoutService.qml +++ b/Services/Keyboard/KeyboardLayoutService.qml @@ -3,9 +3,8 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Io -import Quickshell.Hyprland import qs.Commons -import qs.Services +import qs.Services.Compositor Singleton { id: root @@ -187,7 +186,7 @@ Singleton { } // Check for exact matches or partial matches in language map from Commons - const entries = Object.entries(KeyboardLayout.languageMap) + const entries = Object.entries(languageMap) for (var i = 0; i < entries.length; i++) { const lang = entries[i][0] const code = entries[i][1] @@ -230,4 +229,202 @@ Singleton { } } } + + // Comprehensive language name to ISO code mapping + property var languageMap: { + "english"// English variants + : "us", + "american": "us", + "united states": "us", + "us english": "us", + "british": "gb", + "uk": "ua", + "united kingdom"// FIXED: Ukrainian language code should map to Ukraine + : "gb", + "english (uk)": "gb", + "canadian": "ca", + "canada": "ca", + "canadian english": "ca", + "australian": "au", + "australia": "au", + "swedish"// Nordic countries + : "se", + "svenska": "se", + "sweden": "se", + "norwegian": "no", + "norsk": "no", + "norway": "no", + "danish": "dk", + "dansk": "dk", + "denmark": "dk", + "finnish": "fi", + "suomi": "fi", + "finland": "fi", + "icelandic": "is", + "íslenska": "is", + "iceland": "is", + "german"// Western/Central European Germanic + : "de", + "deutsch": "de", + "germany": "de", + "austrian": "at", + "austria": "at", + "österreich": "at", + "swiss": "ch", + "switzerland": "ch", + "schweiz": "ch", + "suisse": "ch", + "dutch": "nl", + "nederlands": "nl", + "netherlands": "nl", + "holland": "nl", + "belgian": "be", + "belgium": "be", + "belgië": "be", + "belgique": "be", + "french"// Romance languages (Western/Southern Europe) + : "fr", + "français": "fr", + "france": "fr", + "canadian french": "ca", + "spanish": "es", + "español": "es", + "spain": "es", + "castilian": "es", + "italian": "it", + "italiano": "it", + "italy": "it", + "portuguese": "pt", + "português": "pt", + "portugal": "pt", + "catalan": "ad", + "català": "ad", + "andorra": "ad", + "romanian"// Eastern European Romance + : "ro", + "română": "ro", + "romania": "ro", + "russian"// Slavic languages (Eastern Europe) + : "ru", + "русский": "ru", + "russia": "ru", + "polish": "pl", + "polski": "pl", + "poland": "pl", + "czech": "cz", + "čeština": "cz", + "czech republic": "cz", + "slovak": "sk", + "slovenčina": "sk", + "slovakia": "sk", + "uk": "ua", + "ukrainian"// Ukrainian language code + : "ua", + "українська": "ua", + "ukraine": "ua", + "bulgarian": "bg", + "български": "bg", + "bulgaria": "bg", + "serbian": "rs", + "srpski": "rs", + "serbia": "rs", + "croatian": "hr", + "hrvatski": "hr", + "croatia": "hr", + "slovenian": "si", + "slovenščina": "si", + "slovenia": "si", + "bosnian": "ba", + "bosanski": "ba", + "bosnia": "ba", + "macedonian": "mk", + "македонски": "mk", + "macedonia": "mk", + "irish"// Celtic languages (Western Europe) + : "ie", + "gaeilge": "ie", + "ireland": "ie", + "welsh": "gb", + "cymraeg": "gb", + "wales": "gb", + "scottish": "gb", + "gàidhlig": "gb", + "scotland": "gb", + "estonian"// Baltic languages (Northern Europe) + : "ee", + "eesti": "ee", + "estonia": "ee", + "latvian": "lv", + "latviešu": "lv", + "latvia": "lv", + "lithuanian": "lt", + "lietuvių": "lt", + "lithuania": "lt", + "hungarian"// Other European languages + : "hu", + "magyar": "hu", + "hungary": "hu", + "greek": "gr", + "ελληνικά": "gr", + "greece": "gr", + "albanian": "al", + "shqip": "al", + "albania": "al", + "maltese": "mt", + "malti": "mt", + "malta": "mt", + "turkish"// West/Southwest Asian languages + : "tr", + "türkçe": "tr", + "turkey": "tr", + "arabic": "ar", + "العربية": "ar", + "arab": "ar", + "hebrew": "il", + "עברית": "il", + "israel": "il", + "brazilian"// South American languages + : "br", + "brazilian portuguese": "br", + "brasil": "br", + "brazil": "br", + "japanese"// East Asian languages + : "jp", + "日本語": "jp", + "japan": "jp", + "korean": "kr", + "한국어": "kr", + "korea": "kr", + "south korea": "kr", + "chinese": "cn", + "中文": "cn", + "china": "cn", + "simplified chinese": "cn", + "traditional chinese": "tw", + "taiwan": "tw", + "繁體中文": "tw", + "thai"// Southeast Asian languages + : "th", + "ไทย": "th", + "thailand": "th", + "vietnamese": "vn", + "tiếng việt": "vn", + "vietnam": "vn", + "hindi"// South Asian languages + : "in", + "हिन्दी": "in", + "india": "in", + "afrikaans"// African languages + : "za", + "south africa": "za", + "south african": "za", + "qwerty"// Layout variants + : "us", + "dvorak": "us", + "colemak": "us", + "workman": "us", + "azerty": "fr", + "norman": "fr", + "qwertz": "de" + } } diff --git a/Services/LockKeysService.qml b/Services/Keyboard/LockKeysService.qml similarity index 100% rename from Services/LockKeysService.qml rename to Services/Keyboard/LockKeysService.qml diff --git a/Services/CalendarService.qml b/Services/Location/CalendarService.qml similarity index 100% rename from Services/CalendarService.qml rename to Services/Location/CalendarService.qml diff --git a/Services/DarkModeService.qml b/Services/Location/DarkModeService.qml similarity index 99% rename from Services/DarkModeService.qml rename to Services/Location/DarkModeService.qml index ad3eef00..e7c3315c 100644 --- a/Services/DarkModeService.qml +++ b/Services/Location/DarkModeService.qml @@ -3,7 +3,6 @@ pragma Singleton import QtQuick import Quickshell import qs.Commons -import qs.Services Singleton { id: root diff --git a/Services/LocationService.qml b/Services/Location/LocationService.qml similarity index 99% rename from Services/LocationService.qml rename to Services/Location/LocationService.qml index 7c2df5d4..775c9616 100644 --- a/Services/LocationService.qml +++ b/Services/Location/LocationService.qml @@ -4,7 +4,6 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons -import qs.Services // Weather logic and caching with stable UI properties Singleton { diff --git a/Services/NightLightService.qml b/Services/Location/NightLightService.qml similarity index 99% rename from Services/NightLightService.qml rename to Services/Location/NightLightService.qml index 864ae87a..e25c828a 100644 --- a/Services/NightLightService.qml +++ b/Services/Location/NightLightService.qml @@ -4,7 +4,7 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.UI Singleton { id: root diff --git a/Services/MatugenTemplates.qml b/Services/MatugenTemplates.qml deleted file mode 100644 index ad9bf6a6..00000000 --- a/Services/MatugenTemplates.qml +++ /dev/null @@ -1,306 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Commons - -// Central place to define which templates we generate and where they write. -// Users can extend it by dropping additional templates into: -// - Assets/MatugenTemplates/ (built-in templates) -// - ~/.config/matugen/ (user-defined templates when enableUserTemplates is true) -// User templates are automatically executed after the main matugen command -// if enableUserTemplates is enabled in settings. -Singleton { - id: root - - // Build the base TOML using current settings - function buildConfigToml() { - var lines = [] - var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light" - - if (Settings.data.colorSchemes.useWallpaperColors) { - addWallpaperBasedTemplates(lines, mode) - } - - addApplicationTemplates(lines, mode) - - if (lines.length > 0) { - const config = ["[config]"].concat(lines) - return config.join("\n") + "\n" - } - - return "" - } - - // Build user templates TOML for ~/.config/noctalia/user-templates.toml - function buildUserTemplatesToml() { - var lines = [] - lines.push("[config]") - lines.push("") - lines.push("[templates]") - lines.push("") - lines.push("# User-defined templates") - lines.push("# Add your custom templates below") - lines.push("# Example:") - lines.push("# [templates.myapp]") - lines.push("# input_path = \"~/.config/noctalia/templates/myapp.css\"") - lines.push("# output_path = \"~/.config/myapp/theme.css\"") - lines.push("# post_hook = \"myapp --reload-theme\"") - lines.push("") - lines.push("# Remove this section and add your own templates") - lines.push("#[templates.placeholder]") - lines.push("#input_path = \"" + Quickshell.shellDir + "/Assets/MatugenTemplates/noctalia.json\"") - lines.push("#output_path = \"" + Settings.cacheDir + "placeholder.json\"") - lines.push("") - - return lines.join("\n") + "\n" - } - - // Write user templates TOML to ~/.config/noctalia/user-templates.toml - function writeUserTemplatesToml() { - var userConfigPath = Settings.configDir + "user-templates.toml" - - // Check if file already exists - fileCheckProcess.command = ["test", "-f", userConfigPath] - fileCheckProcess.running = true - } - - function doWriteUserTemplatesToml() { - var userConfigPath = Settings.configDir + "user-templates.toml" - var configContent = buildUserTemplatesToml() - - // Ensure directory exists (should already exist but just in case) - Quickshell.execDetached(["mkdir", "-p", Settings.configDir]) - - // Write the config file - Quickshell.execDetached(["sh", "-c", `echo '${configContent.replace(/'/g, "'\\''")}' > '${userConfigPath}'`]) - - Logger.d("MatugenTemplates", "User templates config written to:", userConfigPath) - } - - // -------------------------------- - function addWallpaperBasedTemplates(lines, mode) { - // Noctalia colors - lines.push("[templates.noctalia]") - lines.push('input_path = "' + Quickshell.shellDir + '/Assets/MatugenTemplates/noctalia.json"') - lines.push('output_path = "' + Settings.configDir + 'colors.json"') - - // Terminal templates (only for wallpaper-based colors) - addTerminalTemplates(lines) - } - - // -------------------------------- - function addTerminalTemplates(lines) { - var terminals = [{ - "name": "foot", - "path": "Terminal/foot", - "output": "~/.config/foot/themes/noctalia" - }, { - "name": "ghostty", - "path": "Terminal/ghostty", - "output": "~/.config/ghostty/themes/noctalia", - "post_hook": "pkill -SIGUSR2 ghostty" - }, { - "name": "kitty", - "path": "Terminal/kitty.conf", - "output": "~/.config/kitty/themes/noctalia.conf" - }, { - "name": "alacritty", - "path": "Terminal/alacritty.toml", - "output": "~/.config/alacritty/themes/noctalia.toml" - }, { - "name": "wezterm", - "path": "Terminal/wezterm.toml", - "output": "~/.config/wezterm/colors/Noctalia.toml", - "post_hook": "touch ~/.config/wezterm/wezterm.lua" - }] - - terminals.forEach(function (terminal) { - if (Settings.data.templates[terminal.name]) { - lines.push("\n[templates." + terminal.name + "]") - lines.push('input_path = "' + Quickshell.shellDir + '/Assets/MatugenTemplates/' + terminal.path + '"') - lines.push('output_path = "' + terminal.output + '"') - lines.push('post_hook = "' + AppThemeService.colorsApplyScript + " " + terminal.name + '"') - } - }) - } - - // Applications configuration - readonly property var applications: [{ - "name": "gtk", - "templates": [{ - "version": "gtk3", - "output": "~/.config/gtk-3.0/gtk.css" - }, { - "version": "gtk4", - "output": "~/.config/gtk-4.0/gtk.css" - }], - "input": "gtk.css", - "postHook": "gsettings set org.gnome.desktop.interface color-scheme prefer-{mode}" - }, { - "name": "qt", - "templates": [{ - "version": "qt5", - "output": "~/.config/qt5ct/colors/noctalia.conf" - }, { - "version": "qt6", - "output": "~/.config/qt6ct/colors/noctalia.conf" - }], - "input": "qtct.conf" - }, { - "name": "kcolorscheme", - "templates": [{ - "version": "kcolorscheme", - "output": "~/.local/share/color-schemes/noctalia.colors" - }], - "input": "kcolorscheme.colors" - }, { - "name": "fuzzel", - "templates": [{ - "version": "fuzzel", - "output": "~/.config/fuzzel/themes/noctalia" - }], - "input": "fuzzel.conf", - "postHook": AppThemeService.colorsApplyScript + " fuzzel" - }, { - "name": "vicinae", - "templates": [{ - "version": "vicinae", - "output": "~/.local/share/vicinae/themes/matugen.toml" - }], - "input": "vicinae.toml", - "postHook": "cp -n " + Quickshell.shellDir + "/Assets/noctalia.svg ~/.local/share/vicinae/themes/noctalia.svg && " + AppThemeService.colorsApplyScript + " vicinae" - }, { - "name": "walker", - "templates": [{ - "version": "walker", - "output": "~/.config/walker/themes/noctalia/style.css" - }], - "input": "walker.css" - }, { - "name": "pywalfox", - "templates": [{ - "version": "pywalfox", - "output": "~/.cache/wal/colors.json" - }], - "input": "pywalfox.json", - "postHook": AppThemeService.colorsApplyScript + " pywalfox" - }, { - "name": "discord_vesktop", - "templates": [{ - "version": "discord_vesktop", - "output": "~/.config/vesktop/themes/noctalia.theme.css" - }], - "input": "vesktop.css" - }, { - "name": "discord_webcord", - "templates": [{ - "version": "discord_webcord", - "output": "~/.config/webcord/themes/noctalia.theme.css" - }], - "input": "vesktop.css" - }, { - "name": "discord_armcord", - "templates": [{ - "version": "discord_armcord", - "output": "~/.config/armcord/themes/noctalia.theme.css" - }], - "input": "vesktop.css" - }, { - "name": "discord_equibop", - "templates": [{ - "version": "discord_equibop", - "output": "~/.config/equibop/themes/noctalia.theme.css" - }], - "input": "vesktop.css" - }, { - "name": "discord_lightcord", - "templates": [{ - "version": "discord_lightcord", - "output": "~/.config/lightcord/themes/noctalia.theme.css" - }], - "input": "vesktop.css" - }, { - "name": "discord_dorion", - "templates": [{ - "version": "discord_dorion", - "output": "~/.config/dorion/themes/noctalia.theme.css" - }], - "input": "vesktop.css" - }, { - "name": "discord_vencord", - "templates": [{ - "version": "discord_vencord", - "output": "~/.config/discord/themes/noctalia.theme.css" - }], - "input": "vesktop.css", - "requiresThemesFolder": true - }, { - "name": "code", - "templates": [{ - "version": "code", - "output": "~/.vscode/extensions/hyprluna.hyprluna-theme-1.0.2/themes/hyprluna.json" - }], - "input": "code.json" - }] - - // -------------------------------- - function addApplicationTemplates(lines, mode) { - applications.forEach(function (app) { - // Check if app has a condition and if it's met - var shouldInclude = true - if (app.condition !== undefined) { - shouldInclude = app.condition - } - - if (Settings.data.templates[app.name] && shouldInclude) { - app.templates.forEach(function (template) { - lines.push("\n[templates." + template.version + "]") - lines.push('input_path = "' + Quickshell.shellDir + '/Assets/MatugenTemplates/' + app.input + '"') - lines.push('output_path = "' + template.output + '"') - if (app.postHook) { - var postHook = app.postHook.replace("{mode}", mode) - lines.push('post_hook = "' + postHook + '"') - } - }) - } - }) - } - - // Extract Discord clients from applications array - readonly property var discordClients: { - var clients = [] - for (var i = 0; i < applications.length; i++) { - var app = applications[i] - if (app.name && app.name.startsWith("discord_")) { - var clientName = app.name.replace("discord_", "") - var themePath = app.templates[0].output - var configPath = themePath.replace("/themes/noctalia.theme.css", "") - clients.push({ - "name": clientName, - "configPath": configPath, - "themePath": themePath, - "requiresThemesFolder": app.requiresThemesFolder || false - }) - } - } - return clients - } - - // Process for checking if user templates file exists - Process { - id: fileCheckProcess - running: false - - onExited: function (exitCode) { - if (exitCode === 0) { - // File exists, skip creation - Logger.d("MatugenTemplates", "User templates config already exists, skipping creation") - } else { - // File doesn't exist, create it - doWriteUserTemplatesToml() - } - } - } -} diff --git a/Services/AudioService.qml b/Services/Media/AudioService.qml similarity index 100% rename from Services/AudioService.qml rename to Services/Media/AudioService.qml diff --git a/Services/Media/CavaService.qml b/Services/Media/CavaService.qml new file mode 100644 index 00000000..8e58c92e --- /dev/null +++ b/Services/Media/CavaService.qml @@ -0,0 +1,151 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI + +Singleton { + id: root + + + /** + * Cava runs if: + * - Bar has an audio visualizer + * - LockScreen is opened + * - A control center is open + */ + property bool shouldRun: BarService.hasAudioVisualizer || PanelService.lockScreen?.active || (PanelService.openedPanel && PanelService.openedPanel.objectName.startsWith("controlCenterPanel")) + + property var values: Array(barsCount).fill(0) + property int barsCount: 32 + + // Idle detection to reduce GPU usage when there's no audio + property bool isIdle: true + property int idleFrameCount: 0 + readonly property int idleThreshold: 30 // Frames of silence before considered idle (0.5s at 60fps) + + // Frame skipping for GPU optimization - skip every Nth frame when playing + property int frameCounter: 0 + readonly property int frameSkip: 1 // Update every N frames (1 = no skip, 2 = every other frame, etc) + + // Change detection - only update if values changed significantly + property var lastValues: Array(barsCount).fill(0) + readonly property real changeThreshold: 0.005 // Minimum change (1%) to trigger update + property var config: ({ + "general": { + "bars": barsCount, + "framerate": Settings.data.audio.cavaFrameRate, + "autosens": 1, + "sensitivity": 100, + "lower_cutoff_freq": 50, + "higher_cutoff_freq": 12000 + }, + "smoothing": { + "monstercat": 1, + "noise_reduction"// Enable monstercat smoothing for less jittery animation + : 77 + }, + "output": { + "method": "raw", + "data_format": "ascii", + "ascii_max_range": 100, + "bit_format": "8bit", + "channels": "mono", + "mono_option": "average" + } + }) + + Process { + id: process + stdinEnabled: true + running: root.shouldRun + command: ["cava", "-p", "/dev/stdin"] + onRunningChanged: { + Logger.d("Cava", "Process running:", running) + } + onExited: { + Logger.i("Cava", "Process exited") + stdinEnabled = true + values = Array(barsCount).fill(0) + } + onStarted: { + Logger.i("Cava", "Process started") + for (const k in config) { + if (typeof config[k] !== "object") { + write(k + "=" + config[k] + "\n") + continue + } + write("[" + k + "]\n") + const obj = config[k] + for (const k2 in obj) { + write(k2 + "=" + obj[k2] + "\n") + } + } + stdinEnabled = false + values = Array(barsCount).fill(0) + } + stdout: SplitParser { + onRead: data => { + const newValues = data.slice(0, -1).split(";").map(v => parseInt(v, 10) / 100) + + // Check if all values are effectively zero (< 0.01) + const allZero = newValues.every(v => v < 0.01) + + if (allZero) { + root.idleFrameCount++ + if (root.idleFrameCount >= root.idleThreshold) { + // We're idle - stop updating values to save GPU + if (!root.isIdle) { + root.isIdle = true + // Set all values to 0 one final time + root.values = Array(root.barsCount).fill(0) + root.lastValues = Array(root.barsCount).fill(0) + Logger.d("Cava", "Idle detected - stopped rendering") + } + // Don't update values while idle + return + } + } else { + // Audio detected - resume updates + root.idleFrameCount = 0 + if (root.isIdle) { + root.isIdle = false + Logger.d("Cava", "Audio detected - resumed rendering") + } + } + + // Frame skipping optimization - skip frames to reduce GPU load + root.frameCounter++ + if (root.frameSkip > 1 && root.frameCounter % root.frameSkip !== 0) { + // Skip this frame + return + } + + // Change detection - only update if values changed significantly + let hasSignificantChange = false + for (var i = 0; i < newValues.length; i++) { + const delta = Math.abs(newValues[i] - root.lastValues[i]) + if (delta > root.changeThreshold) { + hasSignificantChange = true + break + } + } + + // Update values only if there's a significant change + if (hasSignificantChange) { + root.lastValues = newValues.slice() // Copy array + root.values = newValues + } + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.w("Cava", "Error", text) + } + } + } + } +} diff --git a/Services/MediaService.qml b/Services/Media/MediaService.qml similarity index 99% rename from Services/MediaService.qml rename to Services/Media/MediaService.qml index b96cafbd..be8bab1e 100644 --- a/Services/MediaService.qml +++ b/Services/Media/MediaService.qml @@ -4,7 +4,6 @@ import QtQuick import Quickshell import Quickshell.Services.Mpris import qs.Commons -import qs.Services Singleton { id: root diff --git a/Services/ScreenRecorderService.qml b/Services/Media/ScreenRecorderService.qml similarity index 95% rename from Services/ScreenRecorderService.qml rename to Services/Media/ScreenRecorderService.qml index c2e09fc4..7338ccbb 100644 --- a/Services/ScreenRecorderService.qml +++ b/Services/Media/ScreenRecorderService.qml @@ -4,7 +4,8 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.System +import qs.Services.UI Singleton { id: root @@ -113,7 +114,7 @@ Singleton { // Check if gpu-screen-recorder is not installed const stdout = String(recorderProcess.stdout.text || "").trim() if (stdout === "GPU_SCREEN_RECORDER_NOT_INSTALLED") { - ToastService.showError(I18n.tr("toast.recording.not-installed"), I18n.tr("toast.recording.not-installed-desc"), 7000) + ToastService.showError(I18n.tr("toast.recording.not-installed"), I18n.tr("toast.recording.not-installed-desc")) return } @@ -121,9 +122,9 @@ Singleton { if (exitCode !== 0) { const err = String(recorderProcess.stderr.text || "").trim() if (err.length > 0) - ToastService.showError(I18n.tr("toast.recording.failed-start"), err, 7000) + ToastService.showError(I18n.tr("toast.recording.failed-start"), err) else - ToastService.showError(I18n.tr("toast.recording.failed-start"), I18n.tr("toast.recording.failed-gpu"), 7000) + ToastService.showError(I18n.tr("toast.recording.failed-start"), I18n.tr("toast.recording.failed-gpu")) } } else if (isRecording) { // Process ended normally while recording @@ -135,9 +136,9 @@ Singleton { } else { const err2 = String(recorderProcess.stderr.text || "").trim() if (err2.length > 0) - ToastService.showError(I18n.tr("toast.recording.failed-start"), err2, 7000) + ToastService.showError(I18n.tr("toast.recording.failed-start"), err2) else - ToastService.showError(I18n.tr("toast.recording.failed-start"), I18n.tr("toast.recording.failed-general"), 7000) + ToastService.showError(I18n.tr("toast.recording.failed-start"), I18n.tr("toast.recording.failed-general")) } } } @@ -153,7 +154,7 @@ Singleton { } else { isPending = false hasActiveRecording = false - ToastService.showError(I18n.tr("toast.recording.no-portals"), I18n.tr("toast.recording.no-portals-desc"), 8000) + ToastService.showError(I18n.tr("toast.recording.no-portals"), I18n.tr("toast.recording.no-portals-desc")) } } } diff --git a/Services/BluetoothService.qml b/Services/Networking/BluetoothService.qml similarity index 99% rename from Services/BluetoothService.qml rename to Services/Networking/BluetoothService.qml index 0ab4a30f..4a7f8745 100644 --- a/Services/BluetoothService.qml +++ b/Services/Networking/BluetoothService.qml @@ -5,7 +5,7 @@ import Quickshell import Quickshell.Bluetooth import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.UI Singleton { id: root diff --git a/Services/NetworkService.qml b/Services/Networking/NetworkService.qml similarity index 99% rename from Services/NetworkService.qml rename to Services/Networking/NetworkService.qml index 142d300a..b9bbe84a 100644 --- a/Services/NetworkService.qml +++ b/Services/Networking/NetworkService.qml @@ -4,7 +4,7 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.UI Singleton { id: root @@ -516,9 +516,9 @@ Singleton { return cmd } } - environment: { - "LC_ALL": "C" - } + environment: ({ + "LC_ALL": "C" + }) stdout: StdioCollector { onStreamFinished: { diff --git a/Services/GitHubService.qml b/Services/Noctalia/GitHubService.qml similarity index 99% rename from Services/GitHubService.qml rename to Services/Noctalia/GitHubService.qml index 1a2efc8a..6f7c1bf4 100644 --- a/Services/GitHubService.qml +++ b/Services/Noctalia/GitHubService.qml @@ -4,7 +4,6 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons -import qs.Services // GitHub API logic and caching Singleton { diff --git a/Services/UpdateService.qml b/Services/Noctalia/UpdateService.qml similarity index 93% rename from Services/UpdateService.qml rename to Services/Noctalia/UpdateService.qml index d36e57ea..5cbcba5e 100644 --- a/Services/UpdateService.qml +++ b/Services/Noctalia/UpdateService.qml @@ -8,7 +8,7 @@ Singleton { id: root // Public properties - property string baseVersion: "3.0.2" + property string baseVersion: "3.0.5" property bool isDevelopment: true property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}` diff --git a/Services/IdleInhibitorService.qml b/Services/Power/IdleInhibitorService.qml similarity index 99% rename from Services/IdleInhibitorService.qml rename to Services/Power/IdleInhibitorService.qml index 941e2f3d..80aaba3f 100644 --- a/Services/IdleInhibitorService.qml +++ b/Services/Power/IdleInhibitorService.qml @@ -4,7 +4,7 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.UI Singleton { id: root diff --git a/Services/PowerProfileService.qml b/Services/Power/PowerProfileService.qml similarity index 95% rename from Services/PowerProfileService.qml rename to Services/Power/PowerProfileService.qml index 160e5213..f086e9c1 100644 --- a/Services/PowerProfileService.qml +++ b/Services/Power/PowerProfileService.qml @@ -4,7 +4,7 @@ import QtQuick import Quickshell import Quickshell.Services.UPower import qs.Commons -import qs.Services +import qs.Services.UI Singleton { id: root @@ -112,9 +112,9 @@ Singleton { onNoctaliaPerformanceModeChanged: { if (noctaliaPerformanceMode) { - ToastService.showNotice(I18n.tr("toast.noctalia-performance.label"), I18n.tr("toast.noctalia-performance.enabled"), "seedling") + ToastService.showNotice(I18n.tr("toast.noctalia-performance.label"), I18n.tr("toast.noctalia-performance.enabled"), "rocket") } else { - ToastService.showNotice(I18n.tr("toast.noctalia-performance.label"), I18n.tr("toast.noctalia-performance.disabled"), "seedling-off") + ToastService.showNotice(I18n.tr("toast.noctalia-performance.label"), I18n.tr("toast.noctalia-performance.disabled"), "rocket-off") } } } diff --git a/Services/DistroService.qml b/Services/System/DistroService.qml similarity index 100% rename from Services/DistroService.qml rename to Services/System/DistroService.qml diff --git a/Services/FontService.qml b/Services/System/FontService.qml similarity index 100% rename from Services/FontService.qml rename to Services/System/FontService.qml diff --git a/Services/NotificationService.qml b/Services/System/NotificationService.qml similarity index 60% rename from Services/NotificationService.qml rename to Services/System/NotificationService.qml index c943d875..277938a1 100644 --- a/Services/NotificationService.qml +++ b/Services/System/NotificationService.qml @@ -6,8 +6,9 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Notifications import qs.Commons -import qs.Services -import "../Helpers/sha256.js" as Checksum +import qs.Services.Power +import qs.Services.UI +import "../../Helpers/sha256.js" as Checksum Singleton { id: root @@ -20,18 +21,18 @@ Singleton { // State property real lastSeenTs: 0 + // Volatile property that doesn't persist to settings (similar to noctaliaPerformanceMode) + property bool doNotDisturb: false // Models property ListModel activeList: ListModel {} property ListModel historyList: ListModel {} // Internal state - property var activeMap: ({}) + property var activeNotifications: ({}) // Maps internal ID to {notification, watcher, metadata} + property var quickshellIdToInternalId: ({}) property var imageQueue: [] - // Performance optimization: Track notification metadata separately - property var notificationMetadata: ({}) // Stores timestamp and duration for each notification - PanelWindow { implicitHeight: 1 implicitWidth: 1 @@ -76,7 +77,7 @@ Singleton { } } - // Notification server - only created when notifications are enabled + // Notification server property var notificationServerLoader: null Component { @@ -89,14 +90,43 @@ Singleton { } } + Component { + id: notificationWatcherComponent + Connections { + property var targetNotification + property var targetDataId + target: targetNotification + + function onSummaryChanged() { + updateNotificationFromObject(targetDataId) + } + function onBodyChanged() { + updateNotificationFromObject(targetDataId) + } + function onAppNameChanged() { + updateNotificationFromObject(targetDataId) + } + function onUrgencyChanged() { + updateNotificationFromObject(targetDataId) + } + function onAppIconChanged() { + updateNotificationFromObject(targetDataId) + } + function onImageChanged() { + updateNotificationFromObject(targetDataId) + } + function onActionsChanged() { + updateNotificationFromObject(targetDataId) + } + } + } + function updateNotificationServer() { - // Destroy existing server if it exists if (notificationServerLoader) { notificationServerLoader.destroy() notificationServerLoader = null } - // Create server only if enabled if (Settings.isLoaded && Settings.data.notifications.enabled !== false) { notificationServerLoader = notificationServerComponent.createObject(root) } @@ -118,51 +148,137 @@ Singleton { } } + // Helper function to generate content-based ID for deduplication + function getContentId(summary, body, appName) { + return Checksum.sha256(JSON.stringify({ + "summary": summary || "", + "body": body || "", + "app": appName || "" + })) + } + // Main handler function handleNotification(notification) { + const quickshellId = notification.id const data = createData(notification) addToHistory(data) - if (Settings.data.notifications?.doNotDisturb || PowerProfileService.noctaliaPerformanceMode) + if (root.doNotDisturb || PowerProfileService.noctaliaPerformanceMode) return - activeMap[data.id] = notification + // Check if this is a replacement notification + const existingInternalId = quickshellIdToInternalId[quickshellId] + if (existingInternalId && activeNotifications[existingInternalId]) { + updateExistingNotification(existingInternalId, notification, data) + return + } + + // Check for duplicate content + const duplicateId = findDuplicateNotification(data) + if (duplicateId) { + removeNotification(duplicateId) + } + + // Add new notification + addNewNotification(quickshellId, notification, data) + } + + function updateExistingNotification(internalId, notification, data) { + const index = findNotificationIndex(internalId) + if (index < 0) + return + + const existing = activeList.get(index) + const oldTimestamp = existing.timestamp + const oldProgress = existing.progress + + // Update properties (keeping original timestamp and progress) + activeList.setProperty(index, "summary", data.summary) + activeList.setProperty(index, "body", data.body) + activeList.setProperty(index, "appName", data.appName) + activeList.setProperty(index, "urgency", data.urgency) + activeList.setProperty(index, "expireTimeout", data.expireTimeout) + activeList.setProperty(index, "originalImage", data.originalImage) + activeList.setProperty(index, "cachedImage", data.cachedImage) + activeList.setProperty(index, "actionsJson", data.actionsJson) + activeList.setProperty(index, "timestamp", oldTimestamp) + activeList.setProperty(index, "progress", oldProgress) + + // Update stored notification object + const notifData = activeNotifications[internalId] + notifData.notification = notification notification.tracked = true - notification.closed.connect(() => removeActive(data.id)) + notification.closed.connect(() => removeNotification(internalId)) - // Store metadata for efficient progress calculation - const durations = [Settings.data.notifications?.lowUrgencyDuration * 1000 || 3000, Settings.data.notifications?.normalUrgencyDuration * 1000 || 8000, Settings.data.notifications?.criticalUrgencyDuration * 1000 || 15000] + // Update metadata + notifData.metadata.urgency = data.urgency + notifData.metadata.duration = calculateDuration(data) + } - let expire = 0 - if (Settings.data.notifications?.respectExpireTimeout) { - if (data.expireTimeout === 0) { - expire = -1 // Never expire - } else if (data.expireTimeout > 0) { - expire = data.expireTimeout - } else { - expire = durations[data.urgency] + function addNewNotification(quickshellId, notification, data) { + // Map IDs + quickshellIdToInternalId[quickshellId] = data.id + + // Create watcher + const watcher = notificationWatcherComponent.createObject(root, { + "targetNotification": notification, + "targetDataId": data.id + }) + + // Store notification data + activeNotifications[data.id] = { + "notification": notification, + "watcher": watcher, + "metadata": { + "timestamp": data.timestamp.getTime(), + "duration": calculateDuration(data), + "urgency": data.urgency, + "paused": false, + "pauseTime": 0 } - } else { - expire = durations[data.urgency] } - notificationMetadata[data.id] = { - "timestamp": data.timestamp.getTime(), - "duration": expire, - "urgency": data.urgency, - "paused": false, - "pauseTime": 0 - } + notification.tracked = true + notification.closed.connect(() => removeNotification(data.id)) + // Add to list activeList.insert(0, data) + + // Remove overflow while (activeList.count > maxVisible) { const last = activeList.get(activeList.count - 1) - activeMap[last.id]?.dismiss() + activeNotifications[last.id]?.notification?.dismiss() activeList.remove(activeList.count - 1) - delete notificationMetadata[last.id] + cleanupNotification(last.id) } } + function findDuplicateNotification(data) { + const contentId = getContentId(data.summary, data.body, data.appName) + + for (var i = 0; i < activeList.count; i++) { + const existing = activeList.get(i) + const existingContentId = getContentId(existing.summary, existing.body, existing.appName) + if (existingContentId === contentId) { + return existing.id + } + } + return null + } + + function calculateDuration(data) { + const durations = [Settings.data.notifications?.lowUrgencyDuration * 1000 || 3000, Settings.data.notifications?.normalUrgencyDuration * 1000 || 8000, Settings.data.notifications?.criticalUrgencyDuration * 1000 || 15000] + + if (Settings.data.notifications?.respectExpireTimeout) { + if (data.expireTimeout === 0) + return -1 // Never expire + if (data.expireTimeout > 0) + return data.expireTimeout + } + + return durations[data.urgency] + } + function createData(n) { const time = new Date() const id = Checksum.sha256(JSON.stringify({ @@ -178,7 +294,7 @@ Singleton { return { "id": id, - "summary": (n.summary || ""), + "summary": n.summary || "", "body": stripTags(n.body || ""), "appName": getAppName(n.appName || n.desktopEntry || ""), "urgency": n.urgency < 0 || n.urgency > 2 ? 1 : n.urgency, @@ -194,6 +310,104 @@ Singleton { } } + function findNotificationIndex(internalId) { + for (var i = 0; i < activeList.count; i++) { + if (activeList.get(i).id === internalId) { + return i + } + } + return -1 + } + + function updateNotificationFromObject(internalId) { + const notifData = activeNotifications[internalId] + if (!notifData) + return + + const index = findNotificationIndex(internalId) + if (index < 0) + return + + const data = createData(notifData.notification) + const existing = activeList.get(index) + + // Update properties (keeping timestamp and progress) + activeList.setProperty(index, "summary", data.summary) + activeList.setProperty(index, "body", data.body) + activeList.setProperty(index, "appName", data.appName) + activeList.setProperty(index, "urgency", data.urgency) + activeList.setProperty(index, "expireTimeout", data.expireTimeout) + activeList.setProperty(index, "originalImage", data.originalImage) + activeList.setProperty(index, "cachedImage", data.cachedImage) + activeList.setProperty(index, "actionsJson", data.actionsJson) + + // Update metadata + notifData.metadata.urgency = data.urgency + notifData.metadata.duration = calculateDuration(data) + } + + function removeNotification(id) { + const index = findNotificationIndex(id) + if (index >= 0) { + activeList.remove(index) + } + cleanupNotification(id) + } + + function cleanupNotification(id) { + const notifData = activeNotifications[id] + if (notifData) { + notifData.watcher?.destroy() + delete activeNotifications[id] + } + + // Clean up quickshell ID mapping + for (const qsId in quickshellIdToInternalId) { + if (quickshellIdToInternalId[qsId] === id) { + delete quickshellIdToInternalId[qsId] + break + } + } + } + + // Progress updates + Timer { + interval: 50 + repeat: true + running: activeList.count > 0 + onTriggered: updateAllProgress() + } + + function updateAllProgress() { + const now = Date.now() + const toRemove = [] + + for (var i = 0; i < activeList.count; i++) { + const notif = activeList.get(i) + const notifData = activeNotifications[notif.id] + if (!notifData) + continue + + const meta = notifData.metadata + if (meta.duration === -1 || meta.paused) + continue + + const elapsed = now - meta.timestamp + const progress = Math.max(1.0 - (elapsed / meta.duration), 0.0) + + if (progress <= 0) { + toRemove.push(notif.id) + } else if (Math.abs(notif.progress - progress) > 0.005) { + activeList.setProperty(i, "progress", progress) + } + } + + if (toRemove.length > 0) { + animateAndRemove(toRemove[0]) + } + } + + // Image handling function queueImage(path, imageId) { if (!path || !path.startsWith("image://") || !imageId) return @@ -230,64 +444,6 @@ Singleton { } } - function removeActive(id) { - for (var i = 0; i < activeList.count; i++) { - if (activeList.get(i).id === id) { - activeList.remove(i) - delete activeMap[id] - delete notificationMetadata[id] - break - } - } - } - - // Optimized batch progress update - Timer { - interval: 50 // Reduced from 10ms to 50ms (20 updates/sec instead of 100) - repeat: true - running: activeList.count > 0 - onTriggered: updateAllProgress() - } - - function updateAllProgress() { - const now = Date.now() - const toRemove = [] - const updates = [] // Batch updates - - // Collect all updates first - for (var i = 0; i < activeList.count; i++) { - const notif = activeList.get(i) - const meta = notificationMetadata[notif.id] - - if (!meta || meta.duration === -1 || meta.paused) - continue - - // Skip infinite notifications - const elapsed = now - meta.timestamp - const progress = Math.max(1.0 - (elapsed / meta.duration), 0.0) - - if (progress <= 0) { - toRemove.push(notif.id) - } else if (Math.abs(notif.progress - progress) > 0.005) { - // Only update if change is significant - updates.push({ - "index": i, - "progress": progress - }) - } - } - - // Apply batch updates - for (const update of updates) { - activeList.setProperty(update.index, "progress", update.progress) - } - - // Remove expired notifications (one at a time to allow animation) - if (toRemove.length > 0) { - animateAndRemove(toRemove[0]) - } - } - // History management function addToHistory(data) { historyList.insert(0, data) @@ -319,7 +475,7 @@ Singleton { } } - // Persistence - State (lastSeenTs, etc.) + // Persistence - State FileView { id: stateFileView path: stateFile @@ -396,7 +552,6 @@ Singleton { 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() @@ -421,10 +576,10 @@ Singleton { saveState() } + // Utility functions function getAppName(name) { if (!name || name.trim() === "") return "Unknown" - name = name.trim() if (name.includes(".") && (name.startsWith("com.") || name.startsWith("org.") || name.startsWith("io.") || name.startsWith("net."))) { @@ -435,9 +590,8 @@ Singleton { appPart = parts[parts.length - 2] || parts[0] } - if (appPart) { + if (appPart) name = appPart - } } if (name.includes(".")) { @@ -495,25 +649,25 @@ Singleton { } function pauseTimeout(id) { - const meta = notificationMetadata[id] - if (meta && !meta.paused) { - meta.paused = true - meta.pauseTime = Date.now() + const notifData = activeNotifications[id] + if (notifData && !notifData.metadata.paused) { + notifData.metadata.paused = true + notifData.metadata.pauseTime = Date.now() } } function resumeTimeout(id) { - const meta = notificationMetadata[id] - if (meta && meta.paused) { - meta.timestamp += Date.now() - meta.pauseTime - meta.paused = false + const notifData = activeNotifications[id] + if (notifData && notifData.metadata.paused) { + notifData.metadata.timestamp += Date.now() - notifData.metadata.pauseTime + notifData.metadata.paused = false } } // Public API function dismissActiveNotification(id) { - activeMap[id]?.dismiss() - removeActive(id) + activeNotifications[id]?.notification?.dismiss() + removeNotification(id) } function dismissOldestActive() { @@ -524,18 +678,21 @@ Singleton { } function dismissAllActive() { - Object.values(activeMap).forEach(n => n.dismiss()) + for (const id in activeNotifications) { + activeNotifications[id].notification?.dismiss() + activeNotifications[id].watcher?.destroy() + } activeList.clear() - activeMap = {} - notificationMetadata = {} + activeNotifications = {} + quickshellIdToInternalId = {} } function invokeAction(id, actionId) { - const n = activeMap[id] - if (!n?.actions) + const notifData = activeNotifications[id] + if (!notifData?.notification?.actions) return false - for (const action of n.actions) { + for (const action of notifData.notification.actions) { if (action.identifier === actionId && action.invoke) { action.invoke() return true @@ -570,14 +727,10 @@ Singleton { saveHistory() } - // Signals & connections + // Signals signal animateAndRemove(string notificationId) - Connections { - target: Settings.data.notifications - function onDoNotDisturbChanged() { - const enabled = Settings.data.notifications.doNotDisturb - ToastService.showNotice(enabled ? I18n.tr("toast.do-not-disturb.enabled") : I18n.tr("toast.do-not-disturb.disabled"), enabled ? I18n.tr("toast.do-not-disturb.enabled-desc") : I18n.tr("toast.do-not-disturb.disabled-desc"), enabled ? "bell-off" : "bell") - } + onDoNotDisturbChanged: { + ToastService.showNotice(doNotDisturb ? I18n.tr("toast.do-not-disturb.enabled") : I18n.tr("toast.do-not-disturb.disabled"), doNotDisturb ? I18n.tr("toast.do-not-disturb.enabled-desc") : I18n.tr("toast.do-not-disturb.disabled-desc"), doNotDisturb ? "bell-off" : "bell") } } diff --git a/Services/ProgramCheckerService.qml b/Services/System/ProgramCheckerService.qml similarity index 92% rename from Services/ProgramCheckerService.qml rename to Services/System/ProgramCheckerService.qml index b18ef1ea..10a2eff0 100644 --- a/Services/ProgramCheckerService.qml +++ b/Services/System/ProgramCheckerService.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons +import qs.Services.Theming // Service to check if various programs are available on the system Singleton { @@ -25,6 +26,7 @@ Singleton { property bool app2unitAvailable: false property bool codeAvailable: false property bool gnomeCalendarAvailable: false + property bool spicetifyAvailable: false // Discord client auto-detection property var availableDiscordClients: [] @@ -37,8 +39,8 @@ Singleton { // Build shell script to check each client var scriptParts = ["available_clients=\"\";"] - for (var i = 0; i < MatugenTemplates.discordClients.length; i++) { - var client = MatugenTemplates.discordClients[i] + for (var i = 0; i < TemplateRegistry.discordClients.length; i++) { + var client = TemplateRegistry.discordClients[i] var clientName = client.name // Check if this client requires themes folder to exist @@ -73,8 +75,8 @@ Singleton { // Build list of available clients for (var i = 0; i < detectedClients.length; i++) { var clientName = detectedClients[i] - for (var j = 0; j < MatugenTemplates.discordClients.length; j++) { - var client = MatugenTemplates.discordClients[j] + for (var j = 0; j < TemplateRegistry.discordClients.length; j++) { + var client = TemplateRegistry.discordClients[j] if (client.name === clientName) { availableDiscordClients.push(client) break @@ -111,7 +113,8 @@ Singleton { "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"], "codeAvailable": ["which", "code"], - "gnomeCalendarAvailable": ["which", "gnome-calendar"] + "gnomeCalendarAvailable": ["which", "gnome-calendar"], + "spicetifyAvailable": ["which", "spicetify"] }) // Internal tracking @@ -199,8 +202,8 @@ Singleton { Logger.d("ProgramChecker", "HOME:", Quickshell.env("HOME")) // Test each client directory - for (var i = 0; i < MatugenTemplates.discordClients.length; i++) { - var client = MatugenTemplates.discordClients[i] + for (var i = 0; i < TemplateRegistry.discordClients.length; i++) { + var client = TemplateRegistry.discordClients[i] var configDir = client.configPath.replace("~", Quickshell.env("HOME")) Logger.d("ProgramChecker", "Checking:", configDir) } diff --git a/Services/SystemStatService.qml b/Services/System/SystemStatService.qml similarity index 100% rename from Services/SystemStatService.qml rename to Services/System/SystemStatService.qml diff --git a/Services/Theming/AppThemeService.qml b/Services/Theming/AppThemeService.qml new file mode 100644 index 00000000..5bbffc82 --- /dev/null +++ b/Services/Theming/AppThemeService.qml @@ -0,0 +1,61 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.UI + +Singleton { + id: root + + readonly property string colorsApplyScript: Quickshell.shellDir + '/Bin/colors-apply.sh' + + Connections { + target: WallpaperService + + // When the wallpaper changes, regenerate with Matugen if necessary + function onWallpaperChanged(screenName, path) { + if (screenName === Screen.name && Settings.data.colorSchemes.useWallpaperColors) { + generateFromWallpaper() + } + } + } + + Connections { + target: Settings.data.colorSchemes + function onDarkModeChanged() { + Logger.i("AppThemeService", "Detected dark mode change") + generate() + } + } + + // PUBLIC FUNCTIONS + function init() { + Logger.i("AppThemeService", "Service started") + } + + function generate() { + if (Settings.data.colorSchemes.useWallpaperColors) { + generateFromWallpaper() + } else { + // applyScheme will trigger template generation via schemeReader.onLoaded + ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme) + } + } + + function generateFromWallpaper() { + const wp = WallpaperService.getWallpaper(Screen.name) + if (!wp) { + Logger.e("AppThemeService", "No wallpaper found") + return + } + const mode = Settings.data.colorSchemes.darkMode ? "dark" : "light" + TemplateProcessor.processWallpaperColors(wp, mode) + } + + function generateFromPredefinedScheme(schemeData) { + Logger.i("AppThemeService", "Generating templates from predefined color scheme") + const mode = Settings.data.colorSchemes.darkMode ? "dark" : "light" + TemplateProcessor.processPredefinedScheme(schemeData, mode) + } +} diff --git a/Services/Theming/ColorPaletteGenerator.qml b/Services/Theming/ColorPaletteGenerator.qml new file mode 100644 index 00000000..343b1758 --- /dev/null +++ b/Services/Theming/ColorPaletteGenerator.qml @@ -0,0 +1,100 @@ +pragma Singleton + +import QtQuick +import Quickshell +import "../../Helpers/ColorsConvert.js" as ColorsConvert + +Singleton { + id: root + + + /** + * Generate Material Design 3 color palette from base colors + * @param colors - Object with mPrimary, mSecondary, mTertiary, mError, mSurface, etc. + * @param isDarkMode - Boolean indicating dark or light mode + * @param isStrict - Boolean; if true, use mSurfaceVariant/mOnSurfaceVariant/mOutline directly + * @returns Object with all MD3 color roles in matugen format + */ + function generatePalette(colors, isDarkMode, isStrict) { + const c = hex => ({ + "default": { + "hex": hex, + "hex_stripped": hex.replace(/^#/, "") + } + }) + + // Generate container colors + const primaryContainer = ColorsConvert.generateContainerColor(colors.mPrimary, isDarkMode) + const secondaryContainer = ColorsConvert.generateContainerColor(colors.mSecondary, isDarkMode) + const tertiaryContainer = ColorsConvert.generateContainerColor(colors.mTertiary, isDarkMode) + + // Generate "on" colors + const onPrimary = ColorsConvert.generateOnColor(colors.mPrimary, isDarkMode) + const onSecondary = ColorsConvert.generateOnColor(colors.mSecondary, isDarkMode) + const onTertiary = ColorsConvert.generateOnColor(colors.mTertiary, isDarkMode) + + const onPrimaryContainer = ColorsConvert.generateOnColor(primaryContainer, isDarkMode) + const onSecondaryContainer = ColorsConvert.generateOnColor(secondaryContainer, isDarkMode) + const onTertiaryContainer = ColorsConvert.generateOnColor(tertiaryContainer, isDarkMode) + + // Generate error colors (standard red-based) + const errorContainer = ColorsConvert.generateContainerColor(colors.mError, isDarkMode) + const onError = ColorsConvert.generateOnColor(colors.mError, isDarkMode) + const onErrorContainer = ColorsConvert.generateOnColor(errorContainer, isDarkMode) + + // Surface is same as background in Material Design 3 + const surface = colors.mSurface + const onSurface = isStrict ? colors.mOnSurface : ColorsConvert.generateOnColor(colors.mSurface, isDarkMode) + + // Generate surface variant (slightly different tone) + const surfaceVariant = isStrict ? colors.mSurfaceVariant : ColorsConvert.adjustLightness(colors.mSurface, isDarkMode ? 5 : -3) + const onSurfaceVariant = isStrict ? colors.mOnSurfaceVariant : ColorsConvert.generateOnColor(surfaceVariant, isDarkMode) + + // Generate surface containers (progressive elevation) + const surfaceContainerLowest = ColorsConvert.generateSurfaceVariant(surface, 0, isDarkMode) + const surfaceContainerLow = ColorsConvert.generateSurfaceVariant(surface, 1, isDarkMode) + const surfaceContainer = ColorsConvert.generateSurfaceVariant(surface, 2, isDarkMode) + const surfaceContainerHigh = ColorsConvert.generateSurfaceVariant(surface, 3, isDarkMode) + const surfaceContainerHighest = ColorsConvert.generateSurfaceVariant(surface, 4, isDarkMode) + + // Generate outline colors (for borders/dividers) + const outline = isStrict ? colors.mOutline : ColorsConvert.adjustLightnessAndSaturation(colors.mOnSurface, isDarkMode ? -30 : 30, isDarkMode ? -30 : 30) + const outlineVariant = ColorsConvert.adjustLightness(outline, isDarkMode ? -20 : 20) + + // Shadow is always pitch black + const shadow = "#000000" + + return { + "primary": c(colors.mPrimary), + "on_primary": c(onPrimary), + "primary_container": c(primaryContainer), + "on_primary_container": c(onPrimaryContainer), + "secondary": c(colors.mSecondary), + "on_secondary": c(onSecondary), + "secondary_container": c(secondaryContainer), + "on_secondary_container": c(onSecondaryContainer), + "tertiary": c(colors.mTertiary), + "on_tertiary": c(onTertiary), + "tertiary_container": c(tertiaryContainer), + "on_tertiary_container": c(onTertiaryContainer), + "error": c(colors.mError), + "on_error": c(onError), + "error_container": c(errorContainer), + "on_error_container": c(onErrorContainer), + "background": c(surface), + "on_background": c(onSurface), + "surface": c(surface), + "on_surface": c(onSurface), + "surface_variant": c(surfaceVariant), + "on_surface_variant": c(onSurfaceVariant), + "surface_container_lowest": c(surfaceContainerLowest), + "surface_container_low": c(surfaceContainerLow), + "surface_container": c(surfaceContainer), + "surface_container_high": c(surfaceContainerHigh), + "surface_container_highest": c(surfaceContainerHighest), + "outline": c(outline), + "outline_variant": c(outlineVariant), + "shadow": c(shadow) + } + } +} diff --git a/Services/ColorSchemeService.qml b/Services/Theming/ColorSchemeService.qml similarity index 99% rename from Services/ColorSchemeService.qml rename to Services/Theming/ColorSchemeService.qml index 002a1d89..a853e651 100644 --- a/Services/ColorSchemeService.qml +++ b/Services/Theming/ColorSchemeService.qml @@ -5,7 +5,7 @@ import Qt.labs.folderlistmodel import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +import qs.Services.UI Singleton { id: root diff --git a/Services/Theming/TemplateProcessor.qml b/Services/Theming/TemplateProcessor.qml new file mode 100644 index 00000000..c1f6ae33 --- /dev/null +++ b/Services/Theming/TemplateProcessor.qml @@ -0,0 +1,378 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.System +import qs.Services.UI + +Singleton { + id: root + + readonly property string dynamicConfigPath: Settings.cacheDir + "matugen.dynamic.toml" + + readonly property var schemeNameMap: ({ + "Noctalia (default)": "Noctalia-default", + "Noctalia (legacy)": "Noctalia-legacy", + "Tokyo Night": "Tokyo-Night" + }) + + readonly property var terminalPaths: ({ + "foot": "~/.config/foot/themes/noctalia", + "ghostty": "~/.config/ghostty/themes/noctalia", + "kitty": "~/.config/kitty/themes/noctalia.conf", + "alacritty": "~/.config/alacritty/themes/noctalia.toml", + "wezterm": "~/.config/wezterm/colors/Noctalia.toml" + }) + + + /** + * Process wallpaper colors using matugen CLI + * Dual-path architecture (wallpaper uses matugen CLI) + */ + function processWallpaperColors(wallpaperPath, mode) { + const content = buildMatugenConfig() + if (!content) + return + + const wp = wallpaperPath.replace(/'/g, "'\\''") + const script = buildMatugenScript(content, wp, mode) + + generateProcess.generator = "matugen" + generateProcess.command = ["bash", "-lc", script] + generateProcess.running = true + } + + + /** + * Process predefined color scheme using sed scripts + * Dual-path architecture (predefined uses sed scripts) + */ + function processPredefinedScheme(schemeData, mode) { + handleTerminalThemes(mode) + + const colors = schemeData[mode] + let script = processAllTemplates(colors, mode) + + // Add user templates if enabled (requirement #1) + script += buildUserTemplateCommandForPredefined(schemeData, mode) + + generateProcess.generator = "predefined" + generateProcess.command = ["bash", "-lc", script] + generateProcess.running = true + } + + // ================================================================================ + // WALLPAPER-BASED GENERATION (matugen CLI) + // ================================================================================ + function buildMatugenConfig() { + var lines = [] + var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light" + + if (Settings.data.colorSchemes.useWallpaperColors) { + addWallpaperTemplates(lines, mode) + } + + addApplicationTemplates(lines, mode) + + if (lines.length > 0) { + return ["[config]"].concat(lines).join("\n") + "\n" + } + return "" + } + + function addWallpaperTemplates(lines, mode) { + // Noctalia colors JSON + lines.push("[templates.noctalia]") + lines.push('input_path = "' + Quickshell.shellDir + '/Assets/MatugenTemplates/noctalia.json"') + lines.push('output_path = "' + Settings.configDir + 'colors.json"') + + // Terminal templates + TemplateRegistry.terminals.forEach(terminal => { + if (Settings.data.templates[terminal.id]) { + lines.push(`\n[templates.${terminal.id}]`) + lines.push(`input_path = "${Quickshell.shellDir}/Assets/MatugenTemplates/${terminal.matugenPath}"`) + lines.push(`output_path = "${terminal.outputPath}"`) + const postHook = terminal.postHook || `${TemplateRegistry.colorsApplyScript} ${terminal.id}` + lines.push(`post_hook = "${postHook}"`) + } + }) + } + + function addApplicationTemplates(lines, mode) { + TemplateRegistry.applications.forEach(app => { + if (app.id === "discord") { + // Handle Discord clients specially + if (Settings.data.templates.discord) { + app.clients.forEach(client => { + // Check if this specific client is detected + if (isDiscordClientEnabled(client.name)) { + lines.push(`\n[templates.discord_${client.name}]`) + lines.push(`input_path = "${Quickshell.shellDir}/Assets/MatugenTemplates/${app.input}"`) + lines.push(`output_path = "${client.path}/themes/noctalia.theme.css"`) + } + }) + } + } else { + // Handle regular apps + if (Settings.data.templates[app.id]) { + app.outputs.forEach((output, idx) => { + lines.push(`\n[templates.${app.id}_${idx}]`) + lines.push(`input_path = "${Quickshell.shellDir}/Assets/MatugenTemplates/${app.input}"`) + lines.push(`output_path = "${output.path}"`) + if (app.postProcess) { + lines.push(`post_hook = "${app.postProcess(mode)}"`) + } + }) + } + } + }) + } + + function isDiscordClientEnabled(clientName) { + // Check ProgramCheckerService to see if client is detected + for (var i = 0; i < ProgramCheckerService.availableDiscordClients.length; i++) { + if (ProgramCheckerService.availableDiscordClients[i].name === clientName) { + return true + } + } + return false + } + + function buildMatugenScript(content, wallpaper, mode) { + const delimiter = "MATUGEN_CONFIG_EOF_" + Math.random().toString(36).substr(2, 9) + const pathEsc = dynamicConfigPath.replace(/'/g, "'\\''") + + let script = `cat > '${pathEsc}' << '${delimiter}'\n${content}\n${delimiter}\n` + script += `matugen image '${wallpaper}' --config '${pathEsc}' --mode ${mode} --type ${Settings.data.colorSchemes.matugenSchemeType}` + script += buildUserTemplateCommand(wallpaper, mode) + + return script + "\n" + } + + // ================================================================================ + // PREDEFINED SCHEME GENERATION (sed scripts) + // ================================================================================ + function processAllTemplates(colors, mode) { + let script = "" + const homeDir = Quickshell.env("HOME") + + TemplateRegistry.applications.forEach(app => { + if (app.id === "discord") { + if (Settings.data.templates.discord) { + script += processDiscordClients(app, colors, mode, homeDir) + } + } else { + if (Settings.data.templates[app.id]) { + script += processTemplate(app, colors, mode, homeDir) + } + } + }) + return script + } + + function processDiscordClients(discordApp, colors, mode, homeDir) { + let script = "" + const palette = ColorPaletteGenerator.generatePalette(colors, Settings.data.colorSchemes.darkMode, false) + + discordApp.clients.forEach(client => { + if (!isDiscordClientEnabled(client.name)) + return + + const templatePath = `${Quickshell.shellDir}/Assets/MatugenTemplates/${discordApp.input}` + const outputPath = `${client.path}/themes/noctalia.theme.css`.replace("~", homeDir) + const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/')) + const baseConfigDir = outputDir.replace("/themes", "") + + script += `\n` + script += `if [ -d "${baseConfigDir}" ]; then\n` + script += ` mkdir -p ${outputDir}\n` + script += ` cp '${templatePath}' '${outputPath}'\n` + script += ` ${replaceColorsInFile(outputPath, palette)}` + script += `else\n` + script += ` echo "Discord client ${client.name} not found at ${baseConfigDir}, skipping"\n` + script += `fi\n` + }) + + return script + } + + function processTemplate(app, colors, mode, homeDir) { + const palette = ColorPaletteGenerator.generatePalette(colors, Settings.data.colorSchemes.darkMode, app.strict || false) + let script = "" + + app.outputs.forEach(output => { + const templatePath = `${Quickshell.shellDir}/Assets/MatugenTemplates/${app.input}` + const outputPath = output.path.replace("~", homeDir) + const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/')) + + script += `\n` + script += `mkdir -p ${outputDir}\n` + script += `cp '${templatePath}' '${outputPath}'\n` + script += replaceColorsInFile(outputPath, palette) + script += `\n` + }) + + if (app.postProcess) { + script += app.postProcess(mode) + } + + return script + } + + function replaceColorsInFile(filePath, colors) { + let script = "" + Object.keys(colors).forEach(colorKey => { + const hexValue = colors[colorKey].default.hex + const hexStrippedValue = colors[colorKey].default.hex_stripped + + const escapedHex = hexValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const escapedHexStripped = hexStrippedValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + + // replace hex_stripped + script += `sed -i 's/{{colors\\.${colorKey}\\.default\\.hex_stripped}}/${escapedHexStripped}/g' '${filePath}'\n` + + // replace hex + script += `sed -i 's/{{colors\\.${colorKey}\\.default\\.hex}}/${escapedHex}/g' '${filePath}'\n` + }) + return script + } + + // ================================================================================ + // TERMINAL THEMES (predefined schemes use pre-rendered files) + // ================================================================================ + function handleTerminalThemes(mode) { + const commands = [] + const homeDir = Quickshell.env("HOME") + + Object.keys(terminalPaths).forEach(terminal => { + if (Settings.data.templates[terminal]) { + const outputPath = terminalPaths[terminal].replace("~", homeDir) + const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/')) + const templatePath = getTerminalColorsTemplate(terminal, mode) + + commands.push(`mkdir -p ${outputDir}`) + commands.push(`cp -f ${templatePath} ${outputPath}`) + commands.push(`${TemplateRegistry.colorsApplyScript} ${terminal}`) + } + }) + + if (commands.length > 0) { + copyProcess.command = ["bash", "-lc", commands.join('; ')] + copyProcess.running = true + } + } + + function getTerminalColorsTemplate(terminal, mode) { + let colorScheme = Settings.data.colorSchemes.predefinedScheme + colorScheme = schemeNameMap[colorScheme] || colorScheme + + let extension = "" + if (terminal === 'kitty') { + extension = ".conf" + } else if (terminal === 'wezterm') { + extension = ".toml" + } + + return `${Quickshell.shellDir}/Assets/ColorScheme/${colorScheme}/terminal/${terminal}/${colorScheme}-${mode}${extension}` + } + + // ================================================================================ + // USER TEMPLATES, advanced usage + // ================================================================================ + function buildUserTemplateCommand(input, mode) { + if (!Settings.data.templates.enableUserTemplates) + return "" + + const userConfigPath = getUserConfigPath() + let script = "\n# Execute user config if it exists\n" + script += `if [ -f '${userConfigPath}' ]; then\n` + script += ` matugen image '${input}' --config '${userConfigPath}' --mode ${mode} --type ${Settings.data.colorSchemes.matugenSchemeType}\n` + script += "fi" + + return script + } + + function buildUserTemplateCommandForPredefined(schemeData, mode) { + if (!Settings.data.templates.enableUserTemplates) + return "" + + const userConfigPath = getUserConfigPath() + const colors = schemeData[mode] + const palette = ColorPaletteGenerator.generatePalette(colors, Settings.data.colorSchemes.darkMode, false) + + const tempJsonPath = Settings.cacheDir + "predefined-colors.json" + const tempJsonPathEsc = tempJsonPath.replace(/'/g, "'\\''") + + let script = "\n# Execute user templates with predefined scheme colors\n" + script += `if [ -f '${userConfigPath}' ]; then\n` + script += ` cat > '${tempJsonPathEsc}' << 'EOF'\n` + script += JSON.stringify({ + "colors": palette + }, null, 2) + "\n" + script += "EOF\n" + script += ` matugen json '${tempJsonPathEsc}' --config '${userConfigPath}' --mode ${mode}\n` + script += "fi" + + return script + } + + function getUserConfigPath() { + return (Settings.configDir + "user-templates.toml").replace(/'/g, "'\\''") + } + + // ================================================================================ + // PROCESSES + // ================================================================================ + Process { + id: generateProcess + workingDirectory: Quickshell.shellDir + running: false + + // Error reporting helpers + property string generator: "" + function showWarning() { + const description = (stderr.text && stderr.text.trim() !== "") ? stderr.text.trim() : ((stdout.text && stdout.text.trim() !== "") ? stdout.text.trim() : I18n.tr("toast.theming-processor-failed.desc-generic")) + const title = I18n.tr(`toast.theming-processor-failed.title-${generator}`) + + // Give a bit more time to the user to read, as it can contains important information for debugging user's templates. + ToastService.showWarning(title, description, 8000) + return description + } + + onExited: function (exitCode) { + if (exitCode !== 0) { + const description = generateProcess.showWarning() + Logger.e("TemplateProcessor", "Process failed with exit code", exitCode, description) + } + } + + stdout: StdioCollector { + onStreamFinished: { + if (this.text) + Logger.d("TemplateProcessor", "stdout:", this.text) + } + } + stderr: StdioCollector { + onStreamFinished: { + if (this.text) { + const description = generateProcess.showWarning() + Logger.e("TemplateProcessor", "Process failed", description) + } + } + } + } + + Process { + id: copyProcess + workingDirectory: Quickshell.shellDir + running: false + stderr: StdioCollector { + onStreamFinished: { + if (this.text) + Logger.d("TemplateProcessor", "copyProcess stderr:", this.text) + } + } + } +} diff --git a/Services/Theming/TemplateRegistry.qml b/Services/Theming/TemplateRegistry.qml new file mode 100644 index 00000000..ebb67a01 --- /dev/null +++ b/Services/Theming/TemplateRegistry.qml @@ -0,0 +1,247 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + readonly property string colorsApplyScript: Quickshell.shellDir + '/Bin/colors-apply.sh' + + // Terminal configurations (for wallpaper-based matugen templates) + readonly property var terminals: [{ + "id": "foot", + "name": "Foot", + "matugenPath": "Terminal/foot", + "outputPath": "~/.config/foot/themes/noctalia" + }, { + "id": "ghostty", + "name": "Ghostty", + "matugenPath": "Terminal/ghostty", + "outputPath": "~/.config/ghostty/themes/noctalia", + "postHook": "bash -c 'pgrep -f ghostty >/dev/null && pkill -SIGUSR2 ghostty || true'" + }, { + "id": "kitty", + "name": "Kitty", + "matugenPath": "Terminal/kitty.conf", + "outputPath": "~/.config/kitty/themes/noctalia.conf" + }, { + "id": "alacritty", + "name": "Alacritty", + "matugenPath": "Terminal/alacritty.toml", + "outputPath": "~/.config/alacritty/themes/noctalia.toml" + }, { + "id": "wezterm", + "name": "Wezterm", + "matugenPath": "Terminal/wezterm.toml", + "outputPath": "~/.config/wezterm/colors/Noctalia.toml", + "postHook": "touch ~/.config/wezterm/wezterm.lua" + }] + + // Application configurations - consolidated from MatugenTemplates + AppThemeService + readonly property var applications: [{ + "id": "gtk", + "name": "GTK", + "category": "ui", + "input": "gtk.css", + "outputs": [{ + "path": "~/.config/gtk-3.0/gtk.css" + }, { + "path": "~/.config/gtk-4.0/gtk.css" + }], + "postProcess": mode => `gsettings set org.gnome.desktop.interface color-scheme prefer-${mode}` + }, { + "id": "qt", + "name": "Qt", + "category": "ui", + "input": "qtct.conf", + "outputs": [{ + "path": "~/.config/qt5ct/colors/noctalia.conf" + }, { + "path": "~/.config/qt6ct/colors/noctalia.conf" + }] + }, { + "id": "kcolorscheme", + "name": "KColorScheme", + "category": "ui", + "input": "kcolorscheme.colors", + "outputs": [{ + "path": "~/.local/share/color-schemes/noctalia.colors" + }] + }, { + "id": "fuzzel", + "name": "Fuzzel", + "category": "launchers", + "input": "fuzzel.conf", + "outputs": [{ + "path": "~/.config/fuzzel/themes/noctalia" + }], + "postProcess": () => `${colorsApplyScript} fuzzel` + }, { + "id": "vicinae", + "name": "Vicinae", + "category": "launchers", + "input": "vicinae.toml", + "outputs": [{ + "path": "~/.local/share/vicinae/themes/matugen.toml" + }], + "postProcess": () => `cp --update=none ${Quickshell.shellDir}/Assets/noctalia.svg ~/.local/share/vicinae/themes/noctalia.svg && ${colorsApplyScript} vicinae` + }, { + "id": "walker", + "name": "Walker", + "category": "launchers", + "input": "walker.css", + "outputs": [{ + "path": "~/.config/walker/themes/noctalia/style.css" + }], + "postProcess": () => `${colorsApplyScript} walker`, + "strict": true // Use strict mode for palette generation (preserves custom surface/outline values) + }, { + "id": "pywalfox", + "name": "Pywalfox", + "category": "applications", + "input": "pywalfox.json", + "outputs": [{ + "path": "~/.cache/wal/colors.json" + }], + "postProcess": () => `${colorsApplyScript} pywalfox` + }, // CONSOLIDATED DISCORD CLIENTS + { + "id": "discord", + "name": "Discord", + "category": "applications", + "input": "vesktop.css", + "clients": [{ + "name": "vesktop", + "path": "~/.config/vesktop", + "requiresThemesFolder": false + }, { + "name": "webcord", + "path": "~/.config/webcord", + "requiresThemesFolder": false + }, { + "name": "armcord", + "path": "~/.config/armcord", + "requiresThemesFolder": false + }, { + "name": "equibop", + "path": "~/.config/equibop", + "requiresThemesFolder": false + }, { + "name": "lightcord", + "path": "~/.config/lightcord", + "requiresThemesFolder": false + }, { + "name": "dorion", + "path": "~/.config/dorion", + "requiresThemesFolder": false + }, { + "name": "vencord", + "path": "~/.config/discord", + "requiresThemesFolder": true + }] + }, // VSCode with hardcoded path (requirement #5) + { + "id": "code", + "name": "VSCode", + "category": "applications", + "input": "code.json", + "outputs": [{ + "path": "~/.vscode/extensions/hyprluna.hyprluna-theme-1.0.2/themes/hyprluna.json" + }] + }, { + "id": "spicetify", + "name": "Spicetify", + "category": "applications", + "input": "spicetify.ini", + "outputs": [{ + "path": "~/.config/spicetify/Themes/Comfy/color.ini" + }], + "postProcess": () => `spicetify -q apply --no-restart` + }] + + // Extract Discord clients for ProgramCheckerService compatibility + readonly property var discordClients: { + var clients = [] + var discordApp = applications.find(app => app.id === "discord") + if (discordApp && discordApp.clients) { + discordApp.clients.forEach(client => { + clients.push({ + "name": client.name, + "configPath": client.path, + "themePath": `${client.path}/themes/noctalia.theme.css`, + "requiresThemesFolder": client.requiresThemesFolder || false + }) + }) + } + return clients + } + + // Build user templates TOML content + function buildUserTemplatesToml() { + var lines = [] + lines.push("[config]") + lines.push("") + lines.push("[templates]") + lines.push("") + lines.push("# User-defined templates") + lines.push("# Add your custom templates below") + lines.push("# Example:") + lines.push("# [templates.myapp]") + lines.push("# input_path = \"~/.config/noctalia/templates/myapp.css\"") + lines.push("# output_path = \"~/.config/myapp/theme.css\"") + lines.push("# post_hook = \"myapp --reload-theme\"") + lines.push("") + lines.push("# Remove this section and add your own templates") + lines.push("#[templates.placeholder]") + lines.push("#input_path = \"" + Quickshell.shellDir + "/Assets/MatugenTemplates/noctalia.json\"") + lines.push("#output_path = \"" + Settings.cacheDir + "placeholder.json\"") + lines.push("") + + return lines.join("\n") + "\n" + } + + // Write user templates TOML file (moved from MatugenTemplates) + function writeUserTemplatesToml() { + var userConfigPath = Settings.configDir + "user-templates.toml" + + // Check if file already exists + fileCheckProcess.command = ["test", "-f", userConfigPath] + fileCheckProcess.running = true + } + + function doWriteUserTemplatesToml() { + var userConfigPath = Settings.configDir + "user-templates.toml" + var configContent = buildUserTemplatesToml() + var userConfigPathEsc = userConfigPath.replace(/'/g, "'\\''") + + // Ensure directory exists (should already exist but just in case) + Quickshell.execDetached(["mkdir", "-p", Settings.configDir]) + + // Write the config file using heredoc to avoid escaping issues + var script = `cat > '${userConfigPathEsc}' << 'EOF'\n` + script += configContent + script += "EOF\n" + Quickshell.execDetached(["sh", "-c", script]) + + Logger.d("TemplateRegistry", "User templates config written to:", userConfigPath) + } + + // Process for checking if user templates file exists + Process { + id: fileCheckProcess + running: false + + onExited: function (exitCode) { + if (exitCode === 0) { + // File exists, skip creation + Logger.d("TemplateRegistry", "User templates config already exists, skipping creation") + } else { + // File doesn't exist, create it + doWriteUserTemplatesToml() + } + } + } +} diff --git a/Services/BarService.qml b/Services/UI/BarService.qml similarity index 100% rename from Services/BarService.qml rename to Services/UI/BarService.qml diff --git a/Services/BarWidgetRegistry.qml b/Services/UI/BarWidgetRegistry.qml similarity index 93% rename from Services/BarWidgetRegistry.qml rename to Services/UI/BarWidgetRegistry.qml index 69d30d39..5b0c8ffa 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/UI/BarWidgetRegistry.qml @@ -12,7 +12,8 @@ Singleton { property var widgets: ({ "ActiveWindow": activeWindowComponent, "AudioVisualizer": audioVisualizerComponent, - "Battery": batteryComponent, + "Battery"// HEAVY + : batteryComponent, "Bluetooth": bluetoothComponent, "Brightness": brightnessComponent, "Clock": clockComponent, @@ -25,19 +26,23 @@ Singleton { "MediaMini": mediaMiniComponent, "Microphone": microphoneComponent, "NightLight": nightLightComponent, + "NoctaliaPerformance": noctaliaPerformanceComponent, "NotificationHistory": notificationHistoryComponent, - "PowerProfile": powerProfileComponent, + "PowerProfile"// HEAVY + : powerProfileComponent, "ScreenRecorder": screenRecorderComponent, "SessionMenu": sessionMenuComponent, "Spacer": spacerComponent, "SystemMonitor": systemMonitorComponent, - "Taskbar": taskbarComponent, + "Taskbar"// HEAVY + : taskbarComponent, "TaskbarGrouped": taskbarGroupedComponent, "Tray": trayComponent, - "Volume": volumeComponent, + "Volume"// A BIT HEAVY ? + : volumeComponent, "WiFi": wiFiComponent, "WallpaperSelector": wallpaperSelectorComponent, - "Workspace": workspaceComponent + "Workspace": workspaceComponent // HEAVY }) property var widgetMetadata: ({ @@ -81,7 +86,8 @@ Singleton { "allowUserSettings": true, "useDistroLogo": false, "icon": "noctalia", - "customIconPath": "" + "customIconPath": "", + "colorizeDistroLogo": false }, "CustomButton": { "allowUserSettings": true, @@ -93,7 +99,8 @@ Singleton { "textStream": false, "textIntervalMs": 3000, "textCollapse": "", - "parseJson": false + "parseJson": false, + "hideTextInVerticalBar": false }, "KeyboardLayout": { "allowUserSettings": true, @@ -218,6 +225,9 @@ Singleton { property Component nightLightComponent: Component { NightLight {} } + property Component noctaliaPerformanceComponent: Component { + NoctaliaPerformance {} + } property Component notificationHistoryComponent: Component { NotificationHistory {} } diff --git a/Services/ControlCenterWidgetRegistry.qml b/Services/UI/ControlCenterWidgetRegistry.qml similarity index 100% rename from Services/ControlCenterWidgetRegistry.qml rename to Services/UI/ControlCenterWidgetRegistry.qml diff --git a/Services/PanelService.qml b/Services/UI/PanelService.qml similarity index 58% rename from Services/PanelService.qml rename to Services/UI/PanelService.qml index d33ba786..79682646 100644 --- a/Services/PanelService.qml +++ b/Services/UI/PanelService.qml @@ -16,23 +16,16 @@ Singleton { signal willOpen signal didClose - // Currently opened popups, can have more than one. - // ex: when opening an NIconPicker from a widget setting. - property var openedPopups: [] - property bool hasOpenedPopup: false - signal popupChanged - // Register this panel (called after panel is loaded) function registerPanel(panel) { registeredPanels[panel.objectName] = panel - Logger.i("PanelService", "Registered panel:", panel.objectName) + Logger.d("PanelService", "Registered panel:", panel.objectName) } // Returns a panel (loads it on-demand if not yet loaded) function getPanel(name, screen) { if (!screen) { - Logger.w("PanelService", "missing screen for getPanel:", name) - Logger.callStack() + Logger.d("PanelService", "missing screen for getPanel:", name) // If no screen specified, return the first matching panel for (var key in registeredPanels) { if (key.startsWith(name + "-")) { @@ -49,15 +42,6 @@ Singleton { return registeredPanels[panelKey] } - // Panel not loaded yet - try to load it via the loader - if (registeredPanelLoaders[panelKey]) { - Logger.d("PanelService", "Loading panel on-demand:", panelKey) - registeredPanelLoaders[panelKey].ensureLoaded() - // After ensureLoaded(), the panel should register itself via registerPanel() - // Return it if it registered synchronously - return registeredPanels[panelKey] || null - } - Logger.w("PanelService", "Panel not found:", panelKey) return null } @@ -86,17 +70,4 @@ Singleton { // emit signal didClose() } - - // Popups - function willOpenPopup(popup) { - openedPopups.push(popup) - hasOpenedPopup = (openedPopups.length !== 0) - popupChanged() - } - - function willClosePopup(popup) { - openedPopups = openedPopups.filter(p => p !== popup) - hasOpenedPopup = (openedPopups.length !== 0) - popupChanged() - } } diff --git a/Services/UI/SystemTrayService.qml b/Services/UI/SystemTrayService.qml new file mode 100644 index 00000000..4f2e74ce --- /dev/null +++ b/Services/UI/SystemTrayService.qml @@ -0,0 +1,40 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Services.SystemTray +import qs.Commons + + +/** + * SystemTrayService + * This service ensures that Quickshell's SystemTray service is initialized + * early in the shell startup to avoid programs that should stay in tray, not having access to one (let's hope this works). + */ +Singleton { + id: root + + property bool initialized: false + + Component.onCompleted: { + if (SystemTray && SystemTray.items) { + Logger.i("SystemTrayService", "SystemTray service initialized") + initialized = true + + // Monitor for tray items to confirm it's working + if (SystemTray.items.valuesChanged) { + Logger.d("SystemTrayService", "SystemTray is ready and monitoring for items") + } + } else { + Logger.w("SystemTrayService", "SystemTray service not available") + } + } + + function init() { + // Explicit initialization function + if (!initialized && SystemTray && SystemTray.items) { + Logger.i("SystemTrayService", "SystemTray service initialized via init()") + initialized = true + } + } +} diff --git a/Services/ToastService.qml b/Services/UI/ToastService.qml similarity index 90% rename from Services/ToastService.qml rename to Services/UI/ToastService.qml index fd39e6ba..5a5a0929 100644 --- a/Services/ToastService.qml +++ b/Services/UI/ToastService.qml @@ -18,7 +18,7 @@ Singleton { notify(message, description, "", "warning", duration) } - function showError(message, description = "", duration = 5000) { + function showError(message, description = "", duration = 6000) { notify(message, description, "", "error", duration) } } diff --git a/Services/TooltipService.qml b/Services/UI/TooltipService.qml similarity index 100% rename from Services/TooltipService.qml rename to Services/UI/TooltipService.qml diff --git a/Services/WallpaperService.qml b/Services/UI/WallpaperService.qml similarity index 100% rename from Services/WallpaperService.qml rename to Services/UI/WallpaperService.qml diff --git a/Widgets/AudioSpectrum/NLinearSpectrum.qml b/Widgets/AudioSpectrum/NLinearSpectrum.qml index a0bbd34c..596f7d10 100644 --- a/Widgets/AudioSpectrum/NLinearSpectrum.qml +++ b/Widgets/AudioSpectrum/NLinearSpectrum.qml @@ -14,9 +14,10 @@ Item { property real minimumSignalValue: 0.05 // Default to 5% of height // Pre compute horizontal mirroring - readonly property int valuesCount: values.length + readonly property int valuesCount: (values && Array.isArray(values)) ? values.length : 0 readonly property int totalBars: valuesCount * 2 readonly property real barSlotSize: totalBars > 0 ? (vertical ? height : width) / totalBars : 0 + readonly property bool highQuality: (Settings.data.audio.visualizerType === "low") ? false : true Repeater { model: root.totalBars @@ -27,18 +28,23 @@ Item { property int valueIndex: index < root.valuesCount ? root.valuesCount - 1 - index // Mirrored half : index - root.valuesCount // Normal half - property real rawAmp: root.values[valueIndex] + property real rawAmp: (root.values && root.values[valueIndex] !== undefined) ? root.values[valueIndex] : 0 property real amp: (root.showMinimumSignal && rawAmp === 0) ? root.minimumSignalValue : rawAmp color: root.fillColor border.color: root.strokeColor border.width: root.strokeWidth - antialiasing: true + antialiasing: root.highQuality + smooth: root.highQuality + // Only update when value actually changes - reduces GPU load width: vertical ? root.width * amp : root.barSlotSize * 0.5 height: vertical ? root.barSlotSize * 0.5 : root.height * amp x: vertical ? root.width - width : index * root.barSlotSize + (root.barSlotSize * 0.25) y: vertical ? index * root.barSlotSize + (root.barSlotSize * 0.25) : root.height - height + + // Disable updates when invisible to save GPU + visible: root.visible } } } diff --git a/Widgets/AudioSpectrum/NMirroredSpectrum.qml b/Widgets/AudioSpectrum/NMirroredSpectrum.qml index dd5c98ee..4da22d6d 100644 --- a/Widgets/AudioSpectrum/NMirroredSpectrum.qml +++ b/Widgets/AudioSpectrum/NMirroredSpectrum.qml @@ -14,10 +14,10 @@ Item { property real minimumSignalValue: 0.05 // Default to 5% of height // Pre-compute mirroring - readonly property int valuesCount: values.length + readonly property int valuesCount: (values && Array.isArray(values)) ? values.length : 0 readonly property int totalBars: valuesCount * 2 readonly property real barSlotSize: totalBars > 0 ? (vertical ? height : width) / totalBars : 0 - + readonly property bool highQuality: (Settings.data.audio.visualizerType === "low") ? false : true readonly property real centerY: height / 2 readonly property real centerX: width / 2 @@ -30,7 +30,7 @@ Item { property int valueIndex: index < root.valuesCount ? root.valuesCount - 1 - index // Mirrored half : index - root.valuesCount // Normal half - property real rawAmp: root.values[valueIndex] + property real rawAmp: (root.values && root.values[valueIndex] !== undefined) ? root.values[valueIndex] : 0 property real amp: (root.showMinimumSignal && rawAmp === 0) ? root.minimumSignalValue : rawAmp property real barSize: (vertical ? root.width : root.height) * amp @@ -38,12 +38,16 @@ Item { color: root.fillColor border.color: root.strokeColor border.width: root.strokeWidth - antialiasing: true + antialiasing: root.highQuality + smooth: root.highQuality width: vertical ? barSize : root.barSlotSize * 0.8 height: vertical ? root.barSlotSize * 0.8 : barSize x: vertical ? root.centerX - (barSize / 2) : index * root.barSlotSize + (root.barSlotSize * 0.25) y: vertical ? index * root.barSlotSize + (root.barSlotSize * 0.25) : root.centerY - (barSize / 2) + + // Disable updates when invisible to save GPU + visible: root.visible } } } diff --git a/Widgets/AudioSpectrum/NWaveSpectrum.qml b/Widgets/AudioSpectrum/NWaveSpectrum.qml index 556726b4..e2a78fe9 100644 --- a/Widgets/AudioSpectrum/NWaveSpectrum.qml +++ b/Widgets/AudioSpectrum/NWaveSpectrum.qml @@ -13,26 +13,49 @@ Item { property bool showMinimumSignal: false property real minimumSignalValue: 0.05 // Default to 5% of height - // Redraw when necessary - onWidthChanged: canvas.requestPaint() - onHeightChanged: canvas.requestPaint() - onValuesChanged: canvas.requestPaint() - onFillColorChanged: canvas.requestPaint() - onStrokeColorChanged: canvas.requestPaint() - onShowMinimumSignalChanged: canvas.requestPaint() - onMinimumSignalValueChanged: canvas.requestPaint() - onVerticalChanged: canvas.requestPaint() + // Rendering active state - only redraw when visible and values are changing + property bool renderingActive: visible && values && values.length > 0 + + // Redraw when necessary - only if rendering is active + onWidthChanged: if (renderingActive) + canvas.requestPaint() + onHeightChanged: if (renderingActive) + canvas.requestPaint() + onValuesChanged: if (renderingActive) + canvas.requestPaint() + onFillColorChanged: if (renderingActive) + canvas.requestPaint() + onStrokeColorChanged: if (renderingActive) + canvas.requestPaint() + onShowMinimumSignalChanged: if (renderingActive) + canvas.requestPaint() + onMinimumSignalValueChanged: if (renderingActive) + canvas.requestPaint() + onVerticalChanged: if (renderingActive) + canvas.requestPaint() + + // Clear canvas when not rendering + onRenderingActiveChanged: { + if (!renderingActive) { + var ctx = canvas.getContext("2d") + if (ctx) + ctx.reset() + canvas.requestPaint() + } + } Canvas { id: canvas anchors.fill: parent - antialiasing: true + antialiasing: false // Disable for better performance - shape is smooth enough without it + renderStrategy: Canvas.Threaded // Render in separate thread to reduce main thread load + renderTarget: Canvas.FramebufferObject // Use FBO for better performance onPaint: { var ctx = getContext("2d") ctx.reset() - if (values.length === 0) { + if (!values || !Array.isArray(values) || values.length === 0) { return } diff --git a/Widgets/NBox.qml b/Widgets/NBox.qml index 10ed50bd..7d775bfb 100644 --- a/Widgets/NBox.qml +++ b/Widgets/NBox.qml @@ -1,7 +1,5 @@ import QtQuick import qs.Commons -import qs.Commons -import qs.Services // Rounded group container using the variant surface color. // To be used in side panels and settings panes to group fields or buttons. diff --git a/Widgets/NBusyIndicator.qml b/Widgets/NBusyIndicator.qml index f65da231..e720debe 100644 --- a/Widgets/NBusyIndicator.qml +++ b/Widgets/NBusyIndicator.qml @@ -1,6 +1,5 @@ import QtQuick import qs.Commons -import qs.Services Item { id: root diff --git a/Widgets/NButton.qml b/Widgets/NButton.qml index aed27ab7..ce6c68cf 100644 --- a/Widgets/NButton.qml +++ b/Widgets/NButton.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services +import qs.Services.UI Rectangle { id: root diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index 169aa445..9cdb338b 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -1,7 +1,6 @@ import QtQuick import QtQuick.Layouts import qs.Commons -import qs.Services import qs.Widgets // Compact circular statistic display using Layout management diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml index fccc6c0c..5615aa45 100644 --- a/Widgets/NClock.qml +++ b/Widgets/NClock.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services import Quickshell import "../Helpers/ColorsConvert.js" as ColorsConvert diff --git a/Widgets/NColorPicker.qml b/Widgets/NColorPicker.qml index a37bb6cd..d25087c4 100644 --- a/Widgets/NColorPicker.qml +++ b/Widgets/NColorPicker.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services import qs.Widgets Rectangle { diff --git a/Widgets/NColorPickerDialog.qml b/Widgets/NColorPickerDialog.qml index 21c624e6..4f142f16 100644 --- a/Widgets/NColorPickerDialog.qml +++ b/Widgets/NColorPickerDialog.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services import qs.Widgets Popup { @@ -27,14 +26,6 @@ Popup { modal: true - onOpened: { - PanelService.willOpenPopup(root) - } - - onClosed: { - PanelService.willClosePopup(root) - } - function rgbToHsv(r, g, b) { r /= 255 g /= 255 diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index deb0ba9d..b0c7faed 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services import qs.Widgets RowLayout { @@ -109,14 +108,6 @@ RowLayout { implicitHeight: Math.min(root.popupHeight, contentItem.implicitHeight + Style.marginM * 2) padding: Style.marginM - onOpened: { - PanelService.willOpenPopup(root) - } - - onClosed: { - PanelService.willClosePopup(root) - } - contentItem: NListView { model: combo.popup.visible ? root.model : null implicitHeight: contentHeight diff --git a/Widgets/NContextMenu.qml b/Widgets/NContextMenu.qml index 45b55aed..eed0a423 100644 --- a/Widgets/NContextMenu.qml +++ b/Widgets/NContextMenu.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services Popup { id: root @@ -18,9 +17,6 @@ Popup { width: 180 padding: Style.marginS - onOpened: PanelService.willOpenPopup(root) - onClosed: PanelService.willClosePopup(root) - background: Rectangle { color: Color.mSurfaceVariant border.color: Color.mOutline diff --git a/Widgets/NDivider.qml b/Widgets/NDivider.qml index d11e82c7..737807d7 100644 --- a/Widgets/NDivider.qml +++ b/Widgets/NDivider.qml @@ -2,7 +2,6 @@ import QtQuick import Quickshell import Quickshell.Widgets import qs.Commons -import qs.Services Rectangle { width: parent.width diff --git a/Widgets/NDropShadows.qml b/Widgets/NDropShadows.qml index 9dc2a4e5..1182f579 100644 --- a/Widgets/NDropShadows.qml +++ b/Widgets/NDropShadows.qml @@ -1,13 +1,16 @@ import QtQuick import QtQuick.Effects import qs.Commons -import qs.Services +import qs.Services.Power // Unified shadow system Item { id: root + required property var source + property bool autoPaddingEnabled: false + layer.enabled: Settings.data.general.enableShadows && !PowerProfileService.noctaliaPerformanceMode layer.effect: MultiEffect { source: root.source @@ -18,5 +21,6 @@ Item { shadowColor: Color.black shadowHorizontalOffset: Settings.data.general.shadowOffsetX shadowVerticalOffset: Settings.data.general.shadowOffsetY + autoPaddingEnabled: root.autoPaddingEnabled } } diff --git a/Widgets/NFilePicker.qml b/Widgets/NFilePicker.qml index 9489d114..84eb473d 100644 --- a/Widgets/NFilePicker.qml +++ b/Widgets/NFilePicker.qml @@ -5,7 +5,6 @@ import Qt.labs.folderlistmodel import Quickshell import Quickshell.Io import qs.Commons -import qs.Services import qs.Widgets import "../Helpers/FuzzySort.js" as FuzzySort @@ -28,14 +27,6 @@ Popup { signal accepted(var paths) signal cancelled - onOpened: { - PanelService.willOpenPopup(root) - } - - onClosed: { - PanelService.willClosePopup(root) - } - function openFilePicker() { if (!root.currentPath) root.currentPath = root.initialPath diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 2fd507e5..c72d7d38 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -2,7 +2,7 @@ import QtQuick import Quickshell import Quickshell.Widgets import qs.Commons -import qs.Services +import qs.Services.UI Rectangle { id: root diff --git a/Widgets/NIconButtonHot.qml b/Widgets/NIconButtonHot.qml index ff80b3ef..481d526f 100644 --- a/Widgets/NIconButtonHot.qml +++ b/Widgets/NIconButtonHot.qml @@ -3,7 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import QtQuick.Effects import qs.Commons -import qs.Services +import qs.Services.UI Rectangle { id: root diff --git a/Widgets/NIconPicker.qml b/Widgets/NIconPicker.qml index 2a4916e9..f37e01f7 100644 --- a/Widgets/NIconPicker.qml +++ b/Widgets/NIconPicker.qml @@ -4,7 +4,6 @@ import QtQuick.Layouts import QtQuick.Window import qs.Commons import qs.Widgets -import qs.Services Popup { id: root @@ -15,8 +14,8 @@ Popup { signal iconSelected(string iconName) - width: 900 * Style.uiScaleRatio - height: 700 * Style.uiScaleRatio + width: Math.round(900 * Style.uiScaleRatio) + height: Math.round(700 * Style.uiScaleRatio) anchors.centerIn: Overlay.overlay padding: Style.marginXL @@ -36,11 +35,6 @@ Popup { selectedIcon = initialIcon query = initialIcon searchInput.forceActiveFocus() - PanelService.willOpenPopup(root) - } - - onClosed: { - PanelService.willClosePopup(root) } background: Rectangle { diff --git a/Widgets/NImageCached.qml b/Widgets/NImageCached.qml index 2645d479..82be77bb 100644 --- a/Widgets/NImageCached.qml +++ b/Widgets/NImageCached.qml @@ -31,9 +31,9 @@ Image { } onCachePathChanged: { if (imageHash && cachePath) { - // Check if cache file exists before trying to load it - cacheChecker.command = ["test", "-f", cachePath] - cacheChecker.running = true + // Try to load the cached version, failure will be detected below in onStatusChanged + // Failure is expected and warnings are ok in the console. Don't try to improve without consulting. + source = cachePath } } onStatusChanged: { @@ -49,19 +49,4 @@ Image { }) } } - - // Check if cache file exists to avoid warnings - Process { - id: cacheChecker - running: false - onExited: function (exitCode) { - if (exitCode === 0 && root.cachePath) { - // Cache file exists, load it - root.source = root.cachePath - } else if (root.imagePath) { - // Cache doesn't exist, load original directly - root.source = root.imagePath - } - } - } } diff --git a/Widgets/NImageCircled.qml b/Widgets/NImageCircled.qml index 55ef7568..c97e4440 100644 --- a/Widgets/NImageCircled.qml +++ b/Widgets/NImageCircled.qml @@ -3,7 +3,6 @@ import QtQuick.Effects import Quickshell import Quickshell.Widgets import qs.Commons -import qs.Services Rectangle { id: root diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index 32987e28..a7a5789f 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -3,7 +3,6 @@ import QtQuick.Effects import Quickshell import Quickshell.Widgets import qs.Commons -import qs.Services Rectangle { id: root diff --git a/Widgets/NInputAction.qml b/Widgets/NInputAction.qml index b4b86879..6fac9618 100644 --- a/Widgets/NInputAction.qml +++ b/Widgets/NInputAction.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services // Input and button row RowLayout { diff --git a/Widgets/NRadioButton.qml b/Widgets/NRadioButton.qml index d61b0719..ea3cacbd 100644 --- a/Widgets/NRadioButton.qml +++ b/Widgets/NRadioButton.qml @@ -1,7 +1,6 @@ import QtQuick import QtQuick.Controls import qs.Commons -import qs.Services import qs.Widgets RadioButton { diff --git a/Widgets/NReorderCheckboxes.qml b/Widgets/NReorderCheckboxes.qml index a3a7f7e8..078c5e7a 100644 --- a/Widgets/NReorderCheckboxes.qml +++ b/Widgets/NReorderCheckboxes.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services Item { id: root diff --git a/Widgets/NSearchableComboBox.qml b/Widgets/NSearchableComboBox.qml index b4c82b52..bd334124 100644 --- a/Widgets/NSearchableComboBox.qml +++ b/Widgets/NSearchableComboBox.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services import qs.Widgets import "../Helpers/FuzzySort.js" as Fuzzysort @@ -159,14 +158,6 @@ RowLayout { height: root.popupHeight + 60 padding: Style.marginM - onOpened: { - PanelService.willOpenPopup(root) - } - - onClosed: { - PanelService.willClosePopup(root) - } - contentItem: ColumnLayout { spacing: Style.marginS diff --git a/Widgets/NSectionEditor.qml b/Widgets/NSectionEditor.qml index 65fe857f..a2ecbedf 100644 --- a/Widgets/NSectionEditor.qml +++ b/Widgets/NSectionEditor.qml @@ -4,7 +4,6 @@ import QtQuick.Effects import QtQuick.Layouts import qs.Commons import qs.Widgets -import qs.Services NBox { id: root diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 82bbf102..793dd7bf 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Effects import qs.Commons -import qs.Services +import qs.Services.UI Slider { id: root diff --git a/Widgets/NSpinBox.qml b/Widgets/NSpinBox.qml index a0fdbc16..0b96cfc4 100644 --- a/Widgets/NSpinBox.qml +++ b/Widgets/NSpinBox.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services import qs.Widgets RowLayout { diff --git a/Widgets/NTabBar.qml b/Widgets/NTabBar.qml index db218606..5c0259b7 100644 --- a/Widgets/NTabBar.qml +++ b/Widgets/NTabBar.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services import qs.Widgets Rectangle { diff --git a/Widgets/NTabButton.qml b/Widgets/NTabButton.qml index d9415836..a1def44b 100644 --- a/Widgets/NTabButton.qml +++ b/Widgets/NTabButton.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services import qs.Widgets Rectangle { diff --git a/Widgets/NText.qml b/Widgets/NText.qml index 4a9060ed..57c20ed0 100644 --- a/Widgets/NText.qml +++ b/Widgets/NText.qml @@ -1,7 +1,6 @@ import QtQuick import QtQuick.Layouts import qs.Commons -import qs.Services import qs.Widgets Text { diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index f9ba4f05..b2a5e0ea 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services ColumnLayout { id: root diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 4cdc91f9..82c54711 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services RowLayout { id: root diff --git a/Widgets/NValueSlider.qml b/Widgets/NValueSlider.qml index 19cb8e4c..62006780 100644 --- a/Widgets/NValueSlider.qml +++ b/Widgets/NValueSlider.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons -import qs.Services import qs.Widgets RowLayout { diff --git a/nix/package.nix b/nix/package.nix index a5d07917..cacec580 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -1,25 +1,16 @@ { version ? "dirty", lib, - stdenv, + stdenvNoCC, # build - gcc, qt6, quickshell, - xkeyboard_config, # runtime deps - bash, - bluez, brightnessctl, cava, cliphist, - coreutils, ddcutil, - file, - findutils, - libnotify, matugen, - networkmanager, wlsunset, wl-clipboard, gpu-screen-recorder, # optional @@ -33,6 +24,7 @@ filter = path: type: !(builtins.any (prefix: lib.path.hasPrefix (../. + prefix) (/. + path)) [ /.github + /.gitignore /Assets/Screenshots /Assets/Wallpaper /Bin/dev @@ -41,60 +33,55 @@ /README.md /flake.nix /flake.lock + /shell.nix + /lefthook.yml + /CLAUDE.md ]); }; runtimeDeps = [ - bash - bluez brightnessctl cava cliphist - coreutils ddcutil - file - findutils - libnotify matugen - networkmanager wlsunset wl-clipboard ] - ++ lib.optionals (stdenv.hostPlatform.isx86_64) [gpu-screen-recorder]; + ++ lib.optionals (stdenvNoCC.hostPlatform.system == "x86_64-linux") [ + gpu-screen-recorder + ]; - fontconfig = makeFontsConf { + fontsConf = makeFontsConf { fontDirectories = [ roboto inter-nerdfont ]; }; in - stdenv.mkDerivation { + stdenvNoCC.mkDerivation { pname = "noctalia-shell"; inherit version src; nativeBuildInputs = [ - gcc qt6.wrapQtAppsHook ]; + buildInputs = [ - quickshell - xkeyboard_config qt6.qtbase ]; - propagatedBuildInputs = runtimeDeps; installPhase = '' mkdir -p $out/share/noctalia-shell $out/bin - cp -r ./* $out/share/noctalia-shell - cp ${quickshell}/bin/qs $out/bin/noctalia-shell + cp -r . $out/share/noctalia-shell + ln -s ${quickshell}/bin/qs $out/bin/noctalia-shell ''; preFixup = '' qtWrapperArgs+=( --prefix PATH : ${lib.makeBinPath runtimeDeps} - --set FONTCONFIG_FILE ${fontconfig} + --set FONTCONFIG_FILE ${fontsConf} --add-flags "-p $out/share/noctalia-shell" ) ''; diff --git a/shell.qml b/shell.qml index 8217e7e6..35152cd7 100644 --- a/shell.qml +++ b/shell.qml @@ -9,13 +9,22 @@ // Qt & Quickshell Core import QtQuick import Quickshell +import Quickshell.Services.SystemTray // Commons & Services import qs.Commons -import qs.Services +import qs.Services.Control +import qs.Services.Theming +import qs.Services.Hardware +import qs.Services.Location +import qs.Services.Networking +import qs.Services.Power +import qs.Services.System +import qs.Services.UI // Modules import qs.Modules.Background +import qs.Modules.Bar import qs.Modules.Dock import qs.Modules.MainScreen import qs.Modules.LockScreen @@ -60,20 +69,20 @@ ShellRoot { sourceComponent: Item { Component.onCompleted: { Logger.i("Shell", "---------------------------") + SystemTrayService.init() WallpaperService.init() AppThemeService.init() ColorSchemeService.init() - BarWidgetRegistry.init() LocationService.init() NightLightService.apply() DarkModeService.init() - FontService.init() HooksService.init() BluetoothService.init() BatteryService.init() IdleInhibitorService.init() PowerProfileService.init() DistroService.init() + FontService.init() // Only open the setup wizard for new users if (!Settings.data.setupCompleted) { @@ -100,11 +109,23 @@ ShellRoot { // Item that needs to exists in the shell. IPCService {} - // MainScreen for each screen (manages bar + all panels) + // MainScreen for each screen AllScreens {} } } + // --------------------------------------------- + // Setup Wizard + // --------------------------------------------- + Timer { + id: setupWizardTimer + running: false + interval: 1000 + onTriggered: { + showSetupWizard() + } + } + function checkSetupWizard() { // Wait for distro service if (!DistroService.isReady) { @@ -119,7 +140,7 @@ ShellRoot { } if (Settings.data.settingsVersion >= Settings.settingsVersion) { - showSetupWizard() + setupWizardTimer.start() } else { Settings.data.setupCompleted = true } @@ -134,9 +155,7 @@ ShellRoot { setupPanel.open() } else { // If not yet loaded, ensure it loads and try again shortly - Qt.callLater(() => { - showSetupWizard() - }) + setupWizardTimer.restart() } } }