diff --git a/Assets/MatugenTemplates/niri.kdl b/Assets/MatugenTemplates/niri.kdl index 6646dc84d..c81b5043f 100644 --- a/Assets/MatugenTemplates/niri.kdl +++ b/Assets/MatugenTemplates/niri.kdl @@ -1,15 +1,14 @@ layout { - background-color "transparent" focus-ring { active-color "{{colors.primary.default.hex}}" - inactive-color "{{colors.outline.default.hex}}" + inactive-color "{{colors.surface.default.hex}}" urgent-color "{{colors.error.default.hex}}" } border { active-color "{{colors.primary.default.hex}}" - inactive-color "{{colors.outline.default.hex}}" + inactive-color "{{colors.surface.default.hex}}" urgent-color "{{colors.error.default.hex}}" } @@ -19,7 +18,7 @@ layout { tab-indicator { active-color "{{colors.primary.default.hex}}" - inactive-color "{{colors.outline.default.hex}}" + inactive-color "{{colors.primary_container.default.hex}}" urgent-color "{{colors.error.default.hex}}" } diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 11b257f73..d20e4329f 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -494,6 +494,20 @@ "unknown": "Unbekannt" }, "launcher": { + "categories": { + "all": "Alle", + "audiovideo": "Audio & Video", + "chat": "Chat", + "development": "Entwicklung", + "education": "Bildung", + "game": "Spiele", + "graphics": "Grafiken", + "misc": "Verschiedenes", + "network": "Netzwerk", + "office": "Büro", + "system": "System", + "webbrowser": "Webbrowser" + }, "pin": "An das Dock anheften", "unpin": "Vom Dock lösen" }, diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 47926d856..9accf50fb 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -498,6 +498,20 @@ "unknown": "Unknown" }, "launcher": { + "categories": { + "all": "All", + "audiovideo": "Audio & Video", + "chat": "Chat", + "development": "Development", + "education": "Education", + "game": "Games", + "graphics": "Graphics", + "misc": "Misc", + "network": "Network", + "office": "Office", + "system": "System", + "webbrowser": "Web Browser" + }, "pin": "Pin to dock", "unpin": "Unpin from dock" }, diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 368f7ad80..f95932952 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -494,6 +494,20 @@ "unknown": "Desconocido" }, "launcher": { + "categories": { + "all": "Todo", + "audiovideo": "Audio y video", + "chat": "Chat", + "development": "Desarrollo", + "education": "Educación", + "game": "Juegos", + "graphics": "Gráficos", + "misc": "Varios", + "network": "Red", + "office": "Oficina", + "system": "Sistema", + "webbrowser": "Navegador web" + }, "pin": "Anclar al dock", "unpin": "Desanclar del dock" }, diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 531c6f27b..6f0bb5e05 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -494,6 +494,20 @@ "unknown": "Inconnu" }, "launcher": { + "categories": { + "all": "Tout", + "audiovideo": "Audio et vidéo", + "chat": "Conversation", + "development": "Développement", + "education": "Éducation", + "game": "Jeux", + "graphics": "Graphiques", + "misc": "Divers", + "network": "Réseau", + "office": "Bureau", + "system": "Système", + "webbrowser": "Navigateur web" + }, "pin": "Épingler au dock", "unpin": "Retirer du dock" }, diff --git a/Assets/Translations/ja.json b/Assets/Translations/ja.json index ac363d975..cdba79929 100644 --- a/Assets/Translations/ja.json +++ b/Assets/Translations/ja.json @@ -494,6 +494,20 @@ "unknown": "不明" }, "launcher": { + "categories": { + "all": "すべて", + "audiovideo": "音声 & ビデオ", + "chat": "チャット", + "development": "開発", + "education": "教育", + "game": "ゲーム", + "graphics": "グラフィックス", + "misc": "その他", + "network": "ネットワーク", + "office": "オフィス", + "system": "システム", + "webbrowser": "ウェブブラウザ" + }, "pin": "ドックにピン留め", "unpin": "ドックからピン留めを解除" }, diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index 477093a7f..8ce49e623 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -494,6 +494,20 @@ "unknown": "Onbekend" }, "launcher": { + "categories": { + "all": "Alle", + "audiovideo": "Audio & Video", + "chat": "Chat", + "development": "Ontwikkeling", + "education": "Onderwijs", + "game": "Spellen", + "graphics": "Grafische weergave", + "misc": "Diversen", + "network": "Netwerk", + "office": "Kantoor", + "system": "Systeem", + "webbrowser": "Webbrowser" + }, "pin": "Aan dock vastmaken", "unpin": "Van dock losmaken" }, diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index dcec1786f..db9df4ea8 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -494,6 +494,20 @@ "unknown": "Desconhecido" }, "launcher": { + "categories": { + "all": "Tudo", + "audiovideo": "Áudio e Vídeo", + "chat": "Bate-papo", + "development": "Desenvolvimento", + "education": "Educação", + "game": "Jogos", + "graphics": "Gráficos", + "misc": "Diversos", + "network": "Rede", + "office": "Escritório", + "system": "Sistema", + "webbrowser": "Navegador web" + }, "pin": "Fixar no dock", "unpin": "Desafixar do dock" }, diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index 15451de02..1b6fa7200 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -494,6 +494,20 @@ "unknown": "Неизвестно" }, "launcher": { + "categories": { + "all": "Всё", + "audiovideo": "Аудио и видео", + "chat": "Чат", + "development": "Разработка", + "education": "Образование", + "game": "Игры", + "graphics": "Графика", + "misc": "Разное", + "network": "Сеть", + "office": "Офис", + "system": "Система", + "webbrowser": "Веб-браузер" + }, "pin": "Закрепить на панели", "unpin": "Открепить от панели" }, diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 095c446d0..b5dd2e063 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -494,6 +494,20 @@ "unknown": "Bilinmiyor" }, "launcher": { + "categories": { + "all": "Tümü", + "audiovideo": "Ses ve Video", + "chat": "Sohbet", + "development": "Gelişim", + "education": "Eğitim", + "game": "Oyunlar", + "graphics": "Grafikler", + "misc": "Çeşitli", + "network": "Ağ", + "office": "Ofis", + "system": "Sistem", + "webbrowser": "Web tarayıcı" + }, "pin": "Dock'a sabitle", "unpin": "Dock'dan sabitlemeyi kaldır" }, diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index d3029403c..4f0976932 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -494,6 +494,20 @@ "unknown": "Невідомо" }, "launcher": { + "categories": { + "all": "Все", + "audiovideo": "Аудіо та відео", + "chat": "Чат", + "development": "Розвиток", + "education": "Освіта", + "game": "Ігри", + "graphics": "Графіка", + "misc": "Різне", + "network": "Мережа", + "office": "Офіс", + "system": "Система", + "webbrowser": "Веб-браузер" + }, "pin": "Закріпити в доці", "unpin": "Відкріпити з доку" }, diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 45e32dd0f..a041f150b 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -494,6 +494,20 @@ "unknown": "未知" }, "launcher": { + "categories": { + "all": "全部", + "audiovideo": "音频和视频", + "chat": "聊天", + "development": "发展", + "education": "教育", + "game": "游戏", + "graphics": "图形", + "misc": "杂项", + "network": "网络", + "office": "办公室", + "system": "系统", + "webbrowser": "网页浏览器" + }, "pin": "固定到 Dock", "unpin": "从 Dock 取消固定" }, diff --git a/Assets/settings-default.json b/Assets/settings-default.json index a5341c6b9..6bdb85102 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -252,7 +252,6 @@ "enabled": true, "displayMode": "auto_hide", "backgroundOpacity": 1, - "radiusRatio": 0.1, "floatingRatio": 1, "size": 1, "onlySameOutput": true, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 8a19b02c4..bd55766db 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -434,7 +434,6 @@ Singleton { property bool enabled: true property string displayMode: "auto_hide" // "always_visible", "auto_hide", "exclusive" property real backgroundOpacity: 1.0 - property real radiusRatio: 0.1 property real floatingRatio: 1.0 property real size: 1 property bool onlySameOutput: true diff --git a/Modules/Bar/Extras/BarPillHorizontal.qml b/Modules/Bar/Extras/BarPillHorizontal.qml index 9adb26294..fce99dd84 100644 --- a/Modules/Bar/Extras/BarPillHorizontal.qml +++ b/Modules/Bar/Extras/BarPillHorizontal.qml @@ -85,12 +85,10 @@ Item { id: pillBackground width: collapseToIcon ? pillHeight : root.width height: pillHeight - radius: halfPillHeight + radius: Style.radiusM color: root.bgColor anchors.verticalCenter: parent.verticalCenter - readonly property int halfPillHeight: Math.round(pillHeight * 0.5) - Behavior on color { ColorAnimation { duration: Style.animationFast @@ -111,12 +109,10 @@ Item { opacity: revealed ? Style.opacityFull : Style.opacityNone color: Color.transparent // Make pill background transparent to avoid double opacity - readonly property int halfPillHeight: Math.round(pillHeight * 0.5) - - topLeftRadius: oppositeDirection ? 0 : halfPillHeight - bottomLeftRadius: oppositeDirection ? 0 : halfPillHeight - topRightRadius: oppositeDirection ? halfPillHeight : 0 - bottomRightRadius: oppositeDirection ? halfPillHeight : 0 + topLeftRadius: oppositeDirection ? 0 : Style.radiusM + bottomLeftRadius: oppositeDirection ? 0 : Style.radiusM + topRightRadius: oppositeDirection ? Style.radiusM : 0 + bottomRightRadius: oppositeDirection ? Style.radiusM : 0 anchors.verticalCenter: parent.verticalCenter NText { @@ -161,7 +157,7 @@ Item { id: iconCircle width: pillHeight height: pillHeight - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) color: Color.transparent // Make icon background transparent to avoid double opacity anchors.verticalCenter: parent.verticalCenter diff --git a/Modules/Bar/Extras/BarPillVertical.qml b/Modules/Bar/Extras/BarPillVertical.qml index 23c560085..6bb131b85 100644 --- a/Modules/Bar/Extras/BarPillVertical.qml +++ b/Modules/Bar/Extras/BarPillVertical.qml @@ -41,7 +41,6 @@ Item { // Sizing logic for vertical bars readonly property int buttonSize: Style.capsuleHeight - readonly property int halfButtonSize: Math.round(buttonSize * 0.5) readonly property int pillHeight: buttonSize readonly property int pillOverlap: Math.round(buttonSize * 0.5) readonly property int maxPillWidth: rotateText ? Math.max(buttonSize, Math.round(textItem.implicitHeight + Style.marginM * 2)) : buttonSize @@ -94,7 +93,7 @@ Item { id: pillBackground width: buttonSize height: collapseToIcon ? buttonSize : (revealed ? (buttonSize + maxPillHeight - pillOverlap) : buttonSize) - radius: halfButtonSize + radius: Style.radiusM color: root.bgColor Behavior on color { @@ -119,10 +118,10 @@ Item { color: Color.transparent // Make pill background transparent to avoid double opacity // Radius logic for vertical expansion - rounded on the side that connects to icon - topLeftRadius: openUpward ? halfButtonSize : 0 - bottomLeftRadius: openDownward ? halfButtonSize : 0 - topRightRadius: openUpward ? halfButtonSize : 0 - bottomRightRadius: openDownward ? halfButtonSize : 0 + topLeftRadius: openUpward ? Style.radiusM : 0 + bottomLeftRadius: openDownward ? Style.radiusM : 0 + topRightRadius: openUpward ? Style.radiusM : 0 + bottomRightRadius: openDownward ? Style.radiusM : 0 anchors.horizontalCenter: parent.horizontalCenter @@ -174,7 +173,7 @@ Item { id: iconCircle width: buttonSize height: buttonSize - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) color: Color.transparent // Make icon background transparent to avoid double opacity // Icon positioning based on direction diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 78ff485bb..f700c2c33 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -197,7 +197,7 @@ Item { anchors.verticalCenter: parent.verticalCenter width: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : calculatedVerticalDimension()) : ((!hasFocusedWindow) && (hideMode === "hidden") ? 0 : dynamicWidth) height: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : calculatedVerticalDimension()) : Style.capsuleHeight - radius: isVerticalBar ? width / 2 : Style.radiusM + radius: Style.radiusM color: Style.capsuleColor // Smooth width transition diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index c99fb2d71..4788a87b4 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -230,7 +230,7 @@ Item { anchors.verticalCenter: parent.verticalCenter width: isVerticalBar ? ((shouldHideIdle || isEmptyForHideMode) ? 0 : calculatedVerticalDimension()) : ((shouldHideIdle || isEmptyForHideMode) ? 0 : dynamicWidth) height: isVerticalBar ? ((shouldHideIdle || isEmptyForHideMode) ? 0 : calculatedVerticalDimension()) : Style.capsuleHeight - radius: isVerticalBar ? width / 2 : Style.radiusM + radius: Style.radiusM color: Style.capsuleColor // Smooth width transition @@ -416,7 +416,7 @@ Item { id: trackArt anchors.fill: parent anchors.margins: showProgressRing ? 0 : -1 * scaling // Add negative margin to make album art larger when no progress ring - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) visible: showAlbumArt && hasActivePlayer imagePath: MediaService.trackArtUrl fallbackIcon: MediaService.isPlaying ? "media-pause" : "media-play" @@ -658,7 +658,7 @@ Item { NImageRounded { anchors.fill: parent visible: showAlbumArt && hasActivePlayer - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) imagePath: MediaService.trackArtUrl fallbackIcon: MediaService.isPlaying ? "media-pause" : "media-play" fallbackIconSize: 12 diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 29f74b3a9..81c47f6d9 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -120,7 +120,7 @@ NIconButton { readonly property int count: computeUnreadCount() height: 8 width: height - radius: height / 2 + radius: Style.radiusXS color: Color.mError border.color: Color.mSurface border.width: Style.borderS diff --git a/Modules/Bar/Widgets/Spacer.qml b/Modules/Bar/Widgets/Spacer.qml index ce777902d..c61e80624 100644 --- a/Modules/Bar/Widgets/Spacer.qml +++ b/Modules/Bar/Widgets/Spacer.qml @@ -27,12 +27,11 @@ Item { return {}; } - // Use settings or defaults from BarWidgetRegistry - readonly property int spacerWidth: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width + readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + readonly property int spacerSize: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width - // Set the width based on user settings - implicitWidth: spacerWidth - implicitHeight: Style.barHeight + implicitWidth: isBarVertical ? Style.barHeight : spacerSize + implicitHeight: isBarVertical ? spacerSize : Style.barHeight width: implicitWidth height: implicitHeight } diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index cc90e4319..8195f41be 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -165,7 +165,7 @@ Rectangle { width: isVertical ? Math.max(0, indicatorWidth - Style.marginS * 2) : Math.max(0, indicatorWidth + Style.marginXS * 2) height: isVertical ? Math.max(0, Style.capsuleHeight + Style.marginXS * 2) : pillHeight - radius: Math.min(width, height) / 2 + radius: Style.radiusM // Hide the rectangular indicator when the bar is vertical; keep it available for horizontal layout visible: !root.isVertical color: critical ? criticalColor : warningColor diff --git a/Modules/Bar/Widgets/Taskbar.qml b/Modules/Bar/Widgets/Taskbar.qml index 60f4e0e72..13e3c01fb 100644 --- a/Modules/Bar/Widgets/Taskbar.qml +++ b/Modules/Bar/Widgets/Taskbar.qml @@ -199,7 +199,7 @@ Rectangle { width: 4 height: 4 color: modelData.isFocused ? Color.mPrimary : Color.transparent - radius: width * 0.5 + radius: Math.min(Style.radiusXXS, width / 2) } } diff --git a/Modules/Bar/Widgets/TaskbarGrouped.qml b/Modules/Bar/Widgets/TaskbarGrouped.qml index df708de7e..fe8c3217c 100644 --- a/Modules/Bar/Widgets/TaskbarGrouped.qml +++ b/Modules/Bar/Widgets/TaskbarGrouped.qml @@ -357,7 +357,7 @@ Item { width: model.isFocused ? 4 : 0 height: model.isFocused ? 4 : 0 color: model.isFocused ? Color.mPrimary : Color.transparent - radius: width * 0.5 + radius: Math.min(Style.radiusXXS, width / 2) } layer.effect: ShaderEffect { @@ -437,7 +437,7 @@ Item { id: workspaceNumberBackground anchors.fill: parent - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) color: { if (workspaceModel.isFocused) diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 8317c521c..b54fe46c2 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -378,7 +378,7 @@ Item { } } - radius: width * 0.5 + radius: Style.radiusM color: { if (model.isFocused) return Color.mPrimary; @@ -527,7 +527,7 @@ Item { } } - radius: width * 0.5 + radius: Style.radiusM color: { if (model.isFocused) return Color.mPrimary; diff --git a/Modules/Cards/CalendarMonthCard.qml b/Modules/Cards/CalendarMonthCard.qml index d32f1b28f..73066c1ea 100644 --- a/Modules/Cards/CalendarMonthCard.qml +++ b/Modules/Cards/CalendarMonthCard.qml @@ -340,7 +340,7 @@ NBox { Rectangle { width: 4 height: width - radius: width / 2 + radius: Style.radiusXXS color: parent.parent.parent.parent.parent.getEventColor(modelData, modelData.today) } } diff --git a/Modules/Cards/ProfileCard.qml b/Modules/Cards/ProfileCard.qml index 7a4833479..805c7770e 100644 --- a/Modules/Cards/ProfileCard.qml +++ b/Modules/Cards/ProfileCard.qml @@ -27,7 +27,7 @@ NBox { NImageRounded { Layout.preferredWidth: Math.round(Style.baseWidgetSize * 1.25 * Style.uiScaleRatio) Layout.preferredHeight: Math.round(Style.baseWidgetSize * 1.25 * Style.uiScaleRatio) - radius: width * 0.5 + radius: Math.min(Style.radiusL, Layout.preferredWidth / 2) imagePath: Settings.preprocessPath(Settings.data.general.avatarImage) fallbackIcon: "person" borderColor: Color.mPrimary diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index e787369d7..018812769 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -97,6 +97,48 @@ Loader { } } + // Helper function to normalize app IDs for case-insensitive matching + function normalizeAppId(appId) { + if (!appId || typeof appId !== 'string') + return ""; + return appId.toLowerCase().trim(); + } + + // Helper function to check if an app ID matches a pinned app (case-insensitive) + function isAppIdPinned(appId, pinnedApps) { + if (!appId || !pinnedApps || pinnedApps.length === 0) + return false; + const normalizedId = normalizeAppId(appId); + return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId); + } + + // Helper function to get app name from desktop entry + function getAppNameFromDesktopEntry(appId) { + if (!appId) + return appId; + + try { + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { + const entry = DesktopEntries.heuristicLookup(appId); + if (entry && entry.name) { + return entry.name; + } + } + + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId) { + const entry = DesktopEntries.byId(appId); + if (entry && entry.name) { + return entry.name; + } + } + } catch (e) + // Fall through to return original appId + {} + + // Return original appId if we can't find a desktop entry + return appId; + } + // Function to update the combined dock apps model function updateDockApps() { const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : []; @@ -108,28 +150,36 @@ Loader { // 1. First pass: Add all running apps (both pinned and non-pinned) in their current order runningApps.forEach(toplevel => { if (toplevel && toplevel.appId && !(Settings.data.dock.onlySameOutput && toplevel.screens && !toplevel.screens.includes(modelData))) { - const isPinned = pinnedApps.includes(toplevel.appId); + const isPinned = isAppIdPinned(toplevel.appId, pinnedApps); const appType = isPinned ? "pinned-running" : "running"; + // Use desktop entry name if title is "Loading..." or empty + let appTitle = toplevel.title; + if (!appTitle || appTitle === "Loading..." || appTitle.trim() === "") { + appTitle = getAppNameFromDesktopEntry(toplevel.appId); + } + combined.push({ "type": appType, "toplevel": toplevel, "appId": toplevel.appId, - "title": toplevel.title + "title": appTitle }); - processedAppIds.add(toplevel.appId); + processedAppIds.add(normalizeAppId(toplevel.appId)); } }); // 2. Second pass: Add non-running pinned apps at the end pinnedApps.forEach(pinnedAppId => { - if (!processedAppIds.has(pinnedAppId)) { - // Pinned app that is not running + const normalizedPinnedId = normalizeAppId(pinnedAppId); + if (!processedAppIds.has(normalizedPinnedId)) { + // Pinned app that is not running - get name from desktop entry + const appName = getAppNameFromDesktopEntry(pinnedAppId); combined.push({ "type": "pinned", "toplevel": null, "appId": pinnedAppId, - "title": pinnedAppId + "title": appName }); } }); @@ -299,7 +349,7 @@ Loader { height: Math.round(iconSize * 1.5) color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) anchors.centerIn: parent - radius: height * 0.5 * Settings.data.dock.radiusRatio + radius: Style.radiusL border.width: Style.borderS border.color: Qt.alpha(Color.mOutline, Settings.data.dock.backgroundOpacity) @@ -364,7 +414,21 @@ Loader { property bool isActive: modelData.toplevel && ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData.toplevel property bool hovered: appMouseArea.containsMouse property string appId: modelData ? modelData.appId : "" - property string appTitle: modelData ? (modelData.title || modelData.appId) : "" + property string appTitle: { + if (!modelData) + return ""; + // For running apps, use the toplevel title directly (reactive) + if (modelData.toplevel) { + const toplevelTitle = modelData.toplevel.title || ""; + // If title is "Loading..." or empty, use desktop entry name + if (!toplevelTitle || toplevelTitle === "Loading..." || toplevelTitle.trim() === "") { + return root.getAppNameFromDesktopEntry(modelData.appId) || modelData.appId; + } + return toplevelTitle; + } + // For pinned apps that aren't running, use the stored title + return modelData.title || modelData.appId || ""; + } property bool isRunning: modelData && (modelData.type === "running" || modelData.type === "pinned-running") // Listen for the toplevel being closed diff --git a/Modules/Dock/DockMenu.qml b/Modules/Dock/DockMenu.qml index 9002a2a87..b28b04892 100644 --- a/Modules/Dock/DockMenu.qml +++ b/Modules/Dock/DockMenu.qml @@ -24,11 +24,47 @@ PopupWindow { signal requestClose - implicitWidth: Math.max(160, contextMenuColumn.implicitWidth) + property real menuContentWidth: 160 + + implicitWidth: Math.max(160, menuContentWidth + (Style.marginM * 2)) implicitHeight: contextMenuColumn.implicitHeight + (Style.marginM * 2) color: Color.transparent visible: false + // Hidden text element for measuring text width + NText { + id: textMeasure + visible: false + pointSize: Style.fontSizeS + wrapMode: Text.NoWrap + elide: Text.ElideNone + } + + // Calculate the maximum width needed for all menu items + function calculateMenuWidth() { + let maxWidth = 0; + if (root.items && root.items.length > 0) { + for (let i = 0; i < root.items.length; i++) { + const item = root.items[i]; + if (item) { + // Calculate width: margins + icon (if present) + spacing + text width + let itemWidth = Style.marginS * 2; // left and right margins + if (item.icon && item.icon !== "") { + itemWidth += Style.fontSizeL + Style.marginS; // icon + spacing + } + // Measure actual text width + textMeasure.text = item.text || ""; + textMeasure.forceLayout(); + itemWidth += textMeasure.contentWidth; + if (itemWidth > maxWidth) { + maxWidth = itemWidth; + } + } + } + } + menuContentWidth = Math.max(160, maxWidth); + } + function initItems() { // Is this a running app? const isRunning = root.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(root.toplevel); @@ -69,7 +105,8 @@ PopupWindow { } // Create a menu entry for each app-specific action definied in its .desktop file - if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId) { + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId && root.toplevel?.appId) { + const appId = root.toplevel.appId; const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId); if (entry != null) { entry.actions.forEach(function (action) { @@ -85,6 +122,50 @@ PopupWindow { } root.items = next; + // Force width recalculation when items change + Qt.callLater(() => { + calculateMenuWidth(); + }); + } + + // Helper function to normalize app IDs for case-insensitive matching + function normalizeAppId(appId) { + if (!appId || typeof appId !== 'string') + return ""; + return appId.toLowerCase().trim(); + } + + // Helper function to get desktop entry ID from an app ID + function getDesktopEntryId(appId) { + if (!appId) + return appId; + + // Try to find the desktop entry using heuristic lookup + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { + try { + const entry = DesktopEntries.heuristicLookup(appId); + if (entry && entry.id) { + return entry.id; + } + } catch (e) + // Fall through to return original appId + {} + } + + // Try direct lookup + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId) { + try { + const entry = DesktopEntries.byId(appId); + if (entry && entry.id) { + return entry.id; + } + } catch (e) + // Fall through to return original appId + {} + } + + // Return original appId if we can't find a desktop entry + return appId; } // Helper functions for pin/unpin functionality @@ -92,21 +173,30 @@ PopupWindow { if (!appId) return false; const pinnedApps = Settings.data.dock.pinnedApps || []; - return pinnedApps.includes(appId); + const normalizedId = normalizeAppId(appId); + return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId); } function toggleAppPin(appId) { if (!appId) return; + + // Get the desktop entry ID for consistent pinning + const desktopEntryId = getDesktopEntryId(appId); + const normalizedId = normalizeAppId(desktopEntryId); + let pinnedApps = (Settings.data.dock.pinnedApps || []).slice(); // Create a copy - const isPinned = pinnedApps.includes(appId); + + // Find existing pinned app with case-insensitive matching + const existingIndex = pinnedApps.findIndex(pinnedId => normalizeAppId(pinnedId) === normalizedId); + const isPinned = existingIndex >= 0; if (isPinned) { // Unpin: remove from array - pinnedApps = pinnedApps.filter(id => id !== appId); + pinnedApps.splice(existingIndex, 1); } else { - // Pin: add to array - pinnedApps.push(appId); + // Pin: add desktop entry ID to array + pinnedApps.push(desktopEntryId); } // Update the settings @@ -125,6 +215,10 @@ PopupWindow { anchorItem = item; toplevel = toplevelData; initItems(); + // Calculate width after items are initialized + Qt.callLater(() => { + calculateMenuWidth(); + }); visible = true; canAutoClose = false; gracePeriodTimer.restart(); @@ -240,7 +334,8 @@ PopupWindow { ColumnLayout { id: contextMenuColumn - anchors.fill: parent + anchors.left: parent.left + anchors.top: parent.top anchors.margins: Style.marginM spacing: 0 @@ -253,25 +348,28 @@ PopupWindow { color: root.hoveredItem === index ? Color.mHover : Color.transparent radius: Style.radiusXS - RowLayout { + Row { + id: rowLayout anchors.left: parent.left - anchors.leftMargin: Style.marginS + anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.marginS + anchors.rightMargin: Style.marginS spacing: Style.marginS NIcon { icon: modelData.icon pointSize: Style.fontSizeL color: root.hoveredItem === index ? Color.mOnHover : Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignVCenter + visible: icon !== "" + anchors.verticalCenter: parent.verticalCenter } NText { text: modelData.text pointSize: Style.fontSizeS color: root.hoveredItem === index ? Color.mOnHover : Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignVCenter - elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter } } } diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index cc7f4ab24..e0519dbdf 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -14,6 +14,7 @@ import qs.Services.Hardware import qs.Services.Keyboard import qs.Services.Location import qs.Services.Media +import qs.Services.Networking import qs.Services.System import qs.Services.UI import qs.Widgets @@ -64,10 +65,43 @@ Loader { Item { id: batteryIndicator - property var battery: UPower.displayDevice - property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent - property real percent: isReady ? (battery.percentage * 100) : 0 - property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false + property bool initializationComplete: false + Timer { + interval: 500 + running: true + onTriggered: batteryIndicator.initializationComplete = true + } + + // Find first connected Bluetooth device with battery + function findBluetoothBatteryDevice() { + if (!BluetoothService.devices) { + return null; + } + var devices = BluetoothService.devices.values || []; + for (var i = 0; i < devices.length; i++) { + var device = devices[i]; + if (device && device.connected && device.batteryAvailable && device.battery !== undefined) { + return device; + } + } + return null; + } + + readonly property var bluetoothDevice: findBluetoothBatteryDevice() + readonly property bool hasBluetoothBattery: bluetoothDevice && bluetoothDevice.batteryAvailable && bluetoothDevice.battery !== undefined + readonly property var battery: UPower.displayDevice + readonly property bool isDevicePresent: { + if (hasBluetoothBattery) { + return bluetoothDevice.connected === true; + } + if (battery) { + return (battery.type === UPowerDeviceType.Battery && battery.isPresent !== undefined) ? battery.isPresent : (battery.ready && battery.percentage !== undefined); + } + return false; + } + property bool isReady: initializationComplete && isDevicePresent && (hasBluetoothBattery || (battery && battery.ready && battery.percentage !== undefined)) + property real percent: isReady ? (hasBluetoothBattery ? (bluetoothDevice.battery * 100) : (battery.percentage * 100)) : 0 + property bool charging: isReady ? (hasBluetoothBattery ? false : (battery ? battery.state === UPowerDeviceState.Charging : false)) : false property bool batteryVisible: isReady && percent > 0 } @@ -277,7 +311,7 @@ Loader { Layout.preferredWidth: 70 Layout.preferredHeight: 70 Layout.alignment: Qt.AlignVCenter - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) color: Color.transparent Rectangle { @@ -306,7 +340,7 @@ Loader { anchors.centerIn: parent width: 66 height: 66 - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) imagePath: Settings.preprocessPath(Settings.data.general.avatarImage) fallbackIcon: "person" @@ -428,7 +462,7 @@ Loader { // Compact status indicators container (compact mode only) Rectangle { width: { - var hasBattery = UPower.displayDevice && UPower.displayDevice.ready && UPower.displayDevice.isPresent; + var hasBattery = batteryIndicator.isReady; var hasKeyboard = keyboardLayout.currentLayout !== "Unknown"; if (hasBattery && hasKeyboard) { @@ -446,7 +480,7 @@ Loader { topLeftRadius: Style.radiusL topRightRadius: Style.radiusL color: Color.mSurface - visible: Settings.data.general.compactLockScreen && ((UPower.displayDevice && UPower.displayDevice.ready && UPower.displayDevice.isPresent) || keyboardLayout.currentLayout !== "Unknown") + visible: Settings.data.general.compactLockScreen && (batteryIndicator.isReady || keyboardLayout.currentLayout !== "Unknown") RowLayout { anchors.centerIn: parent @@ -455,16 +489,16 @@ Loader { // Battery indicator RowLayout { spacing: 6 - visible: UPower.displayDevice && UPower.displayDevice.ready && UPower.displayDevice.isPresent + visible: batteryIndicator.isReady NIcon { - icon: BatteryService.getIcon(Math.round(UPower.displayDevice.percentage * 100), UPower.displayDevice.state === UPowerDeviceState.Charging, true) + icon: BatteryService.getIcon(Math.round(batteryIndicator.percent), batteryIndicator.charging, batteryIndicator.isReady) pointSize: Style.fontSizeM - color: UPower.displayDevice.state === UPowerDeviceState.Charging ? Color.mPrimary : Color.mOnSurfaceVariant + color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurfaceVariant } NText { - text: Math.round(UPower.displayDevice.percentage * 100) + "%" + text: Math.round(batteryIndicator.percent) + "%" color: Color.mOnSurfaceVariant pointSize: Style.fontSizeM font.weight: Font.Medium @@ -576,7 +610,7 @@ Loader { // Expand to take remaining space when weather is hidden Layout.fillWidth: !(Settings.data.location.weatherEnabled && LocationService.data.weather !== null) Layout.preferredHeight: 50 - radius: 25 + radius: Style.radiusL color: Color.transparent clip: true visible: MediaService.currentPlayer && MediaService.canPlay @@ -629,14 +663,14 @@ Loader { Rectangle { Layout.preferredWidth: 34 Layout.preferredHeight: 34 - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) color: Color.transparent clip: true NImageRounded { anchors.fill: parent anchors.margins: 2 - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) imagePath: MediaService.trackArtUrl fallbackIcon: "disc" fallbackIconSize: Style.fontSizeM @@ -814,28 +848,25 @@ Loader { // Battery and Keyboard Layout (full mode only) ColumnLayout { - Layout.preferredWidth: 60 Layout.alignment: Qt.AlignRight | Qt.AlignVCenter spacing: 8 // Battery RowLayout { spacing: 4 - visible: UPower.displayDevice && UPower.displayDevice.ready && UPower.displayDevice.isPresent + visible: batteryIndicator.isReady NIcon { - icon: BatteryService.getIcon(Math.round(UPower.displayDevice.percentage * 100), UPower.displayDevice.state === UPowerDeviceState.Charging, true) + icon: BatteryService.getIcon(Math.round(batteryIndicator.percent), batteryIndicator.charging, batteryIndicator.isReady) pointSize: Style.fontSizeM - color: UPower.displayDevice.state === UPowerDeviceState.Charging ? Color.mPrimary : Color.mOnSurfaceVariant + color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurfaceVariant } NText { - text: Math.round(UPower.displayDevice.percentage * 100) + "%" + text: Math.round(batteryIndicator.percent) + "%" color: Color.mOnSurfaceVariant pointSize: Style.fontSizeM font.weight: Font.Medium - elide: Text.ElideRight - Layout.fillWidth: true } } @@ -874,7 +905,7 @@ Loader { Rectangle { Layout.fillWidth: true Layout.preferredHeight: 48 - radius: 24 + radius: Style.radiusL color: Color.mSurface border.color: passwordInput.activeFocus ? Color.mPrimary : Qt.alpha(Color.mOutline, 0.3) border.width: passwordInput.activeFocus ? 2 : 1 @@ -1009,7 +1040,7 @@ Loader { anchors.verticalCenter: parent.verticalCenter width: 36 height: 36 - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) color: eyeButtonArea.containsMouse ? Qt.alpha(Color.mOnSurface, 0.1) : "transparent" visible: passwordInput.text.length > 0 enabled: !lockContext.unlockInProgress @@ -1045,7 +1076,7 @@ Loader { anchors.verticalCenter: parent.verticalCenter width: 36 height: 36 - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) color: submitButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.8) border.color: Color.mPrimary border.width: 1 @@ -1084,261 +1115,114 @@ Loader { RowLayout { Layout.fillWidth: true Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 - spacing: 10 + spacing: 0 Item { Layout.preferredWidth: Style.marginM } - Rectangle { + NButton { Layout.fillWidth: true - Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 - radius: Settings.data.general.compactLockScreen ? 18 : 24 - color: logoutButtonArea.containsMouse ? Color.mHover : "transparent" - border.color: Color.mOutline - border.width: 1 - - RowLayout { - anchors.centerIn: parent - spacing: 6 - - NIcon { - icon: "logout" - pointSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL - color: logoutButtonArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant - } - - NText { - text: I18n.tr("session-menu.logout") - pointSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM - color: logoutButtonArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant - font.weight: Font.Medium - } - } - - MouseArea { - id: logoutButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: CompositorService.logout() - } - - Behavior on color { - ColorAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } - - Behavior on border.color { - ColorAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } + icon: "logout" + text: I18n.tr("session-menu.logout") + outlined: true + backgroundColor: Color.mOnSurfaceVariant + textColor: Color.mOnSurfaceVariant + hoverColor: Color.mHover + fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM + iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL + fontWeight: Style.fontWeightMedium + horizontalAlignment: Qt.AlignHCenter + buttonRadius: Style.radiusL + onClicked: CompositorService.logout() } - Rectangle { - Layout.fillWidth: true - Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth - Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 - radius: Settings.data.general.compactLockScreen ? 18 : 24 - color: suspendButtonArea.containsMouse ? Color.mHover : "transparent" - border.color: Color.mOutline - border.width: 1 - - RowLayout { - anchors.centerIn: parent - spacing: 6 - - NIcon { - icon: "suspend" - pointSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL - color: suspendButtonArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant - } - - NText { - text: I18n.tr("session-menu.suspend") - pointSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM - color: suspendButtonArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant - font.weight: Font.Medium - } - } - - MouseArea { - id: suspendButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: CompositorService.suspend() - } - - Behavior on color { - ColorAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } - - Behavior on border.color { - ColorAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } + Item { + Layout.preferredWidth: 10 } - Rectangle { + NButton { Layout.fillWidth: true - Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth 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 + icon: "suspend" + text: I18n.tr("session-menu.suspend") + outlined: true + backgroundColor: Color.mOnSurfaceVariant + textColor: Color.mOnSurfaceVariant + hoverColor: Color.mHover + fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM + iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL + fontWeight: Style.fontWeightMedium + horizontalAlignment: Qt.AlignHCenter + buttonRadius: Style.radiusL + onClicked: CompositorService.suspend() + } + + Item { + Layout.preferredWidth: 10 visible: Settings.data.general.showHibernateOnLockScreen - - 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 - cursorShape: Qt.PointingHandCursor - 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 { + NButton { Layout.fillWidth: true - Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 - radius: Settings.data.general.compactLockScreen ? 18 : 24 - color: rebootButtonArea.containsMouse ? Color.mHover : "transparent" - border.color: Color.mOutline - border.width: 1 - - RowLayout { - anchors.centerIn: parent - spacing: 6 - - NIcon { - icon: "reboot" - pointSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL - color: rebootButtonArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant - } - - NText { - text: I18n.tr("session-menu.reboot") - pointSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM - color: rebootButtonArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant - font.weight: Font.Medium - } - } - - MouseArea { - id: rebootButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: CompositorService.reboot() - } - - Behavior on color { - ColorAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } - - Behavior on border.color { - ColorAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } + icon: "hibernate" + text: I18n.tr("session-menu.hibernate") + outlined: true + backgroundColor: Color.mOnSurfaceVariant + textColor: Color.mOnSurfaceVariant + hoverColor: Color.mHover + fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM + iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL + fontWeight: Style.fontWeightMedium + horizontalAlignment: Qt.AlignHCenter + buttonRadius: Style.radiusL + visible: Settings.data.general.showHibernateOnLockScreen + onClicked: CompositorService.hibernate() } - Rectangle { + Item { + Layout.preferredWidth: 10 + visible: Settings.data.general.showHibernateOnLockScreen + } + + NButton { Layout.fillWidth: true - Layout.minimumWidth: buttonRowTextMeasurer.minButtonWidth Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 - radius: Settings.data.general.compactLockScreen ? 18 : 24 - color: shutdownButtonArea.containsMouse ? Color.mError : "transparent" - border.color: shutdownButtonArea.containsMouse ? Color.mError : Color.mOutline - border.width: 1 + icon: "reboot" + text: I18n.tr("session-menu.reboot") + outlined: true + backgroundColor: Color.mOnSurfaceVariant + textColor: Color.mOnSurfaceVariant + hoverColor: Color.mHover + fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM + iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL + fontWeight: Style.fontWeightMedium + horizontalAlignment: Qt.AlignHCenter + buttonRadius: Style.radiusL + onClicked: CompositorService.reboot() + } - RowLayout { - anchors.centerIn: parent - spacing: 6 + Item { + Layout.preferredWidth: 10 + } - NIcon { - icon: "shutdown" - pointSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL - color: shutdownButtonArea.containsMouse ? Color.mOnError : Color.mOnSurfaceVariant - } - - NText { - text: I18n.tr("session-menu.shutdown") - color: shutdownButtonArea.containsMouse ? Color.mOnError : Color.mOnSurfaceVariant - pointSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM - font.weight: Font.Medium - } - } - - MouseArea { - id: shutdownButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: CompositorService.shutdown() - } - - Behavior on color { - ColorAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } - - Behavior on border.color { - ColorAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } + NButton { + Layout.fillWidth: true + Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 + icon: "shutdown" + text: I18n.tr("session-menu.shutdown") + outlined: true + backgroundColor: Color.mError + textColor: Color.mOnSurfaceVariant + hoverColor: Color.mError + fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM + iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL + fontWeight: Style.fontWeightMedium + horizontalAlignment: Qt.AlignHCenter + buttonRadius: Style.radiusL + onClicked: CompositorService.shutdown() } Item { diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index ca47a6e92..aa1c7ab02 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -402,7 +402,7 @@ Variants { Layout.preferredWidth: Math.round(40 * Style.uiScaleRatio) Layout.preferredHeight: Math.round(40 * Style.uiScaleRatio) Layout.alignment: Qt.AlignVCenter - radius: width * 0.5 + radius: Math.min(Style.radiusL, Layout.preferredWidth / 2) imagePath: model.originalImage || "" borderColor: Color.transparent borderWidth: 0 diff --git a/Modules/Panels/Battery/BatteryPanel.qml b/Modules/Panels/Battery/BatteryPanel.qml index af774c1fd..976fd94eb 100644 --- a/Modules/Panels/Battery/BatteryPanel.qml +++ b/Modules/Panels/Battery/BatteryPanel.qml @@ -240,7 +240,7 @@ SmartPanel { Rectangle { Layout.fillWidth: true height: Math.round(8 * Style.uiScaleRatio) - radius: height / 2 + radius: Math.min(Style.radiusL, height / 2) color: Color.mSurfaceVariant Rectangle { diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index d55a5808f..89281817a 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -17,7 +17,7 @@ SmartPanel { readonly property bool previewActive: !!(searchText && searchText.startsWith(">clip") && Settings.data.appLauncher.enableClipPreview && ClipboardService.items && ClipboardService.items.length > 0 && selectedIndex >= 0 && results && results[selectedIndex] && results[selectedIndex].clipboardId) // Panel configuration - readonly property int listPanelWidth: Math.round(600 * Style.uiScaleRatio) + readonly property int listPanelWidth: Math.round(500 * Style.uiScaleRatio) readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio) readonly property int totalBaseWidth: listPanelWidth + (Style.marginL * 2) @@ -68,8 +68,10 @@ SmartPanel { } // Target columns, but actual columns may vary based on available width + // Account for NTabBar margins (Style.marginXS on each side) to match category tabs width readonly property int targetGridColumns: 5 - readonly property int gridCellSize: Math.floor((listPanelWidth - Style.marginS - (targetGridColumns * Style.marginXXS)) / targetGridColumns) + readonly property int gridContentWidth: listPanelWidth - (2 * Style.marginXS) + readonly property int gridCellSize: Math.floor((gridContentWidth - ((targetGridColumns - 1) * Style.marginS)) / targetGridColumns) // Actual columns that fit in the GridView // This gets updated dynamically by the GridView when its actual width is known @@ -85,6 +87,12 @@ SmartPanel { var currentIndex = emojiPlugin.categories.indexOf(emojiPlugin.selectedCategory); var nextIndex = (currentIndex + 1) % emojiPlugin.categories.length; emojiPlugin.selectCategory(emojiPlugin.categories[nextIndex]); + } else if ((activePlugin === null || activePlugin === appsPlugin) && appsPlugin.isBrowsingMode) { + // In apps browsing mode (no search), Tab navigates between categories + var availableCategories = appsPlugin.availableCategories || ["all"]; + var currentIndex = availableCategories.indexOf(appsPlugin.selectedCategory); + var nextIndex = (currentIndex + 1) % availableCategories.length; + appsPlugin.selectCategory(availableCategories[nextIndex]); } else { selectNextWrapped(); } @@ -629,7 +637,7 @@ SmartPanel { // Emoji category tabs (shown when in browsing mode) NTabBar { id: emojiCategoryTabs - visible: root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode + visible: root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode && !root.searchText.startsWith(">") Layout.fillWidth: true currentIndex: { if (visible && emojiPlugin.categories) { @@ -653,6 +661,34 @@ SmartPanel { } } + // App category tabs (shown when browsing apps without search) + NTabBar { + id: appCategoryTabs + visible: (root.activePlugin === null || root.activePlugin === appsPlugin) && appsPlugin.isBrowsingMode && !root.searchText.startsWith(">") + Layout.fillWidth: true + currentIndex: { + if (visible && appsPlugin.availableCategories) { + return appsPlugin.availableCategories.indexOf(appsPlugin.selectedCategory); + } + return 0; + } + + Repeater { + model: appsPlugin.availableCategories || [] + NIconTabButton { + required property string modelData + required property int index + icon: appsPlugin.categoryIcons[modelData] || "apps" + tooltipText: appsPlugin.getCategoryName ? appsPlugin.getCategoryName(modelData) : modelData + tabIndex: index + checked: appCategoryTabs.currentIndex === index + onClicked: { + appsPlugin.selectCategory(modelData); + } + } + } + } + Loader { id: resultsViewLoader Layout.fillWidth: true @@ -691,12 +727,20 @@ SmartPanel { property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex) property string appId: (modelData && modelData.appId) ? String(modelData.appId) : "" + // Helper function to normalize app IDs for case-insensitive matching + function normalizeAppId(appId) { + if (!appId || typeof appId !== 'string') + return ""; + return appId.toLowerCase().trim(); + } + // Pin helpers function togglePin(appId) { if (!appId) return; + const normalizedId = normalizeAppId(appId); let arr = (Settings.data.dock.pinnedApps || []).slice(); - const idx = arr.indexOf(appId); + const idx = arr.findIndex(pinnedId => normalizeAppId(pinnedId) === normalizedId); if (idx >= 0) arr.splice(idx, 1); else @@ -705,8 +749,11 @@ SmartPanel { } function isPinned(appId) { + if (!appId) + return false; const arr = Settings.data.dock.pinnedApps || []; - return appId && arr.indexOf(appId) >= 0; + const normalizedId = normalizeAppId(appId); + return arr.some(pinnedId => normalizeAppId(pinnedId) === normalizedId); } // Property to reliably track the current item's ID. @@ -918,14 +965,31 @@ SmartPanel { if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) { return parent.width / 5; } - return gridCellSize + Style.marginXXS; + // Use gridCellSize which already accounts for NTabBar margins + return root.gridCellSize + Style.marginXL; } cellHeight: { if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) { return (parent.width / 5) * 1.2; } - return gridCellSize + Style.marginXXS; + return gridCellSize + Style.marginXL; } + leftMargin: { + if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) { + return 0; + } + // Match NTabBar margins (Style.marginXS on each side) to align with category tabs + return Style.marginXS; + } + rightMargin: { + if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) { + return 0; + } + // Match NTabBar margins (Style.marginXS on each side) to align with category tabs + return Style.marginXS; + } + topMargin: 0 + bottomMargin: 0 model: results cacheBuffer: resultsGrid.height * 2 keyNavigationEnabled: false @@ -996,12 +1060,20 @@ SmartPanel { property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex) property string appId: (modelData && modelData.appId) ? String(modelData.appId) : "" + // Helper function to normalize app IDs for case-insensitive matching + function normalizeAppId(appId) { + if (!appId || typeof appId !== 'string') + return ""; + return appId.toLowerCase().trim(); + } + // Pin helpers function togglePin(appId) { if (!appId) return; + const normalizedId = normalizeAppId(appId); let arr = (Settings.data.dock.pinnedApps || []).slice(); - const idx = arr.indexOf(appId); + const idx = arr.findIndex(pinnedId => normalizeAppId(pinnedId) === normalizedId); if (idx >= 0) arr.splice(idx, 1); else @@ -1010,21 +1082,24 @@ SmartPanel { } function isPinned(appId) { + if (!appId) + return false; const arr = Settings.data.dock.pinnedApps || []; - return appId && arr.indexOf(appId) >= 0; + const normalizedId = normalizeAppId(appId); + return arr.some(pinnedId => normalizeAppId(pinnedId) === normalizedId); } width: { if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) { return resultsGrid.width / 5; } - return gridCellSize; + return resultsGrid.cellWidth; } height: { if (root.activePlugin === emojiPlugin && emojiPlugin.isBrowsingMode) { return (resultsGrid.width / 5) * 1.2; } - return gridCellSize; + return resultsGrid.cellHeight; } radius: Style.radiusM color: gridEntry.isSelected ? Color.mHover : Color.mSurface @@ -1044,7 +1119,7 @@ SmartPanel { } return Style.marginM; } - spacing: Style.marginS + spacing: Style.marginM // Icon badge or Image preview or Emoji Rectangle { diff --git a/Modules/Panels/Launcher/Plugins/ApplicationsPlugin.qml b/Modules/Panels/Launcher/Plugins/ApplicationsPlugin.qml index 7b45515d3..ac61d139e 100644 --- a/Modules/Panels/Launcher/Plugins/ApplicationsPlugin.qml +++ b/Modules/Panels/Launcher/Plugins/ApplicationsPlugin.qml @@ -5,11 +5,54 @@ import "../../../../Helpers/FuzzySort.js" as Fuzzysort import qs.Commons Item { + id: root + property var launcher: null property string name: I18n.tr("plugins.applications") property bool handleSearch: true property var entries: [] + // Category support + property string selectedCategory: "all" + property bool isBrowsingMode: false + property var categories: ["all", "AudioVideo", "Chat", "Development", "Education", "Game", "Graphics", "Network", "Office", "System", "Misc", "WebBrowser"] + property var availableCategories: ["all"] // Reactive property for available categories + + property var categoryIcons: ({ + "all": "apps", + "AudioVideo": "music", + "Chat": "message-circle", + "Development": "code", + "Education": "school" // Includes Science + , + "Game": "device-gamepad", + "Graphics": "brush", + "Network": "wifi", + "Office": "file-text", + "System": "device-desktop" // Includes Settings and Utility + , + "Misc": "dots", + "WebBrowser": "world" + }) + + function getCategoryName(category) { + const names = { + "all": I18n.tr("launcher.categories.all"), + "AudioVideo": I18n.tr("launcher.categories.audiovideo"), + "Chat": I18n.tr("launcher.categories.chat"), + "Development": I18n.tr("launcher.categories.development"), + "Education": I18n.tr("launcher.categories.education"), + "Game": I18n.tr("launcher.categories.game"), + "Graphics": I18n.tr("launcher.categories.graphics"), + "Network": I18n.tr("launcher.categories.network"), + "Office": I18n.tr("launcher.categories.office"), + "System": I18n.tr("launcher.categories.system"), + "Misc": I18n.tr("launcher.categories.misc"), + "WebBrowser": I18n.tr("launcher.categories.webbrowser") + }; + return names[category] || category; + } + // Persistent usage tracking stored in cacheDir property string usageFilePath: Settings.cacheDir + "launcher_app_usage.json" @@ -49,6 +92,199 @@ Item { function onOpened() { // Refresh apps when launcher opens loadApplications(); + // Reset to "all" category when opening + selectedCategory = "all"; + // Set browsing mode initially (will be updated when getResults is called) + isBrowsingMode = true; + } + + function selectCategory(category) { + selectedCategory = category; + if (launcher) { + launcher.updateResults(); + } + } + + function getAppCategories(app) { + if (!app) + return []; + + const result = []; + + if (app.categories) { + if (Array.isArray(app.categories)) { + for (let cat of app.categories) { + if (cat && cat.trim && cat.trim() !== '') { + result.push(cat.trim()); + } else if (cat && typeof cat === 'string' && cat.trim() !== '') { + result.push(cat.trim()); + } + } + } else if (typeof app.categories === 'string') { + const cats = app.categories.split(';').filter(c => c && c.trim() !== ''); + for (let cat of cats) { + const trimmed = cat.trim(); + if (trimmed && !result.includes(trimmed)) { + result.push(trimmed); + } + } + } else if (app.categories.length !== undefined) { + try { + for (let i = 0; i < app.categories.length; i++) { + const cat = app.categories[i]; + if (cat && cat.trim && typeof cat.trim === 'function' && cat.trim() !== '') { + result.push(cat.trim()); + } else if (cat && typeof cat === 'string' && cat.trim() !== '') { + result.push(cat.trim()); + } + } + } catch (e) {} + } + } + + if (app.Categories) { + const cats = app.Categories.split(';').filter(c => c && c.trim() !== ''); + for (let cat of cats) { + const trimmed = cat.trim(); + if (trimmed && !result.includes(trimmed)) { + result.push(trimmed); + } + } + } + + return result; + } + + function getAppCategory(app) { + const appCategories = getAppCategories(app); + if (appCategories.length === 0) + return null; + + const priorityCategories = ["AudioVideo", "Chat", "WebBrowser", "Game", "Development", "Graphics", "Office", "Education", "System", "Network", "Misc"]; + + for (let cat of appCategories) { + if (cat === "AudioVideo" || cat === "Audio" || cat === "Video") { + return "AudioVideo"; + } + } + + if (appCategories.includes("Chat") || appCategories.includes("InstantMessaging")) { + return "Chat"; + } + + if (appCategories.includes("WebBrowser")) { + return "WebBrowser"; + } + + // Map Science to Education + if (appCategories.includes("Science")) { + return "Education"; + } + + // Map Settings to System + if (appCategories.includes("Settings")) { + return "System"; + } + + // Map Utility to System + if (appCategories.includes("Utility")) { + return "System"; + } + + for (let priorityCat of priorityCategories) { + if (appCategories.includes(priorityCat) && root.categories.includes(priorityCat)) { + return priorityCat; + } + } + + return "Misc"; + } + + function appMatchesCategory(app, category) { + // Check if app matches the selected category + if (category === "all") + return true; + + // Get the primary category for this app (first matching standard category) + const primaryCategory = getAppCategory(app); + + // If app has no matching standard category, don't show it in any category (only in "all") + if (!primaryCategory) + return false; + + // Map Audio/Video to AudioVideo + if (category === "AudioVideo") { + const appCategories = getAppCategories(app); + // Show if app has AudioVideo, Audio, or Video + return appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video"); + } + + // Map Science to Education + if (category === "Education") { + const appCategories = getAppCategories(app); + return appCategories.includes("Education") || appCategories.includes("Science"); + } + + // Map Settings and Utility to System + if (category === "System") { + const appCategories = getAppCategories(app); + return appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility"); + } + + // Only show app in its primary category to avoid overlap + // This ensures each app appears in exactly one category tab + return category === primaryCategory; + } + + function getAvailableCategories() { + const categorySet = new Set(); + let hasAudioVideo = false; + let hasEducation = false; + let hasSystem = false; + + for (let app of entries) { + const appCategories = getAppCategories(app); + const primaryCategory = getAppCategory(app); + + if (appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video")) { + hasAudioVideo = true; + } else if (appCategories.includes("Education") || appCategories.includes("Science")) { + hasEducation = true; + } else if (appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility")) { + hasSystem = true; + } else if (primaryCategory && root.categories.includes(primaryCategory)) { + categorySet.add(primaryCategory); + } + } + + if (hasAudioVideo) { + categorySet.add("AudioVideo"); + } + if (hasEducation) { + categorySet.add("Education"); + } + if (hasSystem) { + categorySet.add("System"); + } + + const result = ["all"]; + for (let cat of root.categories) { + if (cat !== "all" && cat !== "Misc" && categorySet.has(cat)) { + result.push(cat); + } + } + + if (categorySet.has("Misc")) { + result.push("Misc"); + } + + if (result.length === 1) { + const fallback = root.categories.filter(c => c !== "Misc"); + fallback.push("Misc"); + return fallback; + } + + return result; } function loadApplications() { @@ -64,6 +300,12 @@ Item { return app; }); Logger.d("ApplicationsPlugin", `Loaded ${entries.length} applications`); + // Update available categories when apps are loaded + updateAvailableCategories(); + } + + function updateAvailableCategories() { + availableCategories = getAvailableCategories(); } function getExecutableName(app) { @@ -99,38 +341,47 @@ Item { if (!entries || entries.length === 0) return []; + // Set browsing mode based on whether there's a query + isBrowsingMode = !query || query.trim() === ""; + + // Filter by category first + let filteredEntries = entries; + if (selectedCategory && selectedCategory !== "all") { + filteredEntries = entries.filter(app => appMatchesCategory(app, selectedCategory)); + } + if (!query || query.trim() === "") { - // Return all apps, optionally sorted by usage + // Return filtered apps, optionally sorted by usage const favoriteApps = Settings.data.appLauncher.favoriteApps || []; let sorted; if (Settings.data.appLauncher.sortByMostUsed) { - sorted = entries.slice().sort((a, b) => { - // Favorites first - const aFav = favoriteApps.includes(getAppKey(a)); - const bFav = favoriteApps.includes(getAppKey(b)); - if (aFav !== bFav) - return aFav ? -1 : 1; - const ua = getUsageCount(a); - const ub = getUsageCount(b); - if (ub !== ua) - return ub - ua; - return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()); - }); + sorted = filteredEntries.slice().sort((a, b) => { + // Favorites first + const aFav = favoriteApps.includes(getAppKey(a)); + const bFav = favoriteApps.includes(getAppKey(b)); + if (aFav !== bFav) + return aFav ? -1 : 1; + const ua = getUsageCount(a); + const ub = getUsageCount(b); + if (ub !== ua) + return ub - ua; + return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()); + }); } else { - sorted = entries.slice().sort((a, b) => { - const aFav = favoriteApps.includes(getAppKey(a)); - const bFav = favoriteApps.includes(getAppKey(b)); - if (aFav !== bFav) - return aFav ? -1 : 1; - return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()); - }); + sorted = filteredEntries.slice().sort((a, b) => { + const aFav = favoriteApps.includes(getAppKey(a)); + const bFav = favoriteApps.includes(getAppKey(b)); + if (aFav !== bFav) + return aFav ? -1 : 1; + return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()); + }); } return sorted.map(app => createResultEntry(app)); } // Use fuzzy search if available, fallback to simple search if (typeof Fuzzysort !== 'undefined') { - const fuzzyResults = Fuzzysort.go(query, entries, { + const fuzzyResults = Fuzzysort.go(query, filteredEntries, { "keys": ["name", "comment", "genericName", "executableName"], "threshold": -1000, "limit": 20 @@ -151,37 +402,37 @@ Item { } else { // Fallback to simple search const searchTerm = query.toLowerCase(); - return entries.filter(app => { - const name = (app.name || "").toLowerCase(); - const comment = (app.comment || "").toLowerCase(); - const generic = (app.genericName || "").toLowerCase(); - const executable = getExecutableName(app).toLowerCase(); - return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm) || executable.includes(searchTerm); - }).sort((a, b) => { - // Prioritize name matches, then executable matches - const aName = a.name.toLowerCase(); - const bName = b.name.toLowerCase(); - const aExecutable = getExecutableName(a).toLowerCase(); - const bExecutable = getExecutableName(b).toLowerCase(); - const aStarts = aName.startsWith(searchTerm); - const bStarts = bName.startsWith(searchTerm); - const aExecStarts = aExecutable.startsWith(searchTerm); - const bExecStarts = bExecutable.startsWith(searchTerm); + return filteredEntries.filter(app => { + const name = (app.name || "").toLowerCase(); + const comment = (app.comment || "").toLowerCase(); + const generic = (app.genericName || "").toLowerCase(); + const executable = getExecutableName(app).toLowerCase(); + return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm) || executable.includes(searchTerm); + }).sort((a, b) => { + // Prioritize name matches, then executable matches + const aName = a.name.toLowerCase(); + const bName = b.name.toLowerCase(); + const aExecutable = getExecutableName(a).toLowerCase(); + const bExecutable = getExecutableName(b).toLowerCase(); + const aStarts = aName.startsWith(searchTerm); + const bStarts = bName.startsWith(searchTerm); + const aExecStarts = aExecutable.startsWith(searchTerm); + const bExecStarts = bExecutable.startsWith(searchTerm); - // Prioritize name matches first - if (aStarts && !bStarts) - return -1; - if (!aStarts && bStarts) - return 1; + // Prioritize name matches first + if (aStarts && !bStarts) + return -1; + if (!aStarts && bStarts) + return 1; - // Then prioritize executable matches - if (aExecStarts && !bExecStarts) - return -1; - if (!aExecStarts && bExecStarts) - return 1; + // Then prioritize executable matches + if (aExecStarts && !bExecStarts) + return -1; + if (!aExecStarts && bExecStarts) + return 1; - return aName.localeCompare(bName); - }).slice(0, 20).map(app => createResultEntry(app)); + return aName.localeCompare(bName); + }).slice(0, 20).map(app => createResultEntry(app)); } } diff --git a/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml b/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml index 31325d0ee..423353a48 100644 --- a/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml +++ b/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml @@ -321,7 +321,7 @@ SmartPanel { anchors.verticalCenter: parent.verticalCenter width: Math.round(40 * Style.uiScaleRatio) height: Math.round(40 * Style.uiScaleRatio) - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) imagePath: model.cachedImage || model.originalImage || "" borderColor: Color.transparent borderWidth: 0 diff --git a/Modules/Panels/SessionMenu/SessionMenu.qml b/Modules/Panels/SessionMenu/SessionMenu.qml index 4b5f9cbdf..5ef5f1f1d 100644 --- a/Modules/Panels/SessionMenu/SessionMenu.qml +++ b/Modules/Panels/SessionMenu/SessionMenu.qml @@ -523,7 +523,7 @@ SmartPanel { anchors.verticalCenter: parent.verticalCenter width: 20 height: 20 - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) color: Color.mPrimary visible: buttonRoot.pending diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/ControlCenterSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/ControlCenterSettings.qml index 9f6f6fcf1..b3975bf24 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/ControlCenterSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/ControlCenterSettings.qml @@ -92,7 +92,7 @@ ColumnLayout { Layout.preferredWidth: Style.fontSizeXL * 2 Layout.preferredHeight: Style.fontSizeXL * 2 Layout.alignment: Qt.AlignVCenter - radius: width * 0.5 + radius: Math.min(Style.radiusL, Layout.preferredWidth / 2) imagePath: valueCustomIconPath visible: valueCustomIconPath !== "" && !valueUseDistroLogo } diff --git a/Modules/Panels/Settings/Tabs/ColorScheme/ColorSchemeTab.qml b/Modules/Panels/Settings/Tabs/ColorScheme/ColorSchemeTab.qml index f657611f7..8767b5217 100644 --- a/Modules/Panels/Settings/Tabs/ColorScheme/ColorSchemeTab.qml +++ b/Modules/Panels/Settings/Tabs/ColorScheme/ColorSchemeTab.qml @@ -477,7 +477,7 @@ ColumnLayout { anchors.topMargin: -3 width: 20 height: 20 - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) color: Color.mSecondary border.width: Style.borderS border.color: Color.mOnSecondary @@ -897,9 +897,9 @@ ColumnLayout { NCheckbox { label: "Emacs" - description: ProgramCheckerService.emacsAvailable ? "Doom: ~/.config/doom/themes/noctalia.el\nStandard: ~/.emacs.d/themes/noctalia.el\n\nApply manually: (load-theme 'noctalia)" : I18n.tr("settings.color-scheme.templates.programs.emacs.description-missing", { - "app": "emacs" - }) + description: ProgramCheckerService.emacsAvailable ? "Doom: ~/.config/doom/themes/noctalia.el\nStandard: ~/.emacs.d/themes/noctalia.el\n\nApply manually: (load-theme 'noctalia t)" : I18n.tr("settings.color-scheme.templates.programs.emacs.description-missing", { + "app": "emacs" + }) checked: Settings.data.templates.emacs enabled: ProgramCheckerService.emacsAvailable opacity: ProgramCheckerService.emacsAvailable ? 1.0 : 0.6 diff --git a/Modules/Panels/Settings/Tabs/DockTab.qml b/Modules/Panels/Settings/Tabs/DockTab.qml index 5e88fa300..5ca75804d 100644 --- a/Modules/Panels/Settings/Tabs/DockTab.qml +++ b/Modules/Panels/Settings/Tabs/DockTab.qml @@ -81,25 +81,6 @@ ColumnLayout { } } - ColumnLayout { - visible: Settings.data.dock.enabled - spacing: Style.marginXXS - Layout.fillWidth: true - NLabel { - label: I18n.tr("settings.dock.appearance.border-radius.label") - description: I18n.tr("settings.dock.appearance.border-radius.description") - } - NValueSlider { - Layout.fillWidth: true - from: 0 - to: 1 - stepSize: 0.01 - value: Settings.data.dock.radiusRatio - onMoved: value => Settings.data.dock.radiusRatio = value - text: Math.floor(Settings.data.dock.radiusRatio * 100) + "%" - } - } - ColumnLayout { visible: Settings.data.dock.enabled spacing: Style.marginXXS diff --git a/Modules/Panels/Settings/Tabs/GeneralTab.qml b/Modules/Panels/Settings/Tabs/GeneralTab.qml index 261af6adc..5c9370db1 100644 --- a/Modules/Panels/Settings/Tabs/GeneralTab.qml +++ b/Modules/Panels/Settings/Tabs/GeneralTab.qml @@ -24,7 +24,7 @@ ColumnLayout { NImageRounded { Layout.preferredWidth: 88 * Style.uiScaleRatio Layout.preferredHeight: width - radius: width * 0.5 + radius: Math.min(Style.radiusL, Layout.preferredWidth / 2) imagePath: Settings.preprocessPath(Settings.data.general.avatarImage) fallbackIcon: "person" borderColor: Color.mPrimary diff --git a/Modules/Panels/Settings/Tabs/UserInterfaceTab.qml b/Modules/Panels/Settings/Tabs/UserInterfaceTab.qml index ee0b197d0..a42f6c325 100644 --- a/Modules/Panels/Settings/Tabs/UserInterfaceTab.qml +++ b/Modules/Panels/Settings/Tabs/UserInterfaceTab.qml @@ -271,7 +271,7 @@ ColumnLayout { NValueSlider { Layout.fillWidth: true - from: 0.1 + from: 0 to: 2.0 stepSize: 0.01 value: Settings.data.general.animationSpeed diff --git a/Modules/Panels/SetupWizard/SetupCustomizeStep.qml b/Modules/Panels/SetupWizard/SetupCustomizeStep.qml index f3447eb83..2b2e55be8 100644 --- a/Modules/Panels/SetupWizard/SetupCustomizeStep.qml +++ b/Modules/Panels/SetupWizard/SetupCustomizeStep.qml @@ -257,7 +257,7 @@ ColumnLayout { } ] delegate: Rectangle { - radius: 16 + radius: Style.radiusM border.width: Style.borderS Layout.preferredHeight: 32 Layout.preferredWidth: Math.max(90, densityText.implicitWidth + Style.marginXL * 2) diff --git a/Services/Compositor/NiriService.qml b/Services/Compositor/NiriService.qml index bf9f9b1d2..59d1a0458 100644 --- a/Services/Compositor/NiriService.qml +++ b/Services/Compositor/NiriService.qml @@ -7,10 +7,8 @@ import qs.Services.Keyboard Item { id: root - // Sorts floating windows after scrolling ones property int floatingWindowPosition: Number.MAX_SAFE_INTEGER - // Properties that match the facade interface property ListModel workspaces: ListModel {} property var windows: [] property int focusedWindowIndex: -1 @@ -19,13 +17,17 @@ Item { property var keyboardLayouts: [] - // Signals that match the facade interface + property var maximizedWindows: [] + property bool originalBarFloatingState: false + property bool barFloatingStateSaved: false + property bool originalBarOuterCornersState: false + property bool barOuterCornersStateSaved: false + signal workspaceChanged signal activeWindowChanged signal windowListChanged signal displayScalesChanged - // Initialization function initialize() { niriEventStream.connected = true; niriCommandSocket.connected = true; @@ -47,17 +49,14 @@ Item { sendSocketCommand(niriEventStream, "EventStream"); } - // Update workspaces function updateWorkspaces() { sendSocketCommand(niriCommandSocket, "Workspaces"); } - // Update windows function updateWindows() { sendSocketCommand(niriCommandSocket, "Windows"); } - // Query display scales function queryDisplayScales() { sendSocketCommand(niriCommandSocket, "Outputs"); } @@ -65,7 +64,6 @@ Item { function recollectOutputs(outputsData) { const scales = {}; - // Niri returns an object with display names as keys for (const outputName in outputsData) { const output = outputsData[outputName]; if (output && output.name) { @@ -91,7 +89,6 @@ Item { } } - // Notify CompositorService (it will emit displayScalesChanged) if (CompositorService && CompositorService.onDisplayScalesUpdated) { CompositorService.onDisplayScalesUpdated(scales); } @@ -113,7 +110,6 @@ Item { }); } - // Sort workspaces by output, then by index workspacesList.sort((a, b) => { if (a.output !== b.output) { return a.output.localeCompare(b.output); @@ -121,7 +117,6 @@ Item { return a.idx - b.idx; }); - // Update the workspaces ListModel workspaces.clear(); for (var i = 0; i < workspacesList.length; i++) { workspaces.append(workspacesList[i]); @@ -130,7 +125,6 @@ Item { workspaceChanged(); } - // Niri command socket Socket { id: niriCommandSocket path: Quickshell.env("NIRI_SOCKET") @@ -161,7 +155,6 @@ Item { } } - // Niri event stream socket Socket { id: niriEventStream path: Quickshell.env("NIRI_SOCKET") @@ -204,7 +197,6 @@ Item { } } - // Utility functions function getWindowPosition(layout) { if (layout.pos_in_scrolling_layout) { return { @@ -240,10 +232,6 @@ Item { }; } - // Sort windows - // 1. by workspace ID - // 2. by position X - // 3. by position Y function compareWindows(a, b) { if (a.workspaceId !== b.workspaceId) { return a.workspaceId - b.workspaceId; @@ -273,7 +261,6 @@ Item { activeWindowChanged(); } - // Event handlers function handleWindowOpenedOrChanged(eventData) { try { const windowData = eventData.window; @@ -281,20 +268,16 @@ Item { const newWindow = getWindowData(windowData); if (existingIndex >= 0) { - // Update existing window windows[existingIndex] = newWindow; } else { - // Add new window windows.push(newWindow); } windows.sort(compareWindows); - // Update focused window index if this window is focused if (newWindow.isFocused) { const oldFocusedIndex = focusedWindowIndex; focusedWindowIndex = windows.findIndex(w => w.id === windowData.id); - // Only emit activeWindowChanged if the focused window actually changed if (oldFocusedIndex !== focusedWindowIndex) { if (oldFocusedIndex >= 0 && oldFocusedIndex < windows.length) { windows[oldFocusedIndex].isFocused = false; @@ -315,16 +298,23 @@ Item { const windowIndex = windows.findIndex(w => w.id === windowId); if (windowIndex >= 0) { - // If this was the focused window, clear focus + const maximizedIndex = maximizedWindows.indexOf(windowId); + if (maximizedIndex >= 0) { + maximizedWindows.splice(maximizedIndex, 1); + + if (maximizedWindows.length === 0 && barFloatingStateSaved) { + Settings.data.bar.floating = originalBarFloatingState; + barFloatingStateSaved = false; + } + } + if (windowIndex === focusedWindowIndex) { focusedWindowIndex = -1; activeWindowChanged(); } else if (focusedWindowIndex > windowIndex) { - // Adjust focused window index if needed focusedWindowIndex--; } - // Remove the window windows.splice(windowIndex, 1); windowListChanged(); } @@ -376,6 +366,66 @@ Item { const window = windows.find(w => w.id === windowId); if (window) { window.position = getWindowPosition(layout); + + if (layout.window_size && window.output && CompositorService) { + const outputInfo = CompositorService.getDisplayInfo(window.output); + if (outputInfo && outputInfo.width && outputInfo.height) { + const windowWidth = layout.window_size[0]; + const windowHeight = layout.window_size[1]; + const outputWidth = outputInfo.width; + const outputHeight = outputInfo.height; + + const barPosition = Settings.data.bar.position || "top"; + const isVerticalBar = barPosition === "left" || barPosition === "right"; + const barSize = Style.barHeight; + + let widthMatch, heightMatch; + if (isVerticalBar) { + widthMatch = Math.abs(windowWidth - (outputWidth - barSize)) < 10; + heightMatch = Math.abs(windowHeight - outputHeight) < 10; + } else { + widthMatch = Math.abs(windowWidth - outputWidth) < 10; + heightMatch = Math.abs(windowHeight - (outputHeight - barSize)) < 50; + } + + const isMaximized = widthMatch && heightMatch; + const wasMaximized = maximizedWindows.indexOf(windowId) >= 0; + + if (isMaximized && !wasMaximized) { + Logger.i("NiriService", "Detected maximize-window-to-edges"); + maximizedWindows.push(windowId); + + if (!barFloatingStateSaved) { + originalBarFloatingState = Settings.data.bar.floating; + barFloatingStateSaved = true; + } + + if (!barOuterCornersStateSaved) { + originalBarOuterCornersState = Settings.data.bar.outerCorners; + barOuterCornersStateSaved = true; + } + + Settings.data.bar.floating = false; + Settings.data.bar.outerCorners = false; + } else if (!isMaximized && wasMaximized) { + const index = maximizedWindows.indexOf(windowId); + if (index >= 0) { + maximizedWindows.splice(index, 1); + } + + if (maximizedWindows.length === 0) { + if (barFloatingStateSaved) { + Settings.data.bar.floating = originalBarFloatingState; + barFloatingStateSaved = false; + } + if (barOuterCornersStateSaved) { + Settings.data.bar.outerCorners = originalBarOuterCornersState; + barOuterCornersStateSaved = false; + } + } + } + } + } } } @@ -417,7 +467,6 @@ Item { } } - // Public functions function switchToWorkspace(workspace) { try { Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspace.idx.toString()]); diff --git a/Services/Noctalia/UpdateService.qml b/Services/Noctalia/UpdateService.qml index e237ca36d..16c026eed 100644 --- a/Services/Noctalia/UpdateService.qml +++ b/Services/Noctalia/UpdateService.qml @@ -11,7 +11,7 @@ Singleton { id: root // Version properties - readonly property string baseVersion: "3.4.0" + readonly property string baseVersion: "3.5.0" readonly property bool isDevelopment: true readonly property string developmentSuffix: "-git" readonly property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + developmentSuffix}` diff --git a/Widgets/NButton.qml b/Widgets/NButton.qml index e98502421..7de9c1ef6 100644 --- a/Widgets/NButton.qml +++ b/Widgets/NButton.qml @@ -20,6 +20,7 @@ Rectangle { property real iconSize: Style.fontSizeL property bool outlined: false property int horizontalAlignment: Qt.AlignHCenter + property real buttonRadius: Style.radiusS // Signals signal clicked @@ -36,7 +37,7 @@ Rectangle { implicitHeight: Math.max(Style.baseWidgetSize, contentRow.implicitHeight + (Style.marginM)) // Appearance - radius: Style.radiusS + radius: root.buttonRadius color: { if (!enabled) return outlined ? Color.transparent : Qt.lighter(Color.mSurfaceVariant, 1.2); diff --git a/Widgets/NColorPicker.qml b/Widgets/NColorPicker.qml index cc8650dba..06399e083 100644 --- a/Widgets/NColorPicker.qml +++ b/Widgets/NColorPicker.qml @@ -49,7 +49,7 @@ Rectangle { Rectangle { Layout.preferredWidth: root.height * 0.6 Layout.preferredHeight: root.height * 0.6 - radius: Layout.preferredWidth * 0.5 + radius: Math.min(Style.radiusL, Layout.preferredWidth / 2) color: root.selectedColor border.color: Color.mOutline border.width: Style.borderS diff --git a/Widgets/NColorPickerDialog.qml b/Widgets/NColorPickerDialog.qml index 9ba5ae931..28b55031d 100644 --- a/Widgets/NColorPickerDialog.qml +++ b/Widgets/NColorPickerDialog.qml @@ -564,7 +564,7 @@ Popup { Rectangle { width: 10 height: 10 - radius: 5 + radius: Math.min(Style.radiusXS, width / 2) color: "transparent" border.color: root.selectedColor.hsvValue < 0.5 ? "white" : "black" border.width: 1 @@ -698,7 +698,7 @@ Popup { Rectangle { width: 24 height: 24 - radius: 4 + radius: Math.min(Style.radiusXS, width / 2) color: modelData.color border.color: root.selectedColor.toString() === modelData.color.toString() ? Color.mPrimary : Color.mOutline border.width: root.selectedColor.toString() === modelData.color.toString() ? 2 : 1 diff --git a/Widgets/NColorSlider.qml b/Widgets/NColorSlider.qml index 1e7264eb6..95a18f10b 100644 --- a/Widgets/NColorSlider.qml +++ b/Widgets/NColorSlider.qml @@ -39,7 +39,7 @@ Slider { height: root.availableHeight - root.knobDiameter width: root.trackWidth - radius: width / 2 + radius: Math.min(Style.radiusL, width / 2) color: Qt.alpha(Color.mSurface, 0.5) border.color: Qt.alpha(Color.mOutline, 0.5) border.width: Style.borderS @@ -96,7 +96,7 @@ Slider { id: knobCutout implicitWidth: root.knobDiameter + root.cutoutExtra implicitHeight: root.knobDiameter + root.cutoutExtra - radius: width / 2 + radius: Math.min(Style.radiusL, width / 2) color: root.cutoutColor !== undefined ? root.cutoutColor : Color.mSurface y: root.visualPosition * (root.availableHeight - root.knobDiameter) - ((root.knobDiameter + root.cutoutExtra) / 2) @@ -114,7 +114,7 @@ Slider { id: knob implicitWidth: root.knobDiameter implicitHeight: root.knobDiameter - radius: width / 2 + radius: Math.min(Style.radiusL, width / 2) color: { if (root.rainbowMode) { // Hue Logic: Map position (0.0 to 1.0) directly to Hue diff --git a/Widgets/NFilePicker.qml b/Widgets/NFilePicker.qml index 05b907035..394b4c820 100644 --- a/Widgets/NFilePicker.qml +++ b/Widgets/NFilePicker.qml @@ -578,7 +578,7 @@ Popup { anchors.margins: Style.marginS width: 24 height: 24 - radius: width / 2 + radius: Math.min(Style.radiusL, width / 2) color: Color.mSecondary border.color: Color.mOutline border.width: Style.borderS diff --git a/Widgets/NGridView.qml b/Widgets/NGridView.qml index 8f588c3ea..6799a5a28 100644 --- a/Widgets/NGridView.qml +++ b/Widgets/NGridView.qml @@ -36,6 +36,10 @@ Item { property alias delegate: gridView.delegate property alias cellWidth: gridView.cellWidth property alias cellHeight: gridView.cellHeight + property alias leftMargin: gridView.leftMargin + property alias rightMargin: gridView.rightMargin + property alias topMargin: gridView.topMargin + property alias bottomMargin: gridView.bottomMargin property alias currentIndex: gridView.currentIndex property alias count: gridView.count property alias contentHeight: gridView.contentHeight diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 4e8404f39..df6cdcb4f 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -37,7 +37,7 @@ Rectangle { opacity: root.enabled ? Style.opacityFull : Style.opacityMedium color: root.enabled && root.hovering ? colorBgHover : colorBg - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) border.color: root.enabled && root.hovering ? colorBorderHover : colorBorder border.width: Style.borderS diff --git a/Widgets/NIconButtonHot.qml b/Widgets/NIconButtonHot.qml index eb50c1159..6ab5b826f 100644 --- a/Widgets/NIconButtonHot.qml +++ b/Widgets/NIconButtonHot.qml @@ -60,7 +60,7 @@ Rectangle { } return colorBg; } - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) border.color: root.enabled && root.hovering ? colorBorderHover : colorBorder border.width: Style.borderS diff --git a/Widgets/NIconTabButton.qml b/Widgets/NIconTabButton.qml index df7e9c07a..0490c86c9 100644 --- a/Widgets/NIconTabButton.qml +++ b/Widgets/NIconTabButton.qml @@ -1,7 +1,9 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell import qs.Commons +import qs.Services.UI import qs.Widgets Rectangle { @@ -9,6 +11,7 @@ Rectangle { // Public properties property string icon: "" + property string tooltipText: "" property bool checked: false property int tabIndex: 0 @@ -51,9 +54,22 @@ Rectangle { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onEntered: root.isHovered = true - onExited: root.isHovered = false + onEntered: { + root.isHovered = true; + if (root.tooltipText) { + TooltipService.show(parent, root.tooltipText); + } + } + onExited: { + root.isHovered = false; + if (root.tooltipText) { + TooltipService.hide(); + } + } onClicked: { + if (root.tooltipText) { + TooltipService.hide(); + } root.clicked(); // Update parent NTabBar's currentIndex if (root.parent && root.parent.parent && root.parent.parent.currentIndex !== undefined) { diff --git a/Widgets/NRadioButton.qml b/Widgets/NRadioButton.qml index 09c53e327..bcaf140d8 100644 --- a/Widgets/NRadioButton.qml +++ b/Widgets/NRadioButton.qml @@ -15,7 +15,7 @@ RadioButton { implicitWidth: Style.baseWidgetSize * 0.625 * pointSize / Style.fontSizeM implicitHeight: Style.baseWidgetSize * 0.625 * pointSize / Style.fontSizeM - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) color: Color.transparent border.color: root.checked ? Color.mPrimary : Color.mOnSurface border.width: Style.borderM @@ -25,7 +25,7 @@ RadioButton { anchors.fill: parent anchors.margins: parent.width * 0.3 - radius: width * 0.5 + radius: Math.min(Style.radiusL, width / 2) color: Qt.alpha(Color.mPrimary, root.checked ? 1 : 0) Behavior on color { diff --git a/Widgets/NSectionEditor.qml b/Widgets/NSectionEditor.qml index 19f20c0bd..fda2a1fdc 100644 --- a/Widgets/NSectionEditor.qml +++ b/Widgets/NSectionEditor.qml @@ -355,7 +355,7 @@ NBox { id: dropIndicator width: 3 height: Style.baseWidgetSize * 1.15 - radius: width / 2 + radius: Style.radiusXXS color: Color.mPrimary opacity: 0 visible: opacity > 0 diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index dc028c757..6ffd5665a 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -31,7 +31,7 @@ Slider { implicitHeight: trackHeight width: root.availableWidth height: implicitHeight - radius: height / 2 + radius: Math.min(Style.radiusL, height / 2) color: Qt.alpha(Color.mSurface, 0.5) border.color: Qt.alpha(Color.mOutline, 0.5) border.width: Style.borderS @@ -46,7 +46,7 @@ Slider { Rectangle { width: parent.height height: parent.height - radius: width / 2 + radius: Math.min(Style.radiusL, width / 2) color: Qt.darker(fillColor, 1.2) //starting color of gradient } @@ -76,7 +76,7 @@ Slider { id: knobCutout implicitWidth: knobDiameter + cutoutExtra implicitHeight: knobDiameter + cutoutExtra - radius: width / 2 + radius: Math.min(Style.radiusL, width / 2) color: root.cutoutColor !== undefined ? root.cutoutColor : Color.mSurface x: root.leftPadding + root.visualPosition * (root.availableWidth - root.knobDiameter) - cutoutExtra anchors.verticalCenter: parent.verticalCenter @@ -93,7 +93,7 @@ Slider { id: knob implicitWidth: knobDiameter implicitHeight: knobDiameter - radius: width / 2 + radius: Math.min(Style.radiusL, width / 2) color: root.pressed ? Color.mHover : Color.mSurface border.color: fillColor border.width: Style.borderL diff --git a/Widgets/NSpinBox.qml b/Widgets/NSpinBox.qml index 44fc550b4..96c937f88 100644 --- a/Widgets/NSpinBox.qml +++ b/Widgets/NSpinBox.qml @@ -85,7 +85,7 @@ RowLayout { id: spinBoxContainer implicitWidth: 120 implicitHeight: Math.round((root.baseSize - 4) / 2) * 2 - radius: height * 0.5 + radius: Style.radiusS color: Color.mSurfaceVariant border.color: (root.hovering || decreaseArea.containsMouse || increaseArea.containsMouse) ? Color.mHover : Color.mOutline border.width: Style.borderS @@ -140,7 +140,7 @@ RowLayout { Rectangle { width: Math.round(parent.height) height: parent.height - radius: Math.round(width / 2) + radius: Math.min(Style.radiusL, width / 2) anchors.left: parent.left color: decreaseArea.containsMouse ? Color.mHover : Color.transparent Behavior on color { @@ -239,7 +239,7 @@ RowLayout { Rectangle { width: Math.round(parent.height) height: parent.height - radius: Math.round(width / 2) + radius: Math.min(Style.radiusL, width / 2) anchors.right: parent.right color: increaseArea.containsMouse ? Color.mHover : Color.transparent Behavior on color { diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index 95e369d12..bf5130387 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -35,7 +35,7 @@ RowLayout { implicitWidth: Math.round(root.baseSize * .85) * 2 implicitHeight: Math.round(root.baseSize * .5) * 2 - radius: height * 0.5 + radius: Math.min(Style.radiusL, height / 2) color: root.checked ? Color.mPrimary : Color.mSurface border.color: Color.mOutline border.width: Style.borderS @@ -56,7 +56,7 @@ RowLayout { implicitWidth: Math.round(root.baseSize * 0.4) * 2 implicitHeight: Math.round(root.baseSize * 0.4) * 2 - radius: height * 0.5 + radius: Math.min(Style.radiusL, height / 2) color: root.checked ? Color.mOnPrimary : Color.mPrimary border.color: root.checked ? Color.mSurface : Color.mSurface border.width: Style.borderM