diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index a40b6585..0474b777 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -652,7 +652,8 @@ "logout": "Abmelden", "reboot": "Neu starten", "shutdown": "Herunterfahren", - "suspend": "Ruhezustand", + "suspend": "Energiesparmodus", + "hibernate": "Ruhezustand", "title": "Sitzungsmenü" }, "settings": { @@ -879,7 +880,7 @@ "programs": { "code": { "description": "Schreibe {Dateipfad}. Das Hyprluna-Theme muss manuell installiert und aktiviert werden", - "description-missing": "Benötigt die Installation von {app}" + "description-missing": "Kein Code-Client erkannt. Installieren Sie VSCode oder VSCodium." }, "description": "Anwendungsspezifisches Theming.", "discord": { @@ -1441,8 +1442,8 @@ "label": "Immer im Vordergrund" }, "background-opacity": { - "description": "Controls the transparency of the OSD background.", - "label": "Background opacity" + "description": "Kontrolliert die Transparenz des OSD Hintergrunds.", + "label": "Hintergrund-Transparenz" }, "description": "Bildschirm-Overlays wie Lautstärke- und Helligkeitsanzeigen konfigurieren.", "duration": { @@ -1567,6 +1568,15 @@ }, "title": "Sitzungsmenü" }, + "system-monitor": { + "general": { + "section": { + "description": "Konfigurieren Sie das Verhalten und Aussehen des Systemmonitors.", + "label": "Allgemein" + } + }, + "title": "Systemmonitor" + }, "user-interface": { "animation-disable": { "description": "Deaktivieren Sie alle Animationen für eine schnellere und reaktionsfreudigere Erfahrung.", diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 105cd572..b92c800e 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -653,6 +653,7 @@ "reboot": "Reboot", "shutdown": "Shutdown", "suspend": "Suspend", + "hibernate": "Hibernate", "title": "Session Menu" }, "settings": { @@ -879,7 +880,7 @@ "programs": { "code": { "description": "Write {filepath}. Hyprluna theme needs to be installed and activated manually.", - "description-missing": "Requires {app} to be installed" + "description-missing": "No Code client detected. Install VSCode or VSCodium." }, "description": "Application-specific theming.", "discord": { @@ -1567,6 +1568,15 @@ }, "title": "Session Menu" }, + "system-monitor": { + "general": { + "section": { + "description": "Configure the System Monitor behavior and appearance.", + "label": "General" + } + }, + "title": "System Monitor" + }, "user-interface": { "animation-disable": { "description": "Disable all animations for a faster, more responsive experience.", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index e92bc358..9076a78d 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -653,6 +653,7 @@ "reboot": "Reiniciar", "shutdown": "Apagar", "suspend": "Suspender", + "hibernate": "Hibernación", "title": "Menú de sesión" }, "settings": { @@ -879,7 +880,7 @@ "programs": { "code": { "description": "Escribe {filepath}. El tema Hyprluna debe ser instalado y activado manualmente.", - "description-missing": "Requiere que {app} esté instalado/a." + "description-missing": "No se detectó cliente de Code. Instala VSCode o VSCodium." }, "description": "Tematización específica de aplicaciones.", "discord": { @@ -1567,6 +1568,15 @@ }, "title": "Menú de sesión" }, + "system-monitor": { + "general": { + "section": { + "description": "Configurar el comportamiento y la apariencia del Monitor del sistema.", + "label": "General" + } + }, + "title": "Monitor del sistema" + }, "user-interface": { "animation-disable": { "description": "Desactiva todas las animaciones para una experiencia más rápida y con mayor capacidad de respuesta.", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 90d6e5f5..10e96498 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -653,6 +653,7 @@ "reboot": "Redémarrer", "shutdown": "Éteindre", "suspend": "Mettre en veille", + "hibernate": "Mise en veille prolongée", "title": "Menu de session" }, "settings": { @@ -879,7 +880,7 @@ "programs": { "code": { "description": "Écrire {filepath}. Le thème Hyprluna doit être installé et activé manuellement.", - "description-missing": "Nécessite l'installation de {app}" + "description-missing": "Aucun client Code détecté. Installez VSCode ou VSCodium." }, "description": "Thématisation spécifique aux applications.", "discord": { @@ -1567,6 +1568,15 @@ }, "title": "Menu de session" }, + "system-monitor": { + "general": { + "section": { + "description": "Configurer le comportement et l'apparence du Moniteur Système.", + "label": "Général" + } + }, + "title": "Moniteur système" + }, "user-interface": { "animation-disable": { "description": "Désactiver toutes les animations pour une expérience plus rapide et plus réactive.", diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index acd1897a..0568efee 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -653,6 +653,7 @@ "reboot": "Opnieuw opstarten", "shutdown": "Uitschakelen", "suspend": "Onderbreken", + "hibernate": "Slaperstand", "title": "Sessie-menu" }, "settings": { @@ -879,7 +880,7 @@ "programs": { "code": { "description": "Schrijf {filepath}. Het Hyprluna-thema moet handmatig worden geïnstalleerd en geactiveerd.", - "description-missing": "Vereist dat {app} is geïnstalleerd." + "description-missing": "Geen Code-client gedetecteerd. Installeer VSCode of VSCodium." }, "description": "Toepassingsspecifieke theming.", "discord": { @@ -1567,6 +1568,15 @@ }, "title": "Sessiemenu" }, + "system-monitor": { + "general": { + "section": { + "description": "Configureer het gedrag en de weergave van de Systeemmonitor.", + "label": "Algemeen" + } + }, + "title": "Systeemmonitor" + }, "user-interface": { "animation-disable": { "description": "Schakel alle animaties uit voor een snellere, responsievere ervaring.", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 642ae508..7d62b4ae 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -653,6 +653,7 @@ "reboot": "Reiniciar", "shutdown": "Desligar", "suspend": "Suspender", + "hibernate": "Hibernação", "title": "Menu da Sessão" }, "settings": { @@ -879,7 +880,7 @@ "programs": { "code": { "description": "Escreva em {filepath}. O tema Hyprluna precisa ser instalado e ativado manualmente.", - "description-missing": "Requer que o {app} esteja instalado." + "description-missing": "Nenhum cliente Code detectado. Instale VSCode ou VSCodium." }, "description": "Tematização específica de aplicativos.", "discord": { @@ -1567,6 +1568,15 @@ }, "title": "Menu de sessão" }, + "system-monitor": { + "general": { + "section": { + "description": "Configure o comportamento e a aparência do Monitor do Sistema.", + "label": "Geral" + } + }, + "title": "Monitor do Sistema" + }, "user-interface": { "animation-disable": { "description": "Desative todas as animações para uma experiência mais rápida e responsiva.", diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index f76968f5..64648c1f 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -653,6 +653,7 @@ "reboot": "Перезагрузить", "shutdown": "Выключить", "suspend": "Приостановить", + "hibernate": "Гибернация", "title": "Меню сеанса" }, "settings": { @@ -879,7 +880,7 @@ "programs": { "code": { "description": "Записать {filepath}. Тему Hyprluna нужно установить и активировать вручную.", - "description-missing": "Требуется установка {app}" + "description-missing": "Клиент Code не обнаружен. Установите VSCode или VSCodium." }, "description": "Тематика для конкретных приложений.", "discord": { @@ -1567,6 +1568,15 @@ }, "title": "Меню сеанса" }, + "system-monitor": { + "general": { + "section": { + "description": "Настроить поведение и внешний вид системного монитора.", + "label": "Общее" + } + }, + "title": "Системный монитор" + }, "user-interface": { "animation-disable": { "description": "Отключить все анимации для более быстрого и отзывчивого взаимодействия.", diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 5347d72a..30335a5c 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -653,6 +653,7 @@ "reboot": "Yeniden başlat", "shutdown": "Bilgisayarı kapat", "suspend": "Askıya al", + "hibernate": "Hazırda Bekletme", "title": "Oturum Menüsü" }, "settings": { @@ -879,7 +880,7 @@ "programs": { "code": { "description": "{filepath} dosyasına yaz. Hyprluna temasının kurulu ve manuel olarak etkinleştirilmiş olması gerekir.", - "description-missing": "Kurulum için {app} gereklidir" + "description-missing": "Code istemcisi tespit edilmedi. VSCode veya VSCodium kurun." }, "description": "Uygulamaya özel temalandırma.", "discord": { @@ -1567,6 +1568,15 @@ }, "title": "Oturum Menüsü" }, + "system-monitor": { + "general": { + "section": { + "description": "Sistem İzleyicisinin davranışını ve görünümünü yapılandırın.", + "label": "Genel" + } + }, + "title": "Sistem İzleme" + }, "user-interface": { "animation-disable": { "description": "Daha hızlı ve daha duyarlı bir deneyim için tüm animasyonları devre dışı bırakın.", diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index cbbb30b7..ef948d3e 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -653,6 +653,7 @@ "reboot": "Перезавантажити", "shutdown": "Вимкнути", "suspend": "Призупинити", + "hibernate": "Гібернація", "title": "Меню сеансу" }, "settings": { @@ -879,7 +880,7 @@ "programs": { "code": { "description": "Записати {filepath}. Тему Hyprluna потрібно встановити та активувати вручну.", - "description-missing": "Потрібна установка {app}" + "description-missing": "Клієнт Code не виявлено. Встановіть VSCode або VSCodium." }, "description": "Оформлення окремих програм.", "discord": { @@ -1567,6 +1568,15 @@ }, "title": "Меню сеансу" }, + "system-monitor": { + "general": { + "section": { + "description": "Налаштуйте поведінку та зовнішній вигляд Системного монітора.", + "label": "Загальне" + } + }, + "title": "Системний монітор" + }, "user-interface": { "animation-disable": { "description": "Вимкнути всі анімації для швидшого, більш відгукового досвіду.", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 1bc217de..8f16bd51 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -653,6 +653,7 @@ "reboot": "重启", "shutdown": "关机", "suspend": "挂起", + "hibernate": "休眠", "title": "会话菜单" }, "settings": { @@ -879,7 +880,7 @@ "programs": { "code": { "description": "写入 {filepath}。Hyprluna 主题需要手动安装和激活。", - "description-missing": "需要安装 {app}" + "description-missing": "未检测到 Code 客户端。请安装 VSCode 或 VSCodium。" }, "description": "应用程序特定主题。", "discord": { @@ -1474,7 +1475,6 @@ "description": "配置屏显菜单(OSD)的可见性与行为。", "label": "常规" } - }, "title": "屏显菜单" }, @@ -1568,6 +1568,15 @@ }, "title": "会话菜单" }, + "system-monitor": { + "general": { + "section": { + "description": "配置系统监视器的行为和外观。", + "label": "通用" + } + }, + "title": "系统监视器" + }, "user-interface": { "animation-disable": { "description": "禁用所有动画以获得更快、更流畅的体验。", diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 1c1806f3..320adebc 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -219,6 +219,10 @@ "action": "suspend", "enabled": true }, + { + "action": "hibernate", + "enabled": true + }, { "action": "reboot", "enabled": true diff --git a/Commons/IconsTabler.qml b/Commons/IconsTabler.qml index f700360b..2db54ae7 100644 --- a/Commons/IconsTabler.qml +++ b/Commons/IconsTabler.qml @@ -42,6 +42,7 @@ Singleton { "powersaver": "leaf", "storage": "database", "ethernet": "sitemap", + "ethernet-off": "sitemap-off", "keyboard": "keyboard", "shutdown": "power", "lock": "lock", @@ -49,6 +50,7 @@ Singleton { "logout": "logout", "reboot": "refresh", "suspend": "player-pause", + "hibernate": "zzz", "nightlight-on": "moon", "nightlight-off": "moon-off", "nightlight-forced": "moon-stars", @@ -123,6 +125,7 @@ Singleton { "settings-about": "info-square-rounded", "settings-lock-screen": "lock", "settings-session-menu": "power", + "settings-system-monitor": "activity", "bluetooth": "bluetooth", "bt-device-generic": "bluetooth", "bt-device-headphones": "headphones", diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 28045d96..d03d3a8b 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -352,6 +352,9 @@ Singleton { }, { "action": "suspend", "enabled": true + }, { + "action": "hibernate", + "enabled": true }, { "action": "reboot", "enabled": true @@ -436,12 +439,16 @@ Singleton { property bool spicetify: false property bool enableUserTemplates: false + property bool code_code: false + property bool code_codium: 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 + property bool discord_vencord: false // To be deleted soon } // night light @@ -654,7 +661,7 @@ Singleton { var anyDiscordEnabled = false // Check if any Discord client was enabled - const discordClients = ["discord_vesktop", "discord_webcord", "discord_armcord", "discord_equibop", "discord_lightcord", "discord_dorion"] + const discordClients = ["discord_vesktop", "discord_webcord", "discord_armcord", "discord_equibop", "discord_lightcord", "discord_dorion", "discord_vencord"] for (var i = 0; i < discordClients.length; i++) { if (adapter.templates[discordClients[i]]) { diff --git a/Modules/Bar/Extras/TrayMenu.qml b/Modules/Bar/Extras/TrayMenu.qml index 05729c38..f4e17e78 100644 --- a/Modules/Bar/Extras/TrayMenu.qml +++ b/Modules/Bar/Extras/TrayMenu.qml @@ -8,13 +8,15 @@ import qs.Widgets PopupWindow { id: root + + property ShellScreen screen + property var trayItem: null property var anchorItem: null property real anchorX property real anchorY property bool isSubMenu: false property bool isHovered: rootMouseArea.containsMouse - property ShellScreen screen property string widgetSection: "" property int widgetIndex: -1 @@ -45,7 +47,7 @@ PopupWindow { implicitWidth: menuWidth // Use the content height of the Flickable for implicit height - implicitHeight: Math.min(screen.height * 0.9, flickable.contentHeight + (Style.marginS * 2)) + implicitHeight: Math.min(screen?.height * 0.9, flickable.contentHeight + (Style.marginS * 2)) visible: false color: Color.transparent anchor.item: anchorItem diff --git a/Modules/Bar/Widgets/ControlCenter.qml b/Modules/Bar/Widgets/ControlCenter.qml index 02dbff04..789bdeb7 100644 --- a/Modules/Bar/Widgets/ControlCenter.qml +++ b/Modules/Bar/Widgets/ControlCenter.qml @@ -71,7 +71,7 @@ NIconButton { if (customIconPath !== "") return customIconPath.startsWith("file://") ? customIconPath : "file://" + customIconPath if (useDistroLogo) - return DistroService.osLogo + return HostService.osLogo return "" } visible: source !== "" diff --git a/Modules/Bar/Widgets/TaskbarGrouped.qml b/Modules/Bar/Widgets/TaskbarGrouped.qml index d6f7f765..a30d16db 100644 --- a/Modules/Bar/Widgets/TaskbarGrouped.qml +++ b/Modules/Bar/Widgets/TaskbarGrouped.qml @@ -33,14 +33,21 @@ Item { } return {} } - readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : false + readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode + readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied + readonly property int characterCount: 2 readonly property bool showWorkspaceNumbers: (widgetSettings.showWorkspaceNumbers !== undefined) ? widgetSettings.showWorkspaceNumbers : true readonly property bool showNumbersOnlyWhenOccupied: (widgetSettings.showNumbersOnlyWhenOccupied !== undefined) ? widgetSettings.showNumbersOnlyWhenOccupied : true + readonly property bool colorizeIcons: (widgetSettings.colorizeIcons !== undefined) ? widgetSettings.colorizeIcons : widgetMetadata.colorizeIcons property ListModel localWorkspaces: ListModel {} property real masterProgress: 0.0 property bool effectsActive: false property color effectColor: Color.mPrimary + // Wheel scroll handling + property int wheelAccumulatedDelta: 0 + property bool wheelCooldown: false + function refreshWorkspaces() { localWorkspaces.clear() if (!screen) @@ -80,11 +87,34 @@ Item { } } + function getFocusedLocalIndex() { + for (var i = 0; i < localWorkspaces.count; i++) { + if (localWorkspaces.get(i).isFocused === true) + return i + } + return -1 + } + + function switchByOffset(offset) { + if (localWorkspaces.count === 0) + return + var current = getFocusedLocalIndex() + if (current < 0) + current = 0 + var next = (current + offset) % localWorkspaces.count + if (next < 0) + next = localWorkspaces.count - 1 + const ws = localWorkspaces.get(next) + if (ws && ws.idx !== undefined) + CompositorService.switchToWorkspace(ws) + } + Component.onCompleted: { refreshWorkspaces() } onScreenChanged: refreshWorkspaces() + onHideUnoccupiedChanged: refreshWorkspaces() implicitWidth: isVerticalBar ? taskbarGrid.implicitWidth + Style.marginM * 2 : Math.round(taskbarGrid.implicitWidth + Style.marginM * 2) implicitHeight: isVerticalBar ? Math.round(taskbarGrid.implicitHeight + Style.marginM * 2) : Style.barHeight @@ -128,6 +158,46 @@ Item { } } + // Debounce timer for wheel interactions + Timer { + id: wheelDebounce + interval: 150 + repeat: false + onTriggered: { + root.wheelCooldown = false + root.wheelAccumulatedDelta = 0 + } + } + + // Scroll to switch workspaces + WheelHandler { + id: wheelHandler + target: root + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: function (event) { + if (root.wheelCooldown) + return + // Prefer vertical delta, fall back to horizontal if needed + var dy = event.angleDelta.y + var dx = event.angleDelta.x + var useDy = Math.abs(dy) >= Math.abs(dx) + var delta = useDy ? dy : dx + // One notch is typically 120 + root.wheelAccumulatedDelta += delta + var step = 120 + if (Math.abs(root.wheelAccumulatedDelta) >= step) { + var direction = root.wheelAccumulatedDelta > 0 ? -1 : 1 + // For vertical layout, natural mapping: wheel up -> previous, down -> next (already handled by sign) + // For horizontal layout, same mapping using vertical wheel + root.switchByOffset(direction) + root.wheelCooldown = true + wheelDebounce.restart() + root.wheelAccumulatedDelta = 0 + event.accepted = true + } + } + } + Component { id: workspaceRepeaterDelegate @@ -192,7 +262,7 @@ Item { smooth: true asynchronous: true opacity: model.isFocused ? Style.opacityFull : 0.6 - layer.enabled: widgetSettings.colorizeIcons === true + layer.enabled: root.colorizeIcons && !model.isFocused Behavior on opacity { NumberAnimation { @@ -252,7 +322,7 @@ Item { Item { id: workspaceNumberContainer - visible: root.showWorkspaceNumbers && (!root.showNumbersOnlyWhenOccupied || container.hasWindows) + visible: root.labelMode !== "none" && root.showWorkspaceNumbers && (!root.showNumbersOnlyWhenOccupied || container.hasWindows) anchors { left: parent.left @@ -318,7 +388,13 @@ Item { anchors.centerIn: parent - text: workspaceModel.idx.toString() + text: { + if (root.labelMode === "name" && workspaceModel.name && workspaceModel.name.length > 0) { + return workspaceModel.name.substring(0, root.characterCount) + } else { + return workspaceModel.idx.toString() + } + } family: Settings.data.ui.fontFixed font { diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 6d914e85..921a66c4 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -396,8 +396,8 @@ Rectangle { 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 + colorBg: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent + colorFg: Color.mOnSurface colorBorder: Color.transparent colorBorderHover: Color.transparent icon: { diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 2c23542b..2508b0ba 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -41,7 +41,7 @@ Item { icon: { try { if (NetworkService.ethernetConnected) { - return "ethernet" + return NetworkService.internetConnectivity ? "ethernet" : "ethernet-off" } let connected = false let signalStrength = 0 @@ -55,7 +55,7 @@ Item { return connected ? NetworkService.signalIcon(signalStrength, true) : "wifi-off" } catch (error) { Logger.e("Wi-Fi", "Error getting icon:", error) - return "signal_wifi_bad" + return "wifi-off" } } text: { diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 4c87d1ca..4bf8b4d5 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -1188,6 +1188,54 @@ Loader { } } + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 + radius: Settings.data.general.compactLockScreen ? 18 : 24 + color: hibernateButtonArea.containsMouse ? Color.mHover : "transparent" + border.color: Color.mOutline + border.width: 1 + + RowLayout { + anchors.centerIn: parent + spacing: 6 + + NIcon { + icon: "hibernate" + pointSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL + color: hibernateButtonArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant + } + + NText { + text: I18n.tr("session-menu.hibernate") + pointSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM + color: hibernateButtonArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant + font.weight: Font.Medium + } + } + + MouseArea { + id: hibernateButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: CompositorService.hibernate() + } + + Behavior on color { + ColorAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + + Behavior on border.color { + ColorAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + Rectangle { Layout.fillWidth: true Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 diff --git a/Modules/MainScreen/Backgrounds/AllBackgrounds.qml b/Modules/MainScreen/Backgrounds/AllBackgrounds.qml index bd5e082e..50123f9a 100644 --- a/Modules/MainScreen/Backgrounds/AllBackgrounds.qml +++ b/Modules/MainScreen/Backgrounds/AllBackgrounds.qml @@ -23,6 +23,8 @@ Item { // Reference to MainScreen (for panel access) required property var windowRoot + readonly property color panelBackgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + anchors.fill: parent // Wrapper with layer caching for better shadow performance @@ -69,91 +71,91 @@ Item { PanelBackground { panel: root.windowRoot.audioPanelPlaceholder shapeContainer: backgroundsShape - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + backgroundColor: panelBackgroundColor } // Battery PanelBackground { panel: root.windowRoot.batteryPanelPlaceholder shapeContainer: backgroundsShape - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + backgroundColor: panelBackgroundColor } // Bluetooth PanelBackground { panel: root.windowRoot.bluetoothPanelPlaceholder shapeContainer: backgroundsShape - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + backgroundColor: panelBackgroundColor } // Calendar PanelBackground { panel: root.windowRoot.calendarPanelPlaceholder shapeContainer: backgroundsShape - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + backgroundColor: panelBackgroundColor } // Control Center PanelBackground { panel: root.windowRoot.controlCenterPanelPlaceholder shapeContainer: backgroundsShape - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + backgroundColor: panelBackgroundColor } // Launcher PanelBackground { panel: root.windowRoot.launcherPanelPlaceholder shapeContainer: backgroundsShape - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + backgroundColor: panelBackgroundColor } // Notification History PanelBackground { panel: root.windowRoot.notificationHistoryPanelPlaceholder shapeContainer: backgroundsShape - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + backgroundColor: panelBackgroundColor } // Session Menu PanelBackground { panel: root.windowRoot.sessionMenuPanelPlaceholder shapeContainer: backgroundsShape - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + backgroundColor: panelBackgroundColor } // Settings PanelBackground { panel: root.windowRoot.settingsPanelPlaceholder shapeContainer: backgroundsShape - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + backgroundColor: panelBackgroundColor } // Setup Wizard PanelBackground { panel: root.windowRoot.setupWizardPanelPlaceholder shapeContainer: backgroundsShape - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + backgroundColor: panelBackgroundColor } // TrayDrawer PanelBackground { panel: root.windowRoot.trayDrawerPanelPlaceholder shapeContainer: backgroundsShape - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + backgroundColor: panelBackgroundColor } // Wallpaper PanelBackground { panel: root.windowRoot.wallpaperPanelPlaceholder shapeContainer: backgroundsShape - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + backgroundColor: panelBackgroundColor } // WiFi PanelBackground { panel: root.windowRoot.wifiPanelPlaceholder shapeContainer: backgroundsShape - backgroundColor: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + backgroundColor: panelBackgroundColor } } diff --git a/Modules/MainScreen/MainScreen.qml b/Modules/MainScreen/MainScreen.qml index 2c15f406..3f51f134 100644 --- a/Modules/MainScreen/MainScreen.qml +++ b/Modules/MainScreen/MainScreen.qml @@ -67,7 +67,7 @@ PanelWindow { // Wayland WlrLayershell.layer: WlrLayer.Top - WlrLayershell.namespace: "noctalia-screen-" + (screen?.name || "unknown") + WlrLayershell.namespace: "noctalia-background-" + (screen?.name || "unknown") WlrLayershell.exclusionMode: ExclusionMode.Ignore // Don't reserve space - BarExclusionZone handles that WlrLayershell.keyboardFocus: WlrKeyboardFocus.None diff --git a/Modules/MainScreen/SmartPanel.qml b/Modules/MainScreen/SmartPanel.qml index 6ea5950b..7574beeb 100644 --- a/Modules/MainScreen/SmartPanel.qml +++ b/Modules/MainScreen/SmartPanel.qml @@ -15,8 +15,6 @@ import qs.Services.UI Item { id: root - // PUBLIC API - unchanged for backward compatibility - // Screen property provided by MainScreen property ShellScreen screen: null @@ -63,7 +61,7 @@ Item { // Note: SmartPanelWindow directly calls these functions via panelWrapper reference function onEscapePressed() {} function onTabPressed() {} - function onShiftTabPressed() {} + function onBackTabPressed() {} function onUpPressed() {} function onDownPressed() {} function onLeftPressed() {} diff --git a/Modules/MainScreen/SmartPanelWindow.qml b/Modules/MainScreen/SmartPanelWindow.qml index ba490a3e..40a52a90 100644 --- a/Modules/MainScreen/SmartPanelWindow.qml +++ b/Modules/MainScreen/SmartPanelWindow.qml @@ -191,12 +191,11 @@ PanelWindow { } else if (event.key === Qt.Key_Right && panelWrapper.onRightPressed) { panelWrapper.onRightPressed() event.accepted = true - } else if (event.key === Qt.Key_Tab) { - if (event.modifiers & Qt.ShiftModifier && panelWrapper.onShiftTabPressed) { - panelWrapper.onShiftTabPressed() - } else if (panelWrapper.onTabPressed) { - panelWrapper.onTabPressed() - } + } else if (event.key === Qt.Key_Tab && panelWrapper.onTabPressed) { + panelWrapper.onTabPressed() + event.accepted = true + } else if (event.key === Qt.Key_Backtab && panelWrapper.onBackTabPressed) { + panelWrapper.onBackTabPressed() event.accepted = true } else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && panelWrapper.onReturnPressed) { panelWrapper.onReturnPressed() diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index 1409d545..d444bdca 100644 --- a/Modules/OSD/OSD.qml +++ b/Modules/OSD/OSD.qml @@ -49,83 +49,83 @@ Variants { function getIcon() { if (currentOSDType === "volume") { if (AudioService.muted) { - return "volume-mute"; + return "volume-mute" } - return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"; + return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high" } else if (currentOSDType === "inputVolume") { if (AudioService.inputMuted) { - return "microphone-off"; + return "microphone-off" } - return "microphone"; + return "microphone" } else if (currentOSDType === "brightness") { - return currentBrightness <= 0.5 ? "brightness-low" : "brightness-high"; + return currentBrightness <= 0.5 ? "brightness-low" : "brightness-high" } - return ""; + return "" } // Get current value (0-1 range) function getCurrentValue() { if (currentOSDType === "volume") { - return isMuted ? 0 : currentVolume; + return isMuted ? 0 : currentVolume } else if (currentOSDType === "inputVolume") { - return isInputMuted ? 0 : currentInputVolume; + return isInputMuted ? 0 : currentInputVolume } else if (currentOSDType === "brightness") { - return currentBrightness; + return currentBrightness } - return 0; + return 0 } // Get maximum value for current OSD type function getMaxValue() { if (currentOSDType === "volume" || currentOSDType === "inputVolume") { - return Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + return Settings.data.audio.volumeOverdrive ? 1.5 : 1.0 } else if (currentOSDType === "brightness") { - return 1.0; + return 1.0 } - return 1.0; + return 1.0 } // Get display percentage function getDisplayPercentage() { if (currentOSDType === "volume") { if (isMuted) - return "0%"; - const max = getMaxValue(); - const pct = Math.round(Math.min(max, currentVolume) * 100); - return pct + "%"; + return "0%" + const max = getMaxValue() + const pct = Math.round(Math.min(max, currentVolume) * 100) + return pct + "%" } else if (currentOSDType === "inputVolume") { if (isInputMuted) - return "0%"; - const max = getMaxValue(); - const pct = Math.round(Math.min(max, currentInputVolume) * 100); - return pct + "%"; + return "0%" + const max = getMaxValue() + const pct = Math.round(Math.min(max, currentInputVolume) * 100) + return pct + "%" } else if (currentOSDType === "brightness") { - const pct = Math.round(Math.min(1.0, currentBrightness) * 100); - return pct + "%"; + const pct = Math.round(Math.min(1.0, currentBrightness) * 100) + return pct + "%" } - return ""; + return "" } // Get progress bar color function getProgressColor() { if (currentOSDType === "volume") { if (isMuted) - return Color.mError; - return Color.mPrimary; + return Color.mError + return Color.mPrimary } else if (currentOSDType === "inputVolume") { if (isInputMuted) - return Color.mError; - return Color.mPrimary; + return Color.mError + return Color.mPrimary } - return Color.mPrimary; + return Color.mPrimary } // Get icon color function getIconColor() { if ((currentOSDType === "volume" && isMuted) || (currentOSDType === "inputVolume" && isInputMuted)) { - return Color.mError; + return Color.mError } - return Color.mOnSurface; + return Color.mOnSurface } sourceComponent: PanelWindow { @@ -146,8 +146,8 @@ Variants { // Ensure an even width to keep the vertical bar perfectly centered readonly property int barThickness: { - const base = Math.max(8, Math.round(8 * Style.uiScaleRatio)); - return (base % 2 === 0) ? base : base + 1; + const base = Math.max(8, Math.round(8 * Style.uiScaleRatio)) + return (base % 2 === 0) ? base : base + 1 } // Anchor selection based on location (window edges) @@ -159,46 +159,46 @@ Variants { // Margins depending on bar position and chosen location margins.top: { if (!(anchors.top)) - return 0; - var base = Style.marginM; + 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; + var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0 + return Style.barHeight + base + floatExtraV } - return base; + return base } margins.bottom: { if (!(anchors.bottom)) - return 0; - var base = Style.marginM; + 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; + var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0 + return Style.barHeight + base + floatExtraV } - return base; + return base } margins.left: { if (!(anchors.left)) - return 0; - var base = Style.marginM; + 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; + var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + return Style.barHeight + base + floatExtraH } - return base; + return base } margins.right: { if (!(anchors.right)) - return 0; - var base = Style.marginM; + 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; + var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + return Style.barHeight + base + floatExtraH } - return base; + return base } implicitWidth: verticalMode ? vWidth : hWidth @@ -249,10 +249,10 @@ Variants { id: visibilityTimer interval: Style.animationNormal + 50 // Add small buffer onTriggered: { - osdItem.visible = false; - root.currentOSDType = ""; + osdItem.visible = false + root.currentOSDType = "" // Deactivate the loader when done - root.active = false; + root.active = false } } @@ -265,8 +265,8 @@ Variants { color: Qt.alpha(Color.mSurface, Settings.data.osd.backgroundOpacity || 1.0) border.color: Qt.alpha(Color.mOutline, Settings.data.osd.backgroundOpacity || 1.0) border.width: { - const bw = Math.max(2, Style.borderM); - return (bw % 2 === 0) ? bw : bw + 1; + const bw = Math.max(2, Style.borderM) + return (bw % 2 === 0) ? bw : bw + 1 } } @@ -365,9 +365,9 @@ Variants { ColumnLayout { // Ensure inner padding respects the rounded corners; avoid clipping the icon/text property int vMargin: { - const styleMargin = Style.marginL; - const cornerGuard = Math.round(osdItem.radius); - return Math.max(styleMargin, cornerGuard); + const styleMargin = Style.marginL + const cornerGuard = Math.round(osdItem.radius) + return Math.max(styleMargin, cornerGuard) } property int vMarginTop: Math.round(Math.max(osdItem.radius, Style.marginS)) property int balanceDelta: Style.marginS @@ -449,46 +449,46 @@ Variants { function show() { // Cancel any pending hide operations - hideTimer.stop(); - visibilityTimer.stop(); + hideTimer.stop() + visibilityTimer.stop() // Make visible and animate in - osdItem.visible = true; + osdItem.visible = true // Use Qt.callLater to ensure the visible change is processed before animation Qt.callLater(() => { - osdItem.opacity = 1; - osdItem.scale = 1.0; - }); + osdItem.opacity = 1 + osdItem.scale = 1.0 + }) // Start the auto-hide timer - hideTimer.start(); + hideTimer.start() } function hide() { - hideTimer.stop(); - visibilityTimer.stop(); + hideTimer.stop() + visibilityTimer.stop() // Start fade out animation - osdItem.opacity = 0; - osdItem.scale = 0.85; // Less dramatic scale change for smoother effect + osdItem.opacity = 0 + osdItem.scale = 0.85 // Less dramatic scale change for smoother effect // Delay hiding the element until after animation completes - visibilityTimer.start(); + visibilityTimer.start() } function hideImmediately() { - hideTimer.stop(); - visibilityTimer.stop(); - osdItem.opacity = 0; - osdItem.scale = 0.85; - osdItem.visible = false; - root.currentOSDType = ""; - root.active = false; + hideTimer.stop() + visibilityTimer.stop() + osdItem.opacity = 0 + osdItem.scale = 0.85 + osdItem.visible = false + root.currentOSDType = "" + root.active = false } } function showOSD() { - osdItem.show(); + osdItem.show() } } @@ -498,34 +498,34 @@ Variants { function onVolumeChanged() { if (volumeInitialized) { - showOSD("volume"); + showOSD("volume") } } function onMutedChanged() { if (muteInitialized) { - showOSD("volume"); + showOSD("volume") } } function onInputVolumeChanged() { if (!inputAudioInitialized) { - return; + return } if (!AudioService.hasInput) { - return; + return } - showOSD("inputVolume"); + showOSD("inputVolume") } function onInputMutedChanged() { if (!inputAudioInitialized) { - return; + return } if (!AudioService.hasInput) { - return; + return } - showOSD("inputVolume"); + showOSD("inputVolume") } } @@ -535,11 +535,11 @@ Variants { interval: 500 running: true onTriggered: { - volumeInitialized = true; - muteInitialized = true; - inputAudioInitialized = true; + volumeInitialized = true + muteInitialized = true + inputAudioInitialized = true // Brightness initializes on first change to avoid showing OSD on startup - connectBrightnessMonitors(); + connectBrightnessMonitors() } } @@ -548,65 +548,65 @@ Variants { target: BrightnessService function onMonitorsChanged() { - connectBrightnessMonitors(); + connectBrightnessMonitors() } } function disconnectBrightnessMonitors() { for (var i = 0; i < BrightnessService.monitors.length; i++) { - let monitor = BrightnessService.monitors[i]; - monitor.brightnessUpdated.disconnect(onBrightnessChanged); + let monitor = BrightnessService.monitors[i] + monitor.brightnessUpdated.disconnect(onBrightnessChanged) } } function connectBrightnessMonitors() { for (var i = 0; i < BrightnessService.monitors.length; i++) { - let monitor = BrightnessService.monitors[i]; + let monitor = BrightnessService.monitors[i] // Disconnect first to avoid duplicate connections - monitor.brightnessUpdated.disconnect(onBrightnessChanged); - monitor.brightnessUpdated.connect(onBrightnessChanged); + monitor.brightnessUpdated.disconnect(onBrightnessChanged) + monitor.brightnessUpdated.connect(onBrightnessChanged) } } function onBrightnessChanged(newBrightness) { - root.lastUpdatedBrightness = newBrightness; + root.lastUpdatedBrightness = newBrightness if (!brightnessInitialized) { - brightnessInitialized = true; - return; + brightnessInitialized = true + return } - showOSD("brightness"); + showOSD("brightness") } function showOSD(type) { // Update the current OSD type - currentOSDType = type; + currentOSDType = type // Activate the loader if not already active if (!root.active) { - root.active = true; + root.active = true } // Show the OSD (may need to wait for loader to create the item) if (root.item) { - root.item.showOSD(); + root.item.showOSD() } else { // If item not ready yet, wait for it Qt.callLater(() => { - if (root.item) { - root.item.showOSD(); - } - }); + if (root.item) { + root.item.showOSD() + } + }) } } function hideOSD() { if (root.item && root.item.osdItem) { - root.item.osdItem.hideImmediately(); + root.item.osdItem.hideImmediately() } else if (root.active) { // If loader is active but item isn't ready, just deactivate - root.active = false; + root.active = false } } } diff --git a/Modules/Panels/ControlCenter/Widgets/WiFi.qml b/Modules/Panels/ControlCenter/Widgets/WiFi.qml index 2ea25141..fae2b122 100644 --- a/Modules/Panels/ControlCenter/Widgets/WiFi.qml +++ b/Modules/Panels/ControlCenter/Widgets/WiFi.qml @@ -11,7 +11,7 @@ NIconButtonHot { icon: { try { if (NetworkService.ethernetConnected) { - return "ethernet" + return NetworkService.internetConnectivity ? "ethernet" : "ethernet-off" } let connected = false let signalStrength = 0 @@ -25,7 +25,7 @@ NIconButtonHot { return connected ? NetworkService.signalIcon(signalStrength, true) : "wifi-off" } catch (error) { Logger.e("Wi-Fi", "Error getting icon:", error) - return "signal_wifi_bad" + return "wifi-off" } } diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index 3faf2bb3..88d07bb2 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -50,12 +50,15 @@ SmartPanel { readonly property int badgeSize: Math.round(Style.baseWidgetSize * 1.6) readonly property int entryHeight: Math.round(badgeSize + Style.marginM * 2) - // Override keyboard handlers from SmartPanel for navigation + // Override keyboard handlers from SmartPanel for navigation. + // Launcher specific: onTabPressed() and onBackTabPressed() are special here. + // They are not coming from SmartPanelWindow as they are consumed by the search field before reaching the panel. + // They are instead being forwared from the search field NTextInput below. function onTabPressed() { selectNextWrapped() } - function onShiftTabPressed() { + function onBackTabPressed() { selectPreviousWrapped() } @@ -346,6 +349,16 @@ SmartPanel { Component.onCompleted: { if (searchInput.inputItem) { searchInput.inputItem.forceActiveFocus() + // Intercept Tab keys before TextField handles them + searchInput.inputItem.Keys.onPressed.connect(function (event) { + if (event.key === Qt.Key_Tab) { + root.onTabPressed() + event.accepted = true + } else if (event.key === Qt.Key_Backtab) { + root.onBackTabPressed() + event.accepted = true + } + }) } } } diff --git a/Modules/Panels/SessionMenu/SessionMenu.qml b/Modules/Panels/SessionMenu/SessionMenu.qml index 07b071ed..b72d0200 100644 --- a/Modules/Panels/SessionMenu/SessionMenu.qml +++ b/Modules/Panels/SessionMenu/SessionMenu.qml @@ -65,6 +65,11 @@ SmartPanel { "title": I18n.tr("session-menu.suspend"), "isShutdown": false }, + "hibernate": { + "icon": "hibernate", + "title": I18n.tr("session-menu.hibernate"), + "isShutdown": false + }, "reboot": { "icon": "reboot", "title": I18n.tr("session-menu.reboot"), @@ -159,6 +164,9 @@ SmartPanel { CompositorService.suspend() } break + case "hibernate": + CompositorService.hibernate() + break case "reboot": CompositorService.reboot() break @@ -220,7 +228,7 @@ SmartPanel { selectNextWrapped() } - function onShiftTabPressed() { + function onBackTabPressed() { selectPreviousWrapped() } diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/TaskbarGroupedSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/TaskbarGroupedSettings.qml index 09342c35..2f5ac5d6 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/TaskbarGroupedSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/TaskbarGroupedSettings.qml @@ -12,16 +12,56 @@ ColumnLayout { property var widgetData: null property var widgetMetadata: null - property bool valueShowWorkspaceNumbers: widgetData.showWorkspaceNumbers !== undefined ? widgetData.showWorkspaceNumbers : widgetMetadata.showWorkspaceNumbers - property bool valueShowNumbersOnlyWhenOccupied: widgetData.showNumbersOnlyWhenOccupied !== undefined ? widgetData.showNumbersOnlyWhenOccupied : widgetMetadata.showNumbersOnlyWhenOccupied + property string valueLabelMode: widgetData.labelMode !== undefined ? widgetData.labelMode : (widgetMetadata ? widgetMetadata.labelMode : "index") + property bool valueHideUnoccupied: widgetData.hideUnoccupied !== undefined ? widgetData.hideUnoccupied : (widgetMetadata ? widgetMetadata.hideUnoccupied : false) + property bool valueShowWorkspaceNumbers: widgetData.showWorkspaceNumbers !== undefined ? widgetData.showWorkspaceNumbers : (widgetMetadata ? widgetMetadata.showWorkspaceNumbers : true) + property bool valueShowNumbersOnlyWhenOccupied: widgetData.showNumbersOnlyWhenOccupied !== undefined ? widgetData.showNumbersOnlyWhenOccupied : (widgetMetadata ? widgetMetadata.showNumbersOnlyWhenOccupied : true) + property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : (widgetMetadata ? widgetMetadata.colorizeIcons : false) function saveSettings() { var settings = Object.assign({}, widgetData || {}) + settings.labelMode = valueLabelMode + settings.hideUnoccupied = valueHideUnoccupied settings.showWorkspaceNumbers = valueShowWorkspaceNumbers settings.showNumbersOnlyWhenOccupied = valueShowNumbersOnlyWhenOccupied + settings.colorizeIcons = valueColorizeIcons return settings } + NComboBox { + id: labelModeCombo + label: I18n.tr("bar.widget-settings.workspace.label-mode.label") + description: I18n.tr("bar.widget-settings.workspace.label-mode.description") + model: [{ + "key": "none", + "name": I18n.tr("options.workspace-labels.none") + }, { + "key": "index", + "name": I18n.tr("options.workspace-labels.index") + }, { + "key": "name", + "name": I18n.tr("options.workspace-labels.name") + }] + currentKey: widgetData.labelMode || widgetMetadata.labelMode + onSelected: key => valueLabelMode = key + minimumWidth: 200 + } + + NToggle { + label: I18n.tr("bar.widget-settings.workspace.hide-unoccupied.label") + description: I18n.tr("bar.widget-settings.workspace.hide-unoccupied.description") + checked: valueHideUnoccupied + onToggled: checked => valueHideUnoccupied = checked + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.widget-settings.active-window.colorize-icons.label") + description: I18n.tr("bar.widget-settings.active-window.colorize-icons.description") + checked: root.valueColorizeIcons + onToggled: checked => root.valueColorizeIcons = checked + } + NToggle { Layout.fillWidth: true label: I18n.tr("bar.widget-settings.taskbar-grouped.show-workspace-numbers.label") diff --git a/Modules/Panels/Settings/SettingsPanel.qml b/Modules/Panels/Settings/SettingsPanel.qml index 58c39833..c0dda187 100644 --- a/Modules/Panels/Settings/SettingsPanel.qml +++ b/Modules/Panels/Settings/SettingsPanel.qml @@ -77,6 +77,7 @@ SmartPanel { Notifications, ScreenRecorder, SessionMenu, + SystemMonitor, UserInterface, Wallpaper } @@ -166,6 +167,10 @@ SmartPanel { id: sessionMenuTab SessionMenuTab {} } + Component { + id: systemMonitorTab + SystemMonitorTab {} + } // Order *DOES* matter function updateTabsModel() { @@ -179,31 +184,56 @@ SmartPanel { "label": "settings.user-interface.title", "icon": "settings-user-interface", "source": userInterfaceTab + }, { + "id": SettingsPanel.Tab.ColorScheme, + "label": "settings.color-scheme.title", + "icon": "settings-color-scheme", + "source": colorSchemeTab + }, { + "id": SettingsPanel.Tab.Wallpaper, + "label": "settings.wallpaper.title", + "icon": "settings-wallpaper", + "source": wallpaperTab }, { "id": SettingsPanel.Tab.Bar, "label": "settings.bar.title", "icon": "settings-bar", "source": barTab - }, { - "id": SettingsPanel.Tab.ControlCenter, - "label": "settings.control-center.title", - "icon": "settings-control-center", - "source": controlCenterTab }, { "id": SettingsPanel.Tab.Dock, "label": "settings.dock.title", "icon": "settings-dock", "source": dockTab + }, { + "id": SettingsPanel.Tab.ControlCenter, + "label": "settings.control-center.title", + "icon": "settings-control-center", + "source": controlCenterTab }, { "id": SettingsPanel.Tab.Launcher, "label": "settings.launcher.title", "icon": "settings-launcher", "source": launcherTab + }, { + "id": SettingsPanel.Tab.Notifications, + "label": "settings.notifications.title", + "icon": "settings-notifications", + "source": notificationsTab + }, { + "id": SettingsPanel.Tab.OSD, + "label": "settings.osd.title", + "icon": "settings-osd", + "source": osdTab }, { "id": SettingsPanel.Tab.LockScreen, "label": "settings.lock-screen.title", "icon": "settings-lock-screen", "source": lockScreenTab + }, { + "id": SettingsPanel.Tab.SessionMenu, + "label": "settings.session-menu.title", + "icon": "settings-session-menu", + "source": sessionMenuTab }, { "id": SettingsPanel.Tab.Audio, "label": "settings.audio.title", @@ -214,16 +244,6 @@ SmartPanel { "label": "settings.display.title", "icon": "settings-display", "source": displayTab - }, { - "id": SettingsPanel.Tab.OSD, - "label": "settings.osd.title", - "icon": "settings-osd", - "source": osdTab - }, { - "id": SettingsPanel.Tab.Notifications, - "label": "settings.notifications.title", - "icon": "settings-notifications", - "source": notificationsTab }, { "id": SettingsPanel.Tab.Network, "label": "settings.network.title", @@ -234,31 +254,27 @@ SmartPanel { "label": "settings.location.title", "icon": "settings-location", "source": locationTab - }, { - "id": SettingsPanel.Tab.ColorScheme, - "label": "settings.color-scheme.title", - "icon": "settings-color-scheme", - "source": colorSchemeTab - }, { - "id": SettingsPanel.Tab.Wallpaper, - "label": "settings.wallpaper.title", - "icon": "settings-wallpaper", - "source": wallpaperTab }, { "id": SettingsPanel.Tab.ScreenRecorder, "label": "settings.screen-recorder.title", "icon": "settings-screen-recorder", "source": screenRecorderTab - }, { - "id": SettingsPanel.Tab.Hooks, - "label": "settings.hooks.title", - "icon": "settings-hooks", - "source": hooksTab }, { "id": SettingsPanel.Tab.SessionMenu, "label": "settings.session-menu.title", "icon": "settings-session-menu", "source": sessionMenuTab + }, // { + // "id": SettingsPanel.Tab.SystemMonitor, + // "label": "settings.system-monitor.title", + // "icon": "settings-system-monitor", + // "source": systemMonitorTab + // }, + { + "id": SettingsPanel.Tab.Hooks, + "label": "settings.hooks.title", + "icon": "settings-hooks", + "source": hooksTab }, { "id": SettingsPanel.Tab.About, "label": "settings.about.title", @@ -340,7 +356,7 @@ SmartPanel { selectNextTab() } - function onShiftTabPressed() { + function onBackTabPressed() { selectPreviousTab() } @@ -401,14 +417,14 @@ SmartPanel { anchors.fill: parent anchors.margins: Style.marginS model: root.tabsModel - spacing: Style.marginXS + spacing: Style.marginXXS currentIndex: root.currentTabIndex verticalPolicy: ScrollBar.AsNeeded delegate: Rectangle { id: tabItem width: sidebarList.verticalScrollBarActive ? sidebarList.width - sidebarList.scrollBarWidth - Style.marginXS : sidebarList.width - height: tabEntryRow.implicitHeight + Style.marginM * 2 + height: tabEntryRow.implicitHeight + Style.marginS * 2 radius: Style.radiusS color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mHover : Color.transparent) readonly property bool selected: index === root.currentTabIndex diff --git a/Modules/Panels/Settings/Tabs/BarTab.qml b/Modules/Panels/Settings/Tabs/BarTab.qml index 5e0c2fad..f39d11a9 100644 --- a/Modules/Panels/Settings/Tabs/BarTab.qml +++ b/Modules/Panels/Settings/Tabs/BarTab.qml @@ -24,22 +24,6 @@ ColumnLayout { }) } - // Handler for drag start - disables panel background clicks - function handleDragStart() { - var panel = PanelService.getPanel("settingsPanel", screen) - if (panel && panel.disableBackgroundClick) { - panel.disableBackgroundClick() - } - } - - // Handler for drag end - re-enables panel background clicks - function handleDragEnd() { - var panel = PanelService.getPanel("settingsPanel", screen) - if (panel && panel.enableBackgroundClick) { - panel.enableBackgroundClick() - } - } - NHeader { label: I18n.tr("settings.bar.appearance.section.label") description: I18n.tr("settings.bar.appearance.section.description") @@ -234,8 +218,6 @@ ColumnLayout { onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex) onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings) onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection) - onDragPotentialStarted: root.handleDragStart() - onDragPotentialEnded: root.handleDragEnd() } // Center Section @@ -251,8 +233,6 @@ ColumnLayout { onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex) onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings) onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection) - onDragPotentialStarted: root.handleDragStart() - onDragPotentialEnded: root.handleDragEnd() } // Right Section @@ -268,8 +248,6 @@ ColumnLayout { onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex) onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings) onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection) - onDragPotentialStarted: root.handleDragStart() - onDragPotentialEnded: root.handleDragEnd() } } } diff --git a/Modules/Panels/Settings/Tabs/ColorSchemeTab.qml b/Modules/Panels/Settings/Tabs/ColorSchemeTab.qml index d11e6e68..ca9c23f8 100644 --- a/Modules/Panels/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Panels/Settings/Tabs/ColorSchemeTab.qml @@ -677,6 +677,8 @@ ColumnLayout { enabled: ProgramCheckerService.availableDiscordClients.length > 0 opacity: ProgramCheckerService.availableDiscordClients.length > 0 ? 1.0 : 0.6 onToggled: checked => { + // Set unified discord property + Settings.data.templates.discord = checked // Enable/disable all detected Discord clients for (var i = 0; i < ProgramCheckerService.availableDiscordClients.length; i++) { var client = ProgramCheckerService.availableDiscordClients[i] @@ -740,19 +742,50 @@ ColumnLayout { } } + // Code clients - single toggle with dynamic description NCheckbox { + id: codeToggle label: "Code" - description: ProgramCheckerService.codeAvailable ? I18n.tr("settings.color-scheme.templates.programs.code.description", { - "filepath": "~/.vscode/extensions/hyprluna.hyprluna-theme-1.0.2/themes/hyprluna.json" - }) : I18n.tr("settings.color-scheme.templates.programs.code.description-missing", { - "app": "code" - }) - checked: Settings.data.templates.code - enabled: ProgramCheckerService.codeAvailable - opacity: ProgramCheckerService.codeAvailable ? 1.0 : 0.6 + description: { + if (ProgramCheckerService.availableCodeClients.length === 0) { + return I18n.tr("settings.color-scheme.templates.programs.code.description-missing") + } else { + // Show detected clients + var clientInfo = [] + for (var i = 0; i < ProgramCheckerService.availableCodeClients.length; i++) { + var client = ProgramCheckerService.availableCodeClients[i] + // Capitalize first letter and format nicely + var clientName = client.name === "code" ? "VSCode" : "VSCodium" + clientInfo.push(clientName) + } + return "Detected: " + clientInfo.join(", ") + } + } + Layout.fillWidth: true + Layout.preferredWidth: -1 + checked: { + // Check if any Code client template is enabled + var anyEnabled = false + for (var i = 0; i < ProgramCheckerService.availableCodeClients.length; i++) { + var client = ProgramCheckerService.availableCodeClients[i] + if (Settings.data.templates["code_" + client.name]) { + anyEnabled = true + break + } + } + return anyEnabled + } + enabled: ProgramCheckerService.availableCodeClients.length > 0 + opacity: ProgramCheckerService.availableCodeClients.length > 0 ? 1.0 : 0.6 onToggled: checked => { - if (ProgramCheckerService.codeAvailable) { - Settings.data.templates.code = checked + // Set unified code property + Settings.data.templates.code = checked + // Enable/disable all detected Code clients + for (var i = 0; i < ProgramCheckerService.availableCodeClients.length; i++) { + var client = ProgramCheckerService.availableCodeClients[i] + Settings.data.templates["code_" + client.name] = checked + } + if (ProgramCheckerService.availableCodeClients.length > 0) { AppThemeService.generate() } } diff --git a/Modules/Panels/Settings/Tabs/ControlCenterTab.qml b/Modules/Panels/Settings/Tabs/ControlCenterTab.qml index 989f56e7..6cdfc69b 100644 --- a/Modules/Panels/Settings/Tabs/ControlCenterTab.qml +++ b/Modules/Panels/Settings/Tabs/ControlCenterTab.qml @@ -38,22 +38,6 @@ ColumnLayout { "required": false }] - // Handler for drag start - disables panel background clicks - function handleDragStart() { - var panel = PanelService.getPanel("settingsPanel", screen) - if (panel && panel.disableBackgroundClick) { - panel.disableBackgroundClick() - } - } - - // Handler for drag end - re-enables panel background clicks - function handleDragEnd() { - var panel = PanelService.getPanel("settingsPanel", screen) - if (panel && panel.enableBackgroundClick) { - panel.enableBackgroundClick() - } - } - function saveCards() { var toSave = [] for (var i = 0; i < cardsModel.length; i++) { @@ -202,12 +186,6 @@ ColumnLayout { Layout.fillWidth: true model: cardsModel disabledIds: Settings.data.location.weatherEnabled ? [] : ["weather-card"] - onDragPotentialStarted: { - root.handleDragStart() - } - onDragPotentialEnded: { - root.handleDragEnd() - } onItemToggled: function (index, enabled) { //Logger.i("ControlCenterTab", "Item", index, "toggled to", enabled) var newModel = cardsModel.slice() @@ -266,8 +244,6 @@ ColumnLayout { onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex) onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings) onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection) - onDragPotentialStarted: root.handleDragStart() - onDragPotentialEnded: root.handleDragEnd() } // Right @@ -285,8 +261,6 @@ ColumnLayout { onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex) onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings) onMoveWidget: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection) - onDragPotentialStarted: root.handleDragStart() - onDragPotentialEnded: root.handleDragEnd() } } } diff --git a/Modules/Panels/Settings/Tabs/GeneralTab.qml b/Modules/Panels/Settings/Tabs/GeneralTab.qml index c3bdfa99..955ed3a7 100644 --- a/Modules/Panels/Settings/Tabs/GeneralTab.qml +++ b/Modules/Panels/Settings/Tabs/GeneralTab.qml @@ -233,7 +233,7 @@ ColumnLayout { } NButton { - visible: !DistroService.isNixOS + visible: !HostService.isNixOS text: I18n.tr("settings.general.launch-setup-wizard") onClicked: { var targetScreen = PanelService.openedPanel ? PanelService.openedPanel.screen : (Quickshell.screens.length > 0 ? Quickshell.screens[0] : null) diff --git a/Modules/Panels/Settings/Tabs/OsdTab.qml b/Modules/Panels/Settings/Tabs/OsdTab.qml index 10f54551..d3695411 100644 --- a/Modules/Panels/Settings/Tabs/OsdTab.qml +++ b/Modules/Panels/Settings/Tabs/OsdTab.qml @@ -11,15 +11,15 @@ ColumnLayout { // Helper functions to update arrays immutably function addMonitor(list, name) { - const arr = (list || []).slice(); + const arr = (list || []).slice() if (!arr.includes(name)) - arr.push(name); - return arr; + arr.push(name) + return arr } function removeMonitor(list, name) { return (list || []).filter(function (n) { - return n !== name; - }); + return n !== name + }) } // Display @@ -30,40 +30,31 @@ ColumnLayout { NComboBox { label: I18n.tr("settings.osd.location.label") description: I18n.tr("settings.osd.location.description") - model: [ - { + model: [{ "key": "top", "name": I18n.tr("options.osd.position.top_center") - }, - { + }, { "key": "top_left", "name": I18n.tr("options.osd.position.top_left") - }, - { + }, { "key": "top_right", "name": I18n.tr("options.osd.position.top_right") - }, - { + }, { "key": "bottom", "name": I18n.tr("options.osd.position.bottom_center") - }, - { + }, { "key": "bottom_left", "name": I18n.tr("options.osd.position.bottom_left") - }, - { + }, { "key": "bottom_right", "name": I18n.tr("options.osd.position.bottom_right") - }, - { + }, { "key": "left", "name": I18n.tr("options.osd.position.center_left") - }, - { + }, { "key": "right", "name": I18n.tr("options.osd.position.center_right") - } - ] + }] currentKey: Settings.data.osd.location || "top_right" onSelected: key => Settings.data.osd.location = key } @@ -152,22 +143,22 @@ ColumnLayout { Layout.fillWidth: true label: modelData.name || I18n.tr("system.unknown") description: { - const compositorScale = CompositorService.getDisplayScale(modelData.name); + const compositorScale = CompositorService.getDisplayScale(modelData.name) I18n.tr("system.monitor-description", { - "model": modelData.model, - "width": modelData.width * compositorScale, - "height": modelData.height * compositorScale, - "scale": compositorScale - }); + "model": modelData.model, + "width": modelData.width * compositorScale, + "height": modelData.height * compositorScale, + "scale": compositorScale + }) } checked: (Settings.data.osd.monitors || []).indexOf(modelData.name) !== -1 onToggled: checked => { - if (checked) { - Settings.data.osd.monitors = addMonitor(Settings.data.osd.monitors, modelData.name); - } else { - Settings.data.osd.monitors = removeMonitor(Settings.data.osd.monitors, modelData.name); - } - } + if (checked) { + Settings.data.osd.monitors = addMonitor(Settings.data.osd.monitors, modelData.name) + } else { + Settings.data.osd.monitors = removeMonitor(Settings.data.osd.monitors, modelData.name) + } + } } } } diff --git a/Modules/Panels/Settings/Tabs/SessionMenuTab.qml b/Modules/Panels/Settings/Tabs/SessionMenuTab.qml index 184f2cba..267aa5b4 100644 --- a/Modules/Panels/Settings/Tabs/SessionMenuTab.qml +++ b/Modules/Panels/Settings/Tabs/SessionMenuTab.qml @@ -22,6 +22,11 @@ ColumnLayout { "text": I18n.tr("session-menu.suspend"), "enabled": true, "required": false + }, { + "id": "hibernate", + "text": I18n.tr("session-menu.hibernate"), + "enabled": true, + "required": false }, { "id": "reboot", "text": I18n.tr("session-menu.reboot"), @@ -39,28 +44,6 @@ ColumnLayout { "required": false }] - // Handler for drag start - disables panel background clicks - function handleDragStart() { - var currentScreen = Quickshell.screens && Quickshell.screens.length > 0 ? Quickshell.screens[0] : null - if (currentScreen) { - var panel = PanelService.getPanel("settingsPanel", currentScreen) - if (panel && panel.disableBackgroundClick) { - panel.disableBackgroundClick() - } - } - } - - // Handler for drag end - re-enables panel background clicks - function handleDragEnd() { - var currentScreen = Quickshell.screens && Quickshell.screens.length > 0 ? Quickshell.screens[0] : null - if (currentScreen) { - var panel = PanelService.getPanel("settingsPanel", currentScreen) - if (panel && panel.enableBackgroundClick) { - panel.enableBackgroundClick() - } - } - } - function saveEntries() { var toSave = [] for (var i = 0; i < entriesModel.length; i++) { @@ -201,12 +184,6 @@ ColumnLayout { Layout.fillWidth: true model: entriesModel disabledIds: [] - onDragPotentialStarted: { - root.handleDragStart() - } - onDragPotentialEnded: { - root.handleDragEnd() - } onItemToggled: function (index, enabled) { var newModel = entriesModel.slice() newModel[index] = Object.assign({}, newModel[index], { diff --git a/Modules/Panels/Settings/Tabs/SystemMonitorTab.qml b/Modules/Panels/Settings/Tabs/SystemMonitorTab.qml new file mode 100644 index 00000000..80d58f33 --- /dev/null +++ b/Modules/Panels/Settings/Tabs/SystemMonitorTab.qml @@ -0,0 +1,22 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + + spacing: Style.marginL + + Component.onCompleted: { + + } + + NHeader { + label: I18n.tr("settings.system-monitor.general.section.label") + description: I18n.tr("settings.system-monitor.general.section.description") + } +} diff --git a/Services/Compositor/CompositorService.qml b/Services/Compositor/CompositorService.qml index 11dc118d..f410c2ea 100644 --- a/Services/Compositor/CompositorService.qml +++ b/Services/Compositor/CompositorService.qml @@ -368,6 +368,11 @@ Singleton { Quickshell.execDetached(["sh", "-c", "systemctl suspend || loginctl suspend"]) } + function hibernate() { + Logger.i("Compositor", "Hibernate requested") + Quickshell.execDetached(["sh", "-c", "systemctl hibernate || loginctl hibernate"]) + } + function lockAndSuspend() { Logger.i("Compositor", "Lock and suspend requested") try { diff --git a/Services/Media/CavaService.qml b/Services/Media/CavaService.qml index b37b582e..ec4810e2 100644 --- a/Services/Media/CavaService.qml +++ b/Services/Media/CavaService.qml @@ -16,7 +16,7 @@ Singleton { * - LockScreen is opened * - A control center is open */ - property bool shouldRun: BarService.hasAudioVisualizer || PanelService.lockScreen?.active || (PanelService.openedPanel && PanelService.openedPanel.objectName.startsWith("controlCenterPanel")) + property bool shouldRun: BarService.hasAudioVisualizer || PanelService.lockScreen?.active || (PanelService.openedPanel && PanelService.openedPanel.panelWrapper.objectName.startsWith("controlCenterPanel")) property var values: Array(barsCount).fill(0) property int barsCount: 32 diff --git a/Services/Networking/NetworkService.qml b/Services/Networking/NetworkService.qml index 17413844..543a9954 100644 --- a/Services/Networking/NetworkService.qml +++ b/Services/Networking/NetworkService.qml @@ -18,6 +18,8 @@ Singleton { property bool ethernetConnected: false property string disconnectingFrom: "" property string forgettingNetwork: "" + property string networkConnectivity: "unknown" + property bool internetConnectivity: true property bool ignoreScanResults: false property bool scanPending: false @@ -98,6 +100,16 @@ Singleton { onTriggered: ethernetStateProcess.running = true } + // Internet connectivity check timer + // Always running every 15s + Timer { + id: connectivityCheckTimer + interval: 15000 + running: true + repeat: true + onTriggered: connectivityCheckProcess.running = true + } + // Core functions function syncWifiState() { wifiStateProcess.running = true @@ -211,6 +223,8 @@ Singleton { // Helper functions function signalIcon(signal, isConnected = false) { + if (isConnected && !root.internetConnectivity) + return "world-off" if (signal >= 80) return "wifi" if (signal >= 50) @@ -285,6 +299,75 @@ Singleton { } } + // Process to check the internet connectivity of the connected network + Process { + id: connectivityCheckProcess + running: false + command: ["nmcli", "networking", "connectivity", "check"] + + property int failedChecks: 0 + + stdout: StdioCollector { + onStreamFinished: { + const result = text.trim() + if (!result) { + return + } + + if (result === "none" && root.networkConnectivity !== result) { + root.networkConnectivity = result + connectivityCheckProcess.failedChecks = 0 + root.scan() + } + + if (result === "full" && root.networkConnectivity !== result) { + root.networkConnectivity = result + root.internetConnectivity = true + connectivityCheckProcess.failedChecks = 0 + root.scan() + } + + if ((result === "limited" || result === "portal") && root.networkConnectivity !== result) { + connectivityCheckProcess.failedChecks++ + if (connectivityCheckProcess.failedChecks === 3) { + root.networkConnectivity = result + pingCheckProcess.running = true + } + } + + if (result === "unknown" && root.networkConnectivity !== result) { + root.networkConnectivity = result + connectivityCheckProcess.failedChecks = 0 + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.w("Network", "Connectivity check error: " + text) + } + } + } + } + + Process { + id: pingCheckProcess + command: ["sh", "-c", "ping -c1 -W2 ping.archlinux.org >/dev/null 2>&1 || " + "ping -c1 -W2 1.1.1.1 >/dev/null 2>&1 || " + "curl -fsI --max-time 5 https://cloudflare.com/cdn-cgi/trace >/dev/null 2>&1"] + + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + connectivityCheckProcess.failedChecks = 0 + } else { + root.internetConnectivity = false + Logger.i("Network", "No internet connectivity") + ToastService.showWarning(root.cachedLastConnected, I18n.tr("toast.internet.limited")) + connectivityCheckProcess.failedChecks = 0 + } + root.scan() + } + } + // Helper process to get existing profiles Process { id: profileCheckProcess diff --git a/Services/System/DistroService.qml b/Services/System/HostService.qml similarity index 91% rename from Services/System/DistroService.qml rename to Services/System/HostService.qml index 0865cd52..adcd766d 100644 --- a/Services/System/DistroService.qml +++ b/Services/System/HostService.qml @@ -15,7 +15,7 @@ Singleton { property bool isReady: false function init() { - Logger.i("DistroService", "Service started") + Logger.i("HostService", "Service started") } // Internal helpers @@ -81,7 +81,7 @@ Singleton { return l ? l.split("=")[1].replace(/"/g, "") : "" } root.osPretty = val("PRETTY_NAME") || val("NAME") - Logger.i("DistroService", root.osPretty) + Logger.i("HostService", root.osPretty) const osId = (val("ID") || "").toLowerCase() root.isNixOS = osId === "nixos" || (root.osPretty || "").toLowerCase().includes("nixos") @@ -91,7 +91,7 @@ Singleton { } root.isReady = true } catch (e) { - Logger.w("DistroService", "failed to read os-release", e) + Logger.w("HostService", "failed to read os-release", e) } } } @@ -102,10 +102,10 @@ Singleton { const p = String(stdout.text || "").trim() if (code === 0 && p) { root.osLogo = `file://${p}` - Logger.i("DistroService", "found", root.osLogo) + Logger.i("HostService", "found", root.osLogo) } else { root.osLogo = "" - Logger.w("DistroService", "none found") + Logger.w("HostService", "none found") } } stdout: StdioCollector {} diff --git a/Services/System/ProgramCheckerService.qml b/Services/System/ProgramCheckerService.qml index 10a2eff0..0d2cd0c8 100644 --- a/Services/System/ProgramCheckerService.qml +++ b/Services/System/ProgramCheckerService.qml @@ -31,6 +31,9 @@ Singleton { // Discord client auto-detection property var availableDiscordClients: [] + // Code client auto-detection + property var availableCodeClients: [] + // Signal emitted when all checks are complete signal checksCompleted @@ -42,12 +45,16 @@ Singleton { for (var i = 0; i < TemplateRegistry.discordClients.length; i++) { var client = TemplateRegistry.discordClients[i] var clientName = client.name + var configPath = client.configPath + + // Use the actual config path from the client, removing ~ prefix + var checkPath = configPath.startsWith("~") ? configPath.substring(2) : configPath.substring(1) // Check if this client requires themes folder to exist if (client.requiresThemesFolder) { - scriptParts.push("if [ -d \"$HOME/.config/" + clientName + "/themes\" ]; then available_clients=\"$available_clients " + clientName + "\"; fi;") + scriptParts.push("if [ -d \"$HOME/" + checkPath + "/themes\" ]; then available_clients=\"$available_clients " + clientName + "\"; fi;") } else { - scriptParts.push("if [ -d \"$HOME/.config/" + clientName + "\" ]; then available_clients=\"$available_clients " + clientName + "\"; fi;") + scriptParts.push("if [ -d \"$HOME/" + checkPath + "\" ]; then available_clients=\"$available_clients " + clientName + "\"; fi;") } } @@ -97,6 +104,66 @@ Singleton { stderr: StdioCollector {} } + // Function to detect Code client by checking config directories + function detectCodeClient() { + // Build shell script to check each client + var scriptParts = ["available_clients=\"\";"] + + for (var i = 0; i < TemplateRegistry.codeClients.length; i++) { + var client = TemplateRegistry.codeClients[i] + var clientName = client.name + var configPath = client.configPath + + // Check if the config directory exists + scriptParts.push("if [ -d \"$HOME" + configPath.substring(1) + "\" ]; then available_clients=\"$available_clients " + clientName + "\"; fi;") + } + + scriptParts.push("echo \"$available_clients\"") + + // Use a Process to check directory existence for all clients + codeDetector.command = ["sh", "-c", scriptParts.join(" ")] + codeDetector.running = true + } + + // Process to detect Code client directories + Process { + id: codeDetector + running: false + + onExited: function (exitCode) { + availableCodeClients = [] + + if (exitCode === 0) { + var detectedClients = stdout.text.trim().split(/\s+/).filter(function (client) { + return client.length > 0 + }) + + if (detectedClients.length > 0) { + // Build list of available clients + for (var i = 0; i < detectedClients.length; i++) { + var clientName = detectedClients[i] + for (var j = 0; j < TemplateRegistry.codeClients.length; j++) { + var client = TemplateRegistry.codeClients[j] + if (client.name === clientName) { + availableCodeClients.push(client) + break + } + } + } + + Logger.i("ProgramChecker", "Detected Code clients:", detectedClients.join(", ")) + } + } + + if (availableCodeClients.length === 0) { + Logger.d("ProgramChecker", "No Code clients detected") + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + // Programs to check - maps property names to commands readonly property var programsToCheck: ({ "matugenAvailable": ["which", "matugen"], @@ -140,8 +207,9 @@ Singleton { // Check next program or emit completion signal if (root.completedChecks >= root.totalChecks) { - // Run Discord client detection after all checks are complete + // Run Discord and Code client detection after all checks are complete root.detectDiscordClient() + root.detectCodeClient() root.checksCompleted() } else { root.checkNextProgram() diff --git a/Services/Theming/TemplateProcessor.qml b/Services/Theming/TemplateProcessor.qml index 63e059ed..9992bf1b 100644 --- a/Services/Theming/TemplateProcessor.qml +++ b/Services/Theming/TemplateProcessor.qml @@ -114,6 +114,18 @@ Singleton { } }) } + } else if (app.id === "code") { + // Handle Code clients specially + if (Settings.data.templates.code) { + app.clients.forEach(client => { + // Check if this specific client is detected + if (isCodeClientEnabled(client.name)) { + lines.push(`\n[templates.code_${client.name}]`) + lines.push(`input_path = "${Quickshell.shellDir}/Assets/MatugenTemplates/${app.input}"`) + lines.push(`output_path = "${client.path}"`) + } + }) + } } else { // Handle regular apps if (Settings.data.templates[app.id]) { @@ -140,6 +152,16 @@ Singleton { return false } + function isCodeClientEnabled(clientName) { + // Check ProgramCheckerService to see if client is detected + for (var i = 0; i < ProgramCheckerService.availableCodeClients.length; i++) { + if (ProgramCheckerService.availableCodeClients[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, "'\\''") @@ -163,6 +185,10 @@ Singleton { if (Settings.data.templates.discord) { script += processDiscordClients(app, colors, mode, homeDir) } + } else if (app.id === "code") { + if (Settings.data.templates.code) { + script += processCodeClients(app, colors, mode, homeDir) + } } else { if (Settings.data.templates[app.id]) { script += processTemplate(app, colors, mode, homeDir) @@ -198,6 +224,39 @@ Singleton { return script } + function processCodeClients(codeApp, colors, mode, homeDir) { + let script = "" + const palette = ColorPaletteGenerator.generatePalette(colors, Settings.data.colorSchemes.darkMode, false) + + codeApp.clients.forEach(client => { + if (!isCodeClientEnabled(client.name)) + return + + const templatePath = `${Quickshell.shellDir}/Assets/MatugenTemplates/${codeApp.input}` + const outputPath = client.path.replace("~", homeDir) + const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/')) + + // Extract base config directory for checking + var baseConfigDir = "" + if (client.name === "code") { + baseConfigDir = "~/.vscode".replace("~", homeDir) + } else if (client.name === "codium") { + baseConfigDir = "~/.vscode-oss".replace("~", homeDir) + } + + 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 "Code 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 = "" diff --git a/Services/Theming/TemplateRegistry.qml b/Services/Theming/TemplateRegistry.qml index ebb67a01..24da2974 100644 --- a/Services/Theming/TemplateRegistry.qml +++ b/Services/Theming/TemplateRegistry.qml @@ -139,17 +139,21 @@ Singleton { "requiresThemesFolder": false }, { "name": "vencord", - "path": "~/.config/discord", - "requiresThemesFolder": true + "path": "~/.config/Vencord", + "requiresThemesFolder": false }] - }, // VSCode with hardcoded path (requirement #5) + }, { "id": "code", "name": "VSCode", "category": "applications", "input": "code.json", - "outputs": [{ + "clients": [{ + "name": "code", "path": "~/.vscode/extensions/hyprluna.hyprluna-theme-1.0.2/themes/hyprluna.json" + }, { + "name": "codium", + "path": "~/.vscode-oss/extensions/hyprluna.hyprluna-theme-1.0.2/themes/hyprluna.json" }] }, { "id": "spicetify", @@ -179,6 +183,33 @@ Singleton { return clients } + // Extract Code clients for ProgramCheckerService compatibility + readonly property var codeClients: { + var clients = [] + var codeApp = applications.find(app => app.id === "code") + if (codeApp && codeApp.clients) { + codeApp.clients.forEach(client => { + // Extract base config directory from theme path + var themePath = client.path + var baseConfigDir = "" + if (client.name === "code") { + // For VSCode: ~/.vscode/extensions/... -> ~/.vscode + baseConfigDir = "~/.vscode" + } else if (client.name === "codium") { + // For VSCodium: ~/.vscode-oss/extensions/... -> ~/.vscode-oss + baseConfigDir = "~/.vscode-oss" + } + clients.push({ + "name": client.name, + "configPath": baseConfigDir, + "themePath": themePath, + "requiresThemesFolder": false + }) + }) + } + return clients + } + // Build user templates TOML content function buildUserTemplatesToml() { var lines = [] diff --git a/Services/UI/BarWidgetRegistry.qml b/Services/UI/BarWidgetRegistry.qml index c6427c9e..c4133e47 100644 --- a/Services/UI/BarWidgetRegistry.qml +++ b/Services/UI/BarWidgetRegistry.qml @@ -162,7 +162,11 @@ Singleton { "TaskbarGrouped": { "allowUserSettings": true, "showWorkspaceNumbers": true, - "showNumbersOnlyWhenOccupied": true + "showNumbersOnlyWhenOccupied": true, + "labelMode": "index", + "hideUnoccupied": false, + "characterCount": 2, + "colorizeIcons": false }, "Tray": { "allowUserSettings": true, diff --git a/Widgets/NHeader.qml b/Widgets/NHeader.qml index 08d58e01..aa40765d 100644 --- a/Widgets/NHeader.qml +++ b/Widgets/NHeader.qml @@ -15,7 +15,7 @@ ColumnLayout { NText { text: root.label pointSize: Style.fontSizeXL - font.weight: Style.fontWeightBold + font.weight: Style.fontWeightSemiBold color: Color.mSecondary visible: root.label !== "" } diff --git a/Widgets/NLabel.qml b/Widgets/NLabel.qml index f1a42440..4ae24762 100644 --- a/Widgets/NLabel.qml +++ b/Widgets/NLabel.qml @@ -16,7 +16,7 @@ ColumnLayout { NText { text: label pointSize: Style.fontSizeL - font.weight: Style.fontWeightBold + font.weight: Style.fontWeightSemiBold color: labelColor visible: label !== "" Layout.fillWidth: true diff --git a/shell.qml b/shell.qml index 35152cd7..fee3667c 100644 --- a/shell.qml +++ b/shell.qml @@ -81,7 +81,7 @@ ShellRoot { BatteryService.init() IdleInhibitorService.init() PowerProfileService.init() - DistroService.init() + HostService.init() FontService.init() // Only open the setup wizard for new users @@ -128,13 +128,13 @@ ShellRoot { function checkSetupWizard() { // Wait for distro service - if (!DistroService.isReady) { + if (!HostService.isReady) { Qt.callLater(checkSetupWizard) return } // No setup wizard on NixOS - if (DistroService.isNixOS) { + if (HostService.isNixOS) { Settings.data.setupCompleted = true return }