Merge branch 'main' into plugin-system

This commit is contained in:
ItsLemmy
2025-12-01 17:22:49 -05:00
57 changed files with 1030 additions and 462 deletions
+3 -4
View File
@@ -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}}"
}
+14
View File
@@ -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"
},
+14
View File
@@ -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"
},
+14
View File
@@ -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"
},
+14
View File
@@ -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"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "不明"
},
"launcher": {
"categories": {
"all": "すべて",
"audiovideo": "音声 & ビデオ",
"chat": "チャット",
"development": "開発",
"education": "教育",
"game": "ゲーム",
"graphics": "グラフィックス",
"misc": "その他",
"network": "ネットワーク",
"office": "オフィス",
"system": "システム",
"webbrowser": "ウェブブラウザ"
},
"pin": "ドックにピン留め",
"unpin": "ドックからピン留めを解除"
},
+14
View File
@@ -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"
},
+14
View File
@@ -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"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "Неизвестно"
},
"launcher": {
"categories": {
"all": "Всё",
"audiovideo": "Аудио и видео",
"chat": "Чат",
"development": "Разработка",
"education": "Образование",
"game": "Игры",
"graphics": "Графика",
"misc": "Разное",
"network": "Сеть",
"office": "Офис",
"system": "Система",
"webbrowser": "Веб-браузер"
},
"pin": "Закрепить на панели",
"unpin": "Открепить от панели"
},
+14
View File
@@ -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"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "Невідомо"
},
"launcher": {
"categories": {
"all": "Все",
"audiovideo": "Аудіо та відео",
"chat": "Чат",
"development": "Розвиток",
"education": "Освіта",
"game": "Ігри",
"graphics": "Графіка",
"misc": "Різне",
"network": "Мережа",
"office": "Офіс",
"system": "Система",
"webbrowser": "Веб-браузер"
},
"pin": "Закріпити в доці",
"unpin": "Відкріпити з доку"
},
+14
View File
@@ -494,6 +494,20 @@
"unknown": "未知"
},
"launcher": {
"categories": {
"all": "全部",
"audiovideo": "音频和视频",
"chat": "聊天",
"development": "发展",
"education": "教育",
"game": "游戏",
"graphics": "图形",
"misc": "杂项",
"network": "网络",
"office": "办公室",
"system": "系统",
"webbrowser": "网页浏览器"
},
"pin": "固定到 Dock",
"unpin": "从 Dock 取消固定"
},
-1
View File
@@ -252,7 +252,6 @@
"enabled": true,
"displayMode": "auto_hide",
"backgroundOpacity": 1,
"radiusRatio": 0.1,
"floatingRatio": 1,
"size": 1,
"onlySameOutput": true,
-1
View File
@@ -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
+6 -10
View File
@@ -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
+6 -7
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+4 -5
View File
@@ -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
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
}
}
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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)
}
}
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 {
+87 -12
View File
@@ -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
+1 -1
View File
@@ -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
-19
View File
@@ -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
+1 -1
View File
@@ -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)
+76 -27
View File
@@ -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()]);
+1 -1
View File
@@ -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
View File
@@ -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);
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+4
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+18 -2
View File
@@ -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) {
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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