diff --git a/Modules/Dock/DockAppButton.qml b/Modules/Dock/DockAppButton.qml index 16a0d5ab..4a0c32a3 100644 --- a/Modules/Dock/DockAppButton.qml +++ b/Modules/Dock/DockAppButton.qml @@ -7,307 +7,297 @@ import qs.Services import qs.Widgets Item { - id: root + id: root - property var appData - property var contextMenu: null - property var windowsMenu: null - property var dockApps: null - property int index: -1 - property bool longPressing: false - property bool dragging: false - property point dragStartPos: Qt.point(0, 0) - property point dragOffset: Qt.point(0, 0) - property int targetIndex: -1 - property int originalIndex: -1 + property var appData + property var contextMenu: null + property var windowsMenu: null + property var dockApps: null + property int index: -1 + property bool longPressing: false + property bool dragging: false + property point dragStartPos: Qt.point(0, 0) + property point dragOffset: Qt.point(0, 0) + property int targetIndex: -1 + property int originalIndex: -1 + property bool showWindowTitle: false + property string windowTitle: "" + property bool isHovered: mouseArea.containsMouse && !dragging + property bool showTooltip: mouseArea.containsMouse && !dragging + property string tooltipText: { + if (!appData) + return ""; - width: 40 - height: 40 - - property bool isHovered: mouseArea.containsMouse && !dragging - - transform: Translate { - id: translateY - y: 0 - } - - SequentialAnimation { - id: bounceAnimation - running: false - - NumberAnimation { - target: translateY - property: "y" - to: -10 - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.emphasizedAccel - } - - NumberAnimation { - target: translateY - property: "y" - to: -8 - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.emphasizedDecel - } - } - - NumberAnimation { - id: exitAnimation - running: false - target: translateY - property: "y" - to: 0 - duration: Anims.durShort - easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.emphasizedDecel - } - - onIsHoveredChanged: { - if (isHovered) { - exitAnimation.stop() - if (!bounceAnimation.running) { - bounceAnimation.restart() - } - } else { - bounceAnimation.stop() - exitAnimation.restart() - } - } - - Rectangle { - anchors.fill: parent - radius: Theme.cornerRadius - color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) - border.width: 2 - border.color: Theme.primary - visible: dragging - z: -1 - } - - Timer { - id: longPressTimer - interval: 500 - repeat: false - onTriggered: { - if (appData && appData.isPinned) { - longPressing = true - } - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - anchors.bottomMargin: -20 - hoverEnabled: true - cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - - onPressed: mouse => { - if (mouse.button === Qt.LeftButton && appData - && appData.isPinned) { - dragStartPos = Qt.point(mouse.x, mouse.y) - longPressTimer.start() - } - } - - onReleased: mouse => { - longPressTimer.stop() - if (longPressing) { - if (dragging && targetIndex >= 0 - && targetIndex !== originalIndex && dockApps) { - dockApps.movePinnedApp(originalIndex, targetIndex) - } - - longPressing = false - dragging = false - dragOffset = Qt.point(0, 0) - targetIndex = -1 - originalIndex = -1 - } - } - - onPositionChanged: mouse => { - if (longPressing && !dragging) { - var distance = Math.sqrt( - Math.pow(mouse.x - dragStartPos.x, - 2) + Math.pow(mouse.y - dragStartPos.y, - 2)) - if (distance > 5) { - dragging = true - targetIndex = index - originalIndex = index - } - } - - if (dragging) { - dragOffset = Qt.point(mouse.x - dragStartPos.x, - mouse.y - dragStartPos.y) - - if (dockApps) { - var threshold = 40 - var newTargetIndex = targetIndex - - if (dragOffset.x > threshold - && targetIndex < dockApps.pinnedAppCount - 1) { - newTargetIndex = targetIndex + 1 - } else if (dragOffset.x < -threshold - && targetIndex > 0) { - newTargetIndex = targetIndex - 1 - } - - if (newTargetIndex !== targetIndex) { - targetIndex = newTargetIndex - dragStartPos = Qt.point(mouse.x, mouse.y) - } - } - } - } - - onClicked: mouse => { - if (!appData || longPressing) - return - - if (mouse.button === Qt.LeftButton) { - var windowCount = appData.windows ? appData.windows.count : 0 - - if (windowCount === 0) { - if (appData && appData.appId) { - var desktopEntry = DesktopEntries.byId(appData.appId) - if (desktopEntry) { - AppUsageHistoryData.addAppUsage({ - "id": appData.appId, - "name": desktopEntry.name - || appData.appId, - "icon": desktopEntry.icon - || "", - "exec": desktopEntry.exec - || "", - "comment": desktopEntry.comment - || "" - }) - } - Quickshell.execDetached(["gtk-launch", appData.appId]) - } - } else if (windowCount === 1) { - var window = appData.windows.get(0) - NiriService.focusWindow(window.id) - } else { - windowsMenu.showForButton(root, appData, 40) - } - } else if (mouse.button === Qt.MiddleButton) { - if (appData && appData.appId) { - var desktopEntry = DesktopEntries.byId(appData.appId) - if (desktopEntry) { - AppUsageHistoryData.addAppUsage({ - "id": appData.appId, - "name": desktopEntry.name - || appData.appId, - "icon": desktopEntry.icon - || "", - "exec": desktopEntry.exec - || "", - "comment": desktopEntry.comment - || "" - }) - } - Quickshell.execDetached(["gtk-launch", appData.appId]) - } - } else if (mouse.button === Qt.RightButton) { - if (contextMenu) { - contextMenu.showForButton(root, appData, 40) - } - } - } - } - - property bool showTooltip: mouseArea.containsMouse && !dragging - property string tooltipText: { - if (!appData || !appData.appId) - return "" - var desktopEntry = DesktopEntries.byId(appData.appId) - return desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId - } - - IconImage { - id: iconImg - width: 40 - height: 40 - anchors.centerIn: parent - source: { - if (!appData || !appData.appId) - return "" - var desktopEntry = DesktopEntries.byId(appData.appId) - if (desktopEntry && desktopEntry.icon) { - var iconPath = Quickshell.iconPath( - desktopEntry.icon, - SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) - return iconPath - } - return "" - } - smooth: true - mipmap: true - asynchronous: true - visible: status === Image.Ready - implicitSize: 40 - } - - Rectangle { - width: 40 - height: 40 - anchors.centerIn: parent - visible: !iconImg.visible - color: Theme.surfaceLight - radius: Theme.cornerRadius - border.width: 1 - border.color: Theme.primarySelected - - Text { - anchors.centerIn: parent - text: { - if (!appData || !appData.appId) - return "?" - var desktopEntry = DesktopEntries.byId(appData.appId) - if (desktopEntry && desktopEntry.name) { - return desktopEntry.name.charAt(0).toUpperCase() + // For window type, show app name + window title + if (appData.type === "window" && showWindowTitle) { + var desktopEntry = DesktopEntries.byId(appData.appId); + var appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId; + return appName + (windowTitle ? " • " + windowTitle : ""); } - return appData.appId.charAt(0).toUpperCase() - } - font.pixelSize: 14 - color: Theme.primary - font.weight: Font.Bold + // For pinned apps, just show app name + if (!appData.appId) + return ""; + + var desktopEntry = DesktopEntries.byId(appData.appId); + return desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId; } - } - Row { - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: -2 - spacing: 2 + width: 40 + height: 40 + onIsHoveredChanged: { + if (isHovered) { + exitAnimation.stop(); + if (!bounceAnimation.running) + bounceAnimation.restart(); - Repeater { - model: appData && appData.windows ? Math.min(appData.windows.count, 4) : 0 + } else { + bounceAnimation.stop(); + exitAnimation.restart(); + } + } - Rectangle { - width: appData && appData.windows && appData.windows.count <= 3 ? 5 : 3 + SequentialAnimation { + id: bounceAnimation + + running: false + + NumberAnimation { + target: translateY + property: "y" + to: -10 + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedAccel + } + + NumberAnimation { + target: translateY + property: "y" + to: -8 + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedDecel + } + + } + + NumberAnimation { + id: exitAnimation + + running: false + target: translateY + property: "y" + to: 0 + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedDecel + } + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) + border.width: 2 + border.color: Theme.primary + visible: dragging + z: -1 + } + + Timer { + id: longPressTimer + + interval: 500 + repeat: false + onTriggered: { + if (appData && appData.isPinned) + longPressing = true; + + } + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + anchors.bottomMargin: -20 + hoverEnabled: true + cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onPressed: (mouse) => { + if (mouse.button === Qt.LeftButton && appData && appData.isPinned) { + dragStartPos = Qt.point(mouse.x, mouse.y); + longPressTimer.start(); + } + } + onReleased: (mouse) => { + longPressTimer.stop(); + if (longPressing) { + if (dragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps) + dockApps.movePinnedApp(originalIndex, targetIndex); + + longPressing = false; + dragging = false; + dragOffset = Qt.point(0, 0); + targetIndex = -1; + originalIndex = -1; + } + } + onPositionChanged: (mouse) => { + if (longPressing && !dragging) { + var distance = Math.sqrt(Math.pow(mouse.x - dragStartPos.x, 2) + Math.pow(mouse.y - dragStartPos.y, 2)); + if (distance > 5) { + dragging = true; + targetIndex = index; + originalIndex = index; + } + } + if (dragging) { + dragOffset = Qt.point(mouse.x - dragStartPos.x, mouse.y - dragStartPos.y); + if (dockApps) { + var threshold = 40; + var newTargetIndex = targetIndex; + if (dragOffset.x > threshold && targetIndex < dockApps.pinnedAppCount - 1) + newTargetIndex = targetIndex + 1; + else if (dragOffset.x < -threshold && targetIndex > 0) + newTargetIndex = targetIndex - 1; + if (newTargetIndex !== targetIndex) { + targetIndex = newTargetIndex; + dragStartPos = Qt.point(mouse.x, mouse.y); + } + } + } + } + onClicked: (mouse) => { + if (!appData || longPressing) + return ; + + if (mouse.button === Qt.LeftButton) { + // Handle based on type + if (appData.type === "pinned") { + // Launch the pinned app + if (appData && appData.appId) { + var desktopEntry = DesktopEntries.byId(appData.appId); + if (desktopEntry) + AppUsageHistoryData.addAppUsage({ + "id": appData.appId, + "name": desktopEntry.name || appData.appId, + "icon": desktopEntry.icon || "", + "exec": desktopEntry.exec || "", + "comment": desktopEntry.comment || "" + }); + + Quickshell.execDetached(["gtk-launch", appData.appId]); + } + } else if (appData.type === "window") { + // Focus the specific window + if (appData.windowId) + NiriService.focusWindow(appData.windowId); + + } + } else if (mouse.button === Qt.MiddleButton) { + if (appData && appData.appId) { + var desktopEntry = DesktopEntries.byId(appData.appId); + if (desktopEntry) + AppUsageHistoryData.addAppUsage({ + "id": appData.appId, + "name": desktopEntry.name || appData.appId, + "icon": desktopEntry.icon || "", + "exec": desktopEntry.exec || "", + "comment": desktopEntry.comment || "" + }); + + Quickshell.execDetached(["gtk-launch", appData.appId]); + } + } else if (mouse.button === Qt.RightButton) { + if (contextMenu) + contextMenu.showForButton(root, appData, 40); + + } + } + } + + IconImage { + id: iconImg + + width: 40 + height: 40 + anchors.centerIn: parent + source: { + if (!appData || !appData.appId) + return ""; + + var desktopEntry = DesktopEntries.byId(appData.appId); + if (desktopEntry && desktopEntry.icon) { + var iconPath = Quickshell.iconPath(desktopEntry.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme); + return iconPath; + } + return ""; + } + smooth: true + mipmap: true + asynchronous: true + visible: status === Image.Ready + implicitSize: 40 + } + + Rectangle { + width: 40 + height: 40 + anchors.centerIn: parent + visible: !iconImg.visible + color: Theme.surfaceLight + radius: Theme.cornerRadius + border.width: 1 + border.color: Theme.primarySelected + + Text { + anchors.centerIn: parent + text: { + if (!appData || !appData.appId) + return "?"; + + var desktopEntry = DesktopEntries.byId(appData.appId); + if (desktopEntry && desktopEntry.name) + return desktopEntry.name.charAt(0).toUpperCase(); + + return appData.appId.charAt(0).toUpperCase(); + } + font.pixelSize: 14 + color: Theme.primary + font.weight: Font.Bold + } + + } + + // Indicator for running/focused state + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: -2 + width: 8 height: 2 radius: 1 + visible: appData && (appData.isRunning || appData.type === "window") color: { - if (!appData || !appData.windows || appData.windows.count === 0) - return "transparent" - var window = appData.windows.get(index) - return window - && window.id == NiriService.focusedWindowId ? Theme.primary : Qt.rgba( - Theme.surfaceText.r, - Theme.surfaceText.g, - Theme.surfaceText.b, - 0.6) + if (!appData) + return "transparent"; + + // For window type, check if focused + if (appData.type === "window" && appData.isFocused) + return Theme.primary; + + // For running apps, show dimmer indicator + if (appData.isRunning || appData.type === "window") + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6); + + return "transparent"; } - } } - } + + transform: Translate { + id: translateY + + y: 0 + } + } diff --git a/Modules/Dock/DockApps.qml b/Modules/Dock/DockApps.qml index 588f0d1b..863e5f2b 100644 --- a/Modules/Dock/DockApps.qml +++ b/Modules/Dock/DockApps.qml @@ -48,87 +48,65 @@ Item { clear() var items = [] - // Use ordered app IDs if available from Niri, fallback to unordered - var runningApps = NiriService.niriAvailable && NiriService.getRunningAppIdsOrdered - ? NiriService.getRunningAppIdsOrdered() - : NiriService.getRunningAppIds() var pinnedApps = [...(SessionData.pinnedApps || [])] - var addedApps = new Set() - + + // First section: Pinned apps (always visible, not representing running windows) pinnedApps.forEach(appId => { - var lowerAppId = appId.toLowerCase() - if (!addedApps.has(lowerAppId)) { - var windows = NiriService.getWindowsByAppId( - appId) - items.push({ - "appId": appId, - "windows": windows, - "isPinned": true, - "isRunning": windows.length > 0 - }) - addedApps.add(lowerAppId) - } - }) - root.pinnedAppCount = pinnedApps.length - var appUsageRanking = AppUsageHistoryData.appUsageRanking || {} - - var unpinnedApps = [] - var unpinnedAppsSet = new Set() - - // First: Add ALL currently running apps that aren't pinned - // They come pre-ordered from NiriService if Niri is available - runningApps.forEach(appId => { - var lowerAppId = appId.toLowerCase() - if (!addedApps.has(lowerAppId)) { - unpinnedApps.push(appId) - unpinnedAppsSet.add(lowerAppId) - } - }) - - // Then: Fill remaining slots up to 3 with recently used apps - var remainingSlots = Math.max(0, 3 - unpinnedApps.length) - if (remainingSlots > 0) { - // Sort recent apps by usage - var recentApps = [] - for (var appId in appUsageRanking) { - var lowerAppId = appId.toLowerCase() - if (!addedApps.has(lowerAppId) && !unpinnedAppsSet.has( - lowerAppId)) { - recentApps.push({ - "appId": appId, - "lastUsed": appUsageRanking[appId].lastUsed - || 0 - }) - } - } - recentApps.sort((a, b) => b.lastUsed - a.lastUsed) - - var recentToAdd = Math.min(remainingSlots, recentApps.length) - for (var i = 0; i < recentToAdd; i++) { - unpinnedApps.push(recentApps[i].appId) - } - } - if (pinnedApps.length > 0 && unpinnedApps.length > 0) { items.push({ - "appId": "__SEPARATOR__", - "windows": [], - "isPinned": false, - "isRunning": false - }) + "type": "pinned", + "appId": appId, + "windowId": -1, // Use -1 instead of null to avoid ListModel warnings + "windowTitle": "", + "workspaceId": -1, // Use -1 instead of null + "isPinned": true, + "isRunning": false, + "isFocused": false + }) + }) + + root.pinnedAppCount = pinnedApps.length + + // Add separator between pinned and running if both exist + if (pinnedApps.length > 0 && NiriService.windows.length > 0) { + items.push({ + "type": "separator", + "appId": "__SEPARATOR__", + "windowId": -1, // Use -1 instead of null + "windowTitle": "", + "workspaceId": -1, // Use -1 instead of null + "isPinned": false, + "isRunning": false, + "isFocused": false + }) } - unpinnedApps.forEach(appId => { - var windows = NiriService.getWindowsByAppId( - appId) - items.push({ - "appId": appId, - "windows": windows, - "isPinned": false, - "isRunning": windows.length > 0 - }) - }) + + // Second section: Running windows (sorted by display->workspace->position) + // NiriService.windows is already sorted by sortWindowsByLayout + NiriService.windows.forEach(window => { + // Limit window title length for tooltip + var title = window.title || "(Unnamed)" + if (title.length > 50) { + title = title.substring(0, 47) + "..." + } + + // Check if this window is focused - compare as numbers + var isFocused = window.id == NiriService.focusedWindowId + + items.push({ + "type": "window", + "appId": window.app_id || "", + "windowId": window.id || -1, + "windowTitle": title, + "workspaceId": window.workspace_id || -1, + "isPinned": false, + "isRunning": true, + "isFocused": isFocused + }) + }) + items.forEach(item => { - append(item) - }) + append(item) + }) } } @@ -136,11 +114,11 @@ Item { id: delegateItem property alias dockButton: button - width: model.appId === "__SEPARATOR__" ? 16 : 40 + width: model.type === "separator" ? 16 : 40 height: 40 Rectangle { - visible: model.appId === "__SEPARATOR__" + visible: model.type === "separator" width: 2 height: 20 color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) @@ -150,7 +128,7 @@ Item { DockAppButton { id: button - visible: model.appId !== "__SEPARATOR__" + visible: model.type !== "separator" anchors.centerIn: parent width: 40 @@ -161,6 +139,10 @@ Item { windowsMenu: root.windowsMenu dockApps: root index: model.index + + // Override tooltip for windows to show window title + showWindowTitle: model.type === "window" + windowTitle: model.windowTitle || "" } } } @@ -174,8 +156,12 @@ Item { function onWindowOpenedOrChanged() { dockModel.updateModel() } + function onFocusedWindowIdChanged() { + dockModel.updateModel() + } } + Connections { target: SessionData function onPinnedAppsChanged() { diff --git a/Modules/TopBar/FocusedApp.qml b/Modules/TopBar/FocusedApp.qml index eac4a750..5a224ff2 100644 --- a/Modules/TopBar/FocusedApp.qml +++ b/Modules/TopBar/FocusedApp.qml @@ -1,97 +1,108 @@ +import Quickshell import QtQuick import qs.Common import qs.Services import qs.Widgets Rectangle { - id: root + id: root - property bool compactMode: SettingsData.focusedWindowCompactMode - property int availableWidth: 400 - readonly property int baseWidth: contentRow.implicitWidth + Theme.spacingS * 2 - readonly property int maxNormalWidth: 456 - readonly property int maxCompactWidth: 288 + property bool compactMode: SettingsData.focusedWindowCompactMode + property int availableWidth: 400 + readonly property int baseWidth: contentRow.implicitWidth + Theme.spacingS * 2 + readonly property int maxNormalWidth: 456 + readonly property int maxCompactWidth: 288 - width: compactMode ? Math.min(baseWidth, - maxCompactWidth) : Math.min(baseWidth, - maxNormalWidth) - height: 30 - radius: Theme.cornerRadius - color: { - if (!FocusedWindowService.focusedAppName - && !FocusedWindowService.focusedWindowTitle) - return "transparent" + width: compactMode ? Math.min(baseWidth, maxCompactWidth) : Math.min(baseWidth, maxNormalWidth) + height: 30 + radius: Theme.cornerRadius + color: { + if (!NiriService.focusedWindowTitle) + return "transparent"; - const baseColor = mouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover - return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, - baseColor.a * Theme.widgetTransparency) - } - clip: true - visible: FocusedWindowService.niriAvailable - && (FocusedWindowService.focusedAppName - || FocusedWindowService.focusedWindowTitle) + const baseColor = mouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); + } + clip: true + visible: NiriService.niriAvailable && NiriService.focusedWindowTitle - Row { - id: contentRow + Row { + id: contentRow - anchors.centerIn: parent - spacing: Theme.spacingS + anchors.centerIn: parent + spacing: Theme.spacingS - StyledText { - id: appText + StyledText { + id: appText + + text: { + if (!NiriService.focusedWindowId) + return ""; + + var window = NiriService.windows.find((w) => { + return w.id == NiriService.focusedWindowId; + }); + if (!window || !window.app_id) + return ""; + + var desktopEntry = DesktopEntries.byId(window.app_id); + return desktopEntry && desktopEntry.name ? desktopEntry.name : window.app_id; + } + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + maximumLineCount: 1 + width: Math.min(implicitWidth, compactMode ? 80 : 180) + visible: !compactMode && text.length > 0 + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.outlineButton + anchors.verticalCenter: parent.verticalCenter + visible: !compactMode && appText.text && titleText.text + } + + StyledText { + id: titleText + + text: NiriService.focusedWindowTitle || "" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + maximumLineCount: 1 + width: Math.min(implicitWidth, compactMode ? 280 : 250) + visible: text.length > 0 + } - text: FocusedWindowService.focusedAppName || "" - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight - maximumLineCount: 1 - width: Math.min(implicitWidth, compactMode ? 80 : 180) - visible: !compactMode && text.length > 0 } - StyledText { - text: "•" - font.pixelSize: Theme.fontSizeSmall - color: Theme.outlineButton - anchors.verticalCenter: parent.verticalCenter - visible: !compactMode && appText.text && titleText.text + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true } - StyledText { - id: titleText + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } - text: FocusedWindowService.focusedWindowTitle || "" - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight - maximumLineCount: 1 - width: Math.min(implicitWidth, compactMode ? 280 : 250) - visible: text.length > 0 } - } - MouseArea { - id: mouseArea + Behavior on width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } - anchors.fill: parent - hoverEnabled: true - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing } - } - Behavior on width { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } } diff --git a/Modules/TopBar/RunningApps.qml b/Modules/TopBar/RunningApps.qml index 7624177a..09ac4b17 100644 --- a/Modules/TopBar/RunningApps.qml +++ b/Modules/TopBar/RunningApps.qml @@ -8,18 +8,16 @@ import qs.Widgets Rectangle { id: root - + property string section: "left" property var parentScreen property var hoveredItem: null property var topBar: null - // The visual root for this window property Item windowRoot: (Window.window ? Window.window.contentItem : null) - readonly property int windowCount: NiriService.windows.length readonly property int calculatedWidth: windowCount > 0 ? windowCount * 24 + (windowCount - 1) * Theme.spacingXS + Theme.spacingS * 2 : 0 - + width: calculatedWidth height: 30 radius: Theme.cornerRadius @@ -27,135 +25,148 @@ Rectangle { clip: false color: { if (windowCount === 0) - return "transparent" - - const baseColor = Theme.secondaryHover - return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, - baseColor.a * Theme.widgetTransparency) + return "transparent"; + + const baseColor = Theme.secondaryHover; + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); } - + Row { id: windowRow + anchors.centerIn: parent spacing: Theme.spacingXS - + Repeater { id: windowRepeater + model: NiriService.windows - + delegate: Item { id: delegateItem - property bool isFocused: String(modelData.id) === String(FocusedWindowService.focusedWindowId) + + property bool isFocused: String(modelData.id) === String(NiriService.focusedWindowId) property string appId: modelData.app_id || "" property string windowTitle: modelData.title || "(Unnamed)" property int windowId: modelData.id property string tooltipText: { - var appName = "Unknown" + var appName = "Unknown"; if (appId) { - var desktopEntry = DesktopEntries.byId(appId) - appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId + var desktopEntry = DesktopEntries.byId(appId); + appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId; } - return appName + (windowTitle ? " • " + windowTitle : "") + return appName + (windowTitle ? " • " + windowTitle : ""); } - + width: 24 height: 24 - + Rectangle { anchors.fill: parent radius: Theme.cornerRadius color: { - if (isFocused) { - return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) - } else { - return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent" - } + if (isFocused) + return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2); + else + return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent"; } - + Behavior on color { ColorAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing } + } + } - + // App icon IconImage { id: iconImg + anchors.centerIn: parent width: 18 height: 18 source: { - if (!appId) return "" - var desktopEntry = DesktopEntries.byId(appId) + if (!appId) + return ""; + + var desktopEntry = DesktopEntries.byId(appId); if (desktopEntry && desktopEntry.icon) { - var iconPath = Quickshell.iconPath( - desktopEntry.icon, - SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) - return iconPath + var iconPath = Quickshell.iconPath(desktopEntry.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme); + return iconPath; } - return "" + return ""; } smooth: true mipmap: true asynchronous: true visible: status === Image.Ready } - + // Fallback text if no icon found Text { anchors.centerIn: parent visible: !iconImg.visible text: { - if (!appId) return "?" - var desktopEntry = DesktopEntries.byId(appId) - if (desktopEntry && desktopEntry.name) { - return desktopEntry.name.charAt(0).toUpperCase() - } - return appId.charAt(0).toUpperCase() + if (!appId) + return "?"; + + var desktopEntry = DesktopEntries.byId(appId); + if (desktopEntry && desktopEntry.name) + return desktopEntry.name.charAt(0).toUpperCase(); + + return appId.charAt(0).toUpperCase(); } font.pixelSize: 10 color: Theme.surfaceText font.weight: Font.Medium } - + MouseArea { id: mouseArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: { - NiriService.focusWindow(windowId) + NiriService.focusWindow(windowId); } onEntered: { - root.hoveredItem = delegateItem - var globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height) - tooltipLoader.active = true + root.hoveredItem = delegateItem; + var globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height); + tooltipLoader.active = true; if (tooltipLoader.item) { - var tooltipY = Theme.barHeight + SettingsData.topBarSpacing + Theme.spacingXS - tooltipLoader.item.showTooltip(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen) + var tooltipY = Theme.barHeight + SettingsData.topBarSpacing + Theme.spacingXS; + tooltipLoader.item.showTooltip(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen); } } onExited: { if (root.hoveredItem === delegateItem) { - root.hoveredItem = null - if (tooltipLoader.item) { - tooltipLoader.item.hideTooltip() - } - tooltipLoader.active = false + root.hoveredItem = null; + if (tooltipLoader.item) + tooltipLoader.item.hideTooltip(); + + tooltipLoader.active = false; } } } + } + } + } - + Loader { id: tooltipLoader + active: false + sourceComponent: RunningAppsTooltip { } + } + } diff --git a/Services/FocusedWindowService.qml b/Services/FocusedWindowService.qml deleted file mode 100644 index 21289f2b..00000000 --- a/Services/FocusedWindowService.qml +++ /dev/null @@ -1,116 +0,0 @@ -pragma Singleton - -pragma ComponentBehavior - -import QtQuick -import Quickshell -import Quickshell.Io - -Singleton { - id: root - - property bool niriAvailable: false - property string focusedAppId: "" - property string focusedAppName: "" - property string focusedWindowTitle: "" - property int focusedWindowId: -1 - - function updateFromNiriData() { - if (!root.niriAvailable) { - clearFocusedWindow() - return - } - - let focusedWindow = NiriService.windows.find(w => w.is_focused) - - if (focusedWindow) { - root.focusedAppId = focusedWindow.app_id || "" - root.focusedWindowTitle = focusedWindow.title || "" - root.focusedAppName = getDisplayName(focusedWindow.app_id || "") - root.focusedWindowId = parseInt(focusedWindow.id) || -1 - } else { - setWorkspaceFallback() - } - } - - function clearFocusedWindow() { - root.focusedAppId = "" - root.focusedAppName = "" - root.focusedWindowTitle = "" - root.focusedWindowId = -1 - } - - function setWorkspaceFallback() { - if (NiriService.focusedWorkspaceIndex >= 0 && NiriService.allWorkspaces.length > 0) { - const workspace = NiriService.allWorkspaces[NiriService.focusedWorkspaceIndex] - if (workspace) { - root.focusedAppId = "niri" - root.focusedAppName = "niri" - if (workspace.name && workspace.name.length > 0) { - root.focusedWindowTitle = workspace.name - } else { - root.focusedWindowTitle = "workspace " + workspace.idx - } - root.focusedWindowId = -1 - } else { - clearFocusedWindow() - } - } else { - clearFocusedWindow() - } - } - - function getDisplayName(appId) { - if (!appId) - return "" - const desktopEntry = DesktopEntries.byId(appId) - return desktopEntry && desktopEntry.name ? desktopEntry.name : "" - } - - Component.onCompleted: { - root.niriAvailable = NiriService.niriAvailable - NiriService.onNiriAvailableChanged.connect(() => { - root.niriAvailable = NiriService.niriAvailable - if (root.niriAvailable) - updateFromNiriData() - }) - if (root.niriAvailable) - updateFromNiriData() - } - - Connections { - function onFocusedWindowIdChanged() { - const focusedWindowId = NiriService.focusedWindowId - if (!focusedWindowId) { - setWorkspaceFallback() - return - } - - const focusedWindow = NiriService.windows.find( - w => w.id == focusedWindowId) - if (focusedWindow) { - root.focusedAppId = focusedWindow.app_id || "" - root.focusedWindowTitle = focusedWindow.title || "" - root.focusedAppName = getDisplayName(focusedWindow.app_id || "") - root.focusedWindowId = parseInt(focusedWindow.id) || -1 - } else { - setWorkspaceFallback() - } - } - - function onWindowsChanged() { - updateFromNiriData() - } - - function onWindowOpenedOrChanged(windowData) { - if (windowData.is_focused) { - root.focusedAppId = windowData.app_id || "" - root.focusedWindowTitle = windowData.title || "" - root.focusedAppName = getDisplayName(windowData.app_id || "") - root.focusedWindowId = parseInt(windowData.id) || -1 - } - } - - target: NiriService - } -} diff --git a/Services/NiriService.qml b/Services/NiriService.qml index c1fd4cb0..3b37ab85 100644 --- a/Services/NiriService.qml +++ b/Services/NiriService.qml @@ -307,6 +307,17 @@ Singleton { function handleWindowsChanged(data) { windows = sortWindowsByLayout(data.windows) + + // Extract focused window from initial state + var focusedWindow = windows.find(w => w.is_focused) + if (focusedWindow) { + focusedWindowId = String(focusedWindow.id) + focusedWindowIndex = windows.findIndex(w => w.id === focusedWindow.id) + } else { + focusedWindowId = "" + focusedWindowIndex = -1 + } + updateFocusedWindow() }