mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge branch 'main' into plugin-system
This commit is contained in:
@@ -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}}"
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "不明"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "すべて",
|
||||
"audiovideo": "音声 & ビデオ",
|
||||
"chat": "チャット",
|
||||
"development": "開発",
|
||||
"education": "教育",
|
||||
"game": "ゲーム",
|
||||
"graphics": "グラフィックス",
|
||||
"misc": "その他",
|
||||
"network": "ネットワーク",
|
||||
"office": "オフィス",
|
||||
"system": "システム",
|
||||
"webbrowser": "ウェブブラウザ"
|
||||
},
|
||||
"pin": "ドックにピン留め",
|
||||
"unpin": "ドックからピン留めを解除"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "Неизвестно"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "Всё",
|
||||
"audiovideo": "Аудио и видео",
|
||||
"chat": "Чат",
|
||||
"development": "Разработка",
|
||||
"education": "Образование",
|
||||
"game": "Игры",
|
||||
"graphics": "Графика",
|
||||
"misc": "Разное",
|
||||
"network": "Сеть",
|
||||
"office": "Офис",
|
||||
"system": "Система",
|
||||
"webbrowser": "Веб-браузер"
|
||||
},
|
||||
"pin": "Закрепить на панели",
|
||||
"unpin": "Открепить от панели"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "Невідомо"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "Все",
|
||||
"audiovideo": "Аудіо та відео",
|
||||
"chat": "Чат",
|
||||
"development": "Розвиток",
|
||||
"education": "Освіта",
|
||||
"game": "Ігри",
|
||||
"graphics": "Графіка",
|
||||
"misc": "Різне",
|
||||
"network": "Мережа",
|
||||
"office": "Офіс",
|
||||
"system": "Система",
|
||||
"webbrowser": "Веб-браузер"
|
||||
},
|
||||
"pin": "Закріпити в доці",
|
||||
"unpin": "Відкріпити з доку"
|
||||
},
|
||||
|
||||
@@ -494,6 +494,20 @@
|
||||
"unknown": "未知"
|
||||
},
|
||||
"launcher": {
|
||||
"categories": {
|
||||
"all": "全部",
|
||||
"audiovideo": "音频和视频",
|
||||
"chat": "聊天",
|
||||
"development": "发展",
|
||||
"education": "教育",
|
||||
"game": "游戏",
|
||||
"graphics": "图形",
|
||||
"misc": "杂项",
|
||||
"network": "网络",
|
||||
"office": "办公室",
|
||||
"system": "系统",
|
||||
"webbrowser": "网页浏览器"
|
||||
},
|
||||
"pin": "固定到 Dock",
|
||||
"unpin": "从 Dock 取消固定"
|
||||
},
|
||||
|
||||
@@ -252,7 +252,6 @@
|
||||
"enabled": true,
|
||||
"displayMode": "auto_hide",
|
||||
"backgroundOpacity": 1,
|
||||
"radiusRatio": 0.1,
|
||||
"floatingRatio": 1,
|
||||
"size": 1,
|
||||
"onlySameOutput": true,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+72
-8
@@ -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
|
||||
|
||||
+111
-13
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+140
-256
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()]);
|
||||
|
||||
@@ -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}`
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user