From 56de8e8f3a401cfc4f46beb999db2d4d30689f74 Mon Sep 17 00:00:00 2001 From: enhulsman Date: Wed, 25 Feb 2026 16:04:58 +0100 Subject: [PATCH 1/3] fix(overview): decouple navigation from Hyprland dispatch Introduce local workspace tracking (trackedWorkspaceId) so Tab/arrow key navigation, window clicks, and Enter-with-empty-search no longer dispatch Hyprland IPC commands while the focus grab is active. This prevents the overview from closing itself mid-interaction due to IPC-triggered onCleared events. Changes: - Tab/Shift-Tab/Arrow keys update trackedWorkspaceId locally - Enter with empty search closes overview, then dispatches workspace - Single-click on window emits workspaceNavigated signal (no dispatch) - Double-click on window closes overview first, then focuses - Remove focuswindow dispatch on hover (prevents cursor warping) - Add "workspace" to HyprlandData IPC ignore list - Dynamic badge sizing (12% of min dimension, floor 12px) - Workspace tile background color prevents white bleed-through - focusedWorkspaceIndicator z:10 for correct render order --- modules/bar/workspaces/HyprlandData.qml | 3 +- modules/widgets/overview/Overview.qml | 32 ++++++--- modules/widgets/overview/OverviewPopup.qml | 67 ++++++++++--------- modules/widgets/overview/OverviewView.qml | 9 +++ modules/widgets/overview/OverviewWindow.qml | 33 ++++----- .../widgets/overview/ScrollingOverview.qml | 11 ++- .../widgets/overview/ScrollingWorkspace.qml | 18 +++-- 7 files changed, 99 insertions(+), 74 deletions(-) diff --git a/modules/bar/workspaces/HyprlandData.qml b/modules/bar/workspaces/HyprlandData.qml index d71ab834..f2e46ba7 100644 --- a/modules/bar/workspaces/HyprlandData.qml +++ b/modules/bar/workspaces/HyprlandData.qml @@ -58,7 +58,8 @@ Singleton { let ignoreList = [ "activewindow", "focusedmon", "monitoradded", "createworkspace", "destroyworkspace", "moveworkspace", - "activespecial", "movewindow", "windowtitle" + "activespecial", "movewindow", "windowtitle", + "workspace" ] if (ignoreList.includes(event.name)) return updateWindowList() diff --git a/modules/widgets/overview/Overview.qml b/modules/widgets/overview/Overview.qml index 7fb49fa3..98eae35a 100644 --- a/modules/widgets/overview/Overview.qml +++ b/modules/widgets/overview/Overview.qml @@ -26,8 +26,10 @@ Item { // Use the screen's monitor instead of focused monitor for multi-monitor support property var currentScreen: null // This will be set from parent + property int trackedWorkspaceId: 1 + signal workspaceNavigated(int wsId) readonly property var monitor: currentScreen ? Hyprland.monitorFor(currentScreen) : Hyprland.focusedMonitor - readonly property int workspaceGroup: Math.floor((monitor?.activeWorkspace?.id - 1 || 0) / workspacesShown) + readonly property int workspaceGroup: Math.floor((trackedWorkspaceId - 1 || 0) / workspacesShown) // Cache these references readonly property var windowList: HyprlandData.windowList @@ -236,7 +238,7 @@ Item { implicitWidth: overviewRoot.workspaceImplicitWidth + workspacePadding implicitHeight: overviewRoot.workspaceImplicitHeight + workspacePadding - color: "transparent" + color: Colors.background radius: Styling.radius(2) border.width: 2 border.color: hoveredWhileDragging ? hoveredBorderColor : "transparent" @@ -263,15 +265,15 @@ Item { acceptedButtons: Qt.LeftButton onClicked: { if (overviewRoot.draggingTargetWorkspace === -1) { - // Only switch workspace, don't close overview - Hyprland.dispatch(`workspace ${workspaceValue}`); + overviewRoot.workspaceNavigated(workspaceValue); } } onDoubleClicked: { if (overviewRoot.draggingTargetWorkspace === -1) { - // Double click closes overview and switches workspace Visibilities.setActiveModule(""); - Hyprland.dispatch(`workspace ${workspaceValue}`); + if (workspaceValue !== (overviewRoot.monitor?.activeWorkspace?.id || -1)) { + Hyprland.dispatch(`workspace ${workspaceValue}`); + } } } } @@ -350,23 +352,31 @@ Item { Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${windowData?.address}`); } } - onWindowClicked: { - // Close overview and focus the specific clicked window - // Skip generic focus restoration since we're handling it specifically + onWindowClicked: (clickSceneX, clickSceneY) => { + // Capture values before closing — delegate gets destroyed after close + var addr = windowData.address; + var sameMonitor = (windowData.monitor === overviewRoot.monitorId); + var absX = Math.round((overviewRoot.monitorData?.x || 0) + clickSceneX); + var absY = Math.round((overviewRoot.monitorData?.y || 0) + clickSceneY); Visibilities.setActiveModule("", true); Qt.callLater(() => { - Hyprland.dispatch(`focuswindow address:${windowData.address}`); + Hyprland.dispatch(`focuswindow address:${addr}`); + if (sameMonitor) { + Hyprland.dispatch(`movecursor ${absX} ${absY}`); + } }); } onWindowClosed: { Hyprland.dispatch(`closewindow address:${windowData.address}`); } + onWorkspaceNavigated: wsId => overviewRoot.workspaceNavigated(wsId) } } Rectangle { id: focusedWorkspaceIndicator - property int activeWorkspaceInGroup: (monitor?.activeWorkspace?.id || 1) - (overviewRoot.workspaceGroup * overviewRoot.workspacesShown) + z: 10 + property int activeWorkspaceInGroup: overviewRoot.trackedWorkspaceId - (overviewRoot.workspaceGroup * overviewRoot.workspacesShown) property int activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / overviewRoot.columns) property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % overviewRoot.columns diff --git a/modules/widgets/overview/OverviewPopup.qml b/modules/widgets/overview/OverviewPopup.qml index 708fdfff..962faa7a 100644 --- a/modules/widgets/overview/OverviewPopup.qml +++ b/modules/widgets/overview/OverviewPopup.qml @@ -31,6 +31,9 @@ PanelWindow { readonly property var screenVisibilities: Visibilities.getForScreen(screen.name) readonly property bool overviewOpen: screenVisibilities ? screenVisibilities.overview : false + property int trackedWorkspaceId: 1 + readonly property int navigableWorkspaces: Math.max(Config.workspaces.shown, 1) + visible: overviewOpen exclusionMode: ExclusionMode.Ignore @@ -192,34 +195,34 @@ PanelWindow { } onAccepted: { - if (overviewLoader.item) { - overviewLoader.item.navigateToSelectedWindow(); + if (searchInput.text.length > 0) { + if (overviewLoader.item) { + overviewLoader.item.navigateToSelectedWindow(); + } + } else { + Visibilities.setActiveModule(""); + var mon = overviewPopup.screen ? Hyprland.monitorFor(overviewPopup.screen) : null; + if (overviewPopup.trackedWorkspaceId !== (mon?.activeWorkspace?.id || -1)) { + Hyprland.dispatch("workspace " + overviewPopup.trackedWorkspaceId); + } } } onTabPressed: { if (searchInput.text.length === 0) { - const current = Hyprland.focusedWorkspace?.id || 1; - const next = current + 1; - if (next > Config.workspaces.shown) { - Hyprland.dispatch("workspace 1"); - } else { - Hyprland.dispatch("workspace r+1"); - } + var next = overviewPopup.trackedWorkspaceId + 1; + if (next > overviewPopup.navigableWorkspaces) next = 1; + overviewPopup.trackedWorkspaceId = next; } else if (overviewLoader.item) { overviewLoader.item.selectNextMatch(); } } - + onShiftTabPressed: { if (searchInput.text.length === 0) { - const current = Hyprland.focusedWorkspace?.id || 1; - const prev = current - 1; - if (prev < 1) { - Hyprland.dispatch("workspace " + Config.workspaces.shown); - } else { - Hyprland.dispatch("workspace r-1"); - } + var prev = overviewPopup.trackedWorkspaceId - 1; + if (prev < 1) prev = overviewPopup.navigableWorkspaces; + overviewPopup.trackedWorkspaceId = prev; } else if (overviewLoader.item) { overviewLoader.item.selectPrevMatch(); } @@ -250,13 +253,9 @@ PanelWindow { onLeftPressed: { if (searchInput.text.length === 0) { - const current = Hyprland.focusedWorkspace?.id || 1; - const prev = current - 1; - if (prev < 1) { - Hyprland.dispatch("workspace " + Config.workspaces.shown); - } else { - Hyprland.dispatch("workspace r-1"); - } + var prev = overviewPopup.trackedWorkspaceId - 1; + if (prev < 1) prev = overviewPopup.navigableWorkspaces; + overviewPopup.trackedWorkspaceId = prev; } else if (overviewLoader.item) { overviewLoader.item.selectPrevMatch(); } @@ -264,13 +263,9 @@ PanelWindow { onRightPressed: { if (searchInput.text.length === 0) { - const current = Hyprland.focusedWorkspace?.id || 1; - const next = current + 1; - if (next > Config.workspaces.shown) { - Hyprland.dispatch("workspace 1"); - } else { - Hyprland.dispatch("workspace r+1"); - } + var next = overviewPopup.trackedWorkspaceId + 1; + if (next > overviewPopup.navigableWorkspaces) next = 1; + overviewPopup.trackedWorkspaceId = next; } else if (overviewLoader.item) { overviewLoader.item.selectNextMatch(); } @@ -307,10 +302,18 @@ PanelWindow { sourceComponent: OverviewView { currentScreen: overviewPopup.screen + trackedWorkspaceId: overviewPopup.trackedWorkspaceId } } } + Connections { + target: overviewLoader.item + function onWorkspaceNavigated(wsId) { + overviewPopup.trackedWorkspaceId = wsId; + } + } + // External scrollbar for scrolling mode (to the right of overview) StyledRect { id: scrollbarContainer @@ -388,6 +391,8 @@ PanelWindow { // Ensure focus when overview opens onOverviewOpenChanged: { if (overviewOpen) { + var mon = screen ? Hyprland.monitorFor(screen) : null; + trackedWorkspaceId = mon?.activeWorkspace?.id || 1; Qt.callLater(() => { searchInput.clear(); if (overviewLoader.item) { diff --git a/modules/widgets/overview/OverviewView.qml b/modules/widgets/overview/OverviewView.qml index 53a43b9a..aee55604 100644 --- a/modules/widgets/overview/OverviewView.qml +++ b/modules/widgets/overview/OverviewView.qml @@ -7,6 +7,8 @@ import qs.config Item { id: root property var currentScreen + property int trackedWorkspaceId: 1 + signal workspaceNavigated(int wsId) // Detect if we're in scrolling layout mode readonly property bool isScrollingLayout: GlobalStates.hyprlandLayout === "scrolling" @@ -51,11 +53,17 @@ Item { sourceComponent: isScrollingLayout ? scrollingOverviewComponent : standardOverviewComponent } + Connections { + target: overviewLoader.item + function onWorkspaceNavigated(wsId) { root.workspaceNavigated(wsId) } + } + // Standard grid overview Component { id: standardOverviewComponent Overview { currentScreen: root.currentScreen + trackedWorkspaceId: root.trackedWorkspaceId Keys.onPressed: event => { if (event.key === Qt.Key_Escape) { @@ -75,6 +83,7 @@ Item { id: scrollingOverviewComponent ScrollingOverview { currentScreen: root.currentScreen + trackedWorkspaceId: root.trackedWorkspaceId Keys.onPressed: event => { if (event.key === Qt.Key_Escape) { diff --git a/modules/widgets/overview/OverviewWindow.qml b/modules/widgets/overview/OverviewWindow.qml index de205a60..1f1ee1c1 100644 --- a/modules/widgets/overview/OverviewWindow.qml +++ b/modules/widgets/overview/OverviewWindow.qml @@ -64,8 +64,9 @@ Item { signal dragStarted signal dragFinished(int targetWorkspace) - signal windowClicked + signal windowClicked(real clickSceneX, real clickSceneY) signal windowClosed + signal workspaceNavigated(int wsId) x: initX y: initY @@ -215,16 +216,17 @@ Item { // Overlay icon when preview is available (smaller, in corner) Image { mipmap: true + readonly property real badgeSize: Math.round(Math.max(Math.min(root.targetWindowWidth, root.targetWindowHeight) * 0.12, 12)) visible: windowPreview.hasContent && !root.compactMode && Config.performance.windowPreview anchors.bottom: parent.bottom anchors.right: parent.right - anchors.margins: 4 - width: 16 - height: 16 + anchors.margins: Math.round(badgeSize * 0.2) + width: badgeSize + height: badgeSize source: Quickshell.iconPath(root.iconPath, "image-missing") - sourceSize: Qt.size(16, 16) + sourceSize: Qt.size(badgeSize, badgeSize) asynchronous: true - opacity: 0.8 + opacity: 0.9 z: 10 } @@ -250,17 +252,6 @@ Item { onEntered: { root.hovered = true; - // Only focus window on hover if it's in the current workspace - if (root.windowData) { - // Get current active workspace from Hyprland - let currentWorkspace = Hyprland.focusedMonitor?.activeWorkspace?.id; - let windowWorkspace = root.windowData?.workspace?.id; - - // Only focus if the window is in the current workspace - if (currentWorkspace && windowWorkspace && currentWorkspace === windowWorkspace) { - Hyprland.dispatch(`focuswindow address:${windowData.address}`); - } - } } onExited: root.hovered = false @@ -372,8 +363,7 @@ Item { return; if (mouse.button === Qt.LeftButton) { - // Single click just focuses the window without closing overview - Hyprland.dispatch(`focuswindow address:${windowData.address}`); + root.workspaceNavigated(windowData.workspace.id); } else if (mouse.button === Qt.MiddleButton) { root.windowClosed(); } @@ -384,8 +374,9 @@ Item { return; if (mouse.button === Qt.LeftButton) { - // Double click closes overview and focuses window - root.windowClicked(); + // Map click to scene coordinates (= screen-local, since popup is fullscreen) + var scenePos = dragArea.mapToItem(null, mouse.x, mouse.y); + root.windowClicked(scenePos.x, scenePos.y); } } } diff --git a/modules/widgets/overview/ScrollingOverview.qml b/modules/widgets/overview/ScrollingOverview.qml index 67183850..d4d6d779 100644 --- a/modules/widgets/overview/ScrollingOverview.qml +++ b/modules/widgets/overview/ScrollingOverview.qml @@ -14,6 +14,9 @@ import qs.config Item { id: scrollingOverviewRoot + property int trackedWorkspaceId: 1 + signal workspaceNavigated(int wsId) + // Config values readonly property real scale: Config.overview.scale readonly property int totalWorkspaces: Config.overview.rows * Config.overview.columns @@ -211,7 +214,7 @@ Item { property bool isManualScrolling: false // Calculate target scroll position to center active workspace - readonly property int activeWorkspaceId: monitor?.activeWorkspace?.id || 1 + readonly property int activeWorkspaceId: trackedWorkspaceId readonly property real workspaceRowHeight: workspaceHeight + workspaceSpacing // Scroll to center active workspace when it changes @@ -271,7 +274,7 @@ Item { barPosition: scrollingOverviewRoot.barPosition barReserved: scrollingOverviewRoot.barReserved windowList: scrollingOverviewRoot.windowList - isActive: (scrollingOverviewRoot.monitor?.activeWorkspace?.id || 0) === workspaceId + isActive: scrollingOverviewRoot.trackedWorkspaceId === workspaceId activeBorderColor: scrollingOverviewRoot.activeBorderColor focusedWindowAddress: scrollingOverviewRoot.focusedWindowAddress @@ -298,6 +301,8 @@ Item { dragOverlay: dragOverlayItem overviewRoot: scrollingOverviewRoot + onWorkspaceNavigated: wsId => scrollingOverviewRoot.workspaceNavigated(wsId) + width: scrollingOverviewRoot.workspaceWidth height: scrollingOverviewRoot.workspaceHeight } @@ -307,7 +312,7 @@ Item { // Floating active workspace indicator (inside content, moves with scroll) Rectangle { id: focusedWorkspaceIndicator - readonly property int activeWorkspaceId: scrollingOverviewRoot.monitor?.activeWorkspace?.id || 1 + readonly property int activeWorkspaceId: scrollingOverviewRoot.trackedWorkspaceId x: 0 y: (activeWorkspaceId - 1) * (workspaceHeight + workspaceSpacing) diff --git a/modules/widgets/overview/ScrollingWorkspace.qml b/modules/widgets/overview/ScrollingWorkspace.qml index 6a8e5f08..304595c4 100644 --- a/modules/widgets/overview/ScrollingWorkspace.qml +++ b/modules/widgets/overview/ScrollingWorkspace.qml @@ -33,6 +33,7 @@ Item { property int draggingTargetWorkspace: -1 property Item dragOverlay: null property Item overviewRoot: null + signal workspaceNavigated(int wsId) // Callbacks for search matching (set by parent) property var checkWindowMatched: function (addr) { @@ -265,8 +266,10 @@ Item { TapHandler { acceptedButtons: Qt.LeftButton onDoubleTapped: { - Hyprland.dispatch(`workspace ${root.workspaceId}`); Visibilities.setActiveModule("", true); + if (root.overviewRoot && root.workspaceId !== (root.overviewRoot.monitor?.activeWorkspace?.id || -1)) { + Hyprland.dispatch(`workspace ${root.workspaceId}`); + } } } @@ -428,16 +431,17 @@ Item { // Corner icon when preview available Image { mipmap: true + readonly property real badgeSize: Math.round(Math.max(Math.min(windowDelegate.targetWidth, windowDelegate.targetHeight) * 0.12, 12)) visible: windowPreview.hasContent && !windowDelegate.compactMode && Config.performance.windowPreview anchors.bottom: parent.bottom anchors.right: parent.right - anchors.margins: 4 - width: 16 - height: 16 + anchors.margins: Math.round(badgeSize * 0.2) + width: badgeSize + height: badgeSize source: Quickshell.iconPath(windowDelegate.iconPath, "image-missing") - sourceSize: Qt.size(16, 16) + sourceSize: Qt.size(badgeSize, badgeSize) asynchronous: true - opacity: 0.8 + opacity: 0.9 z: 10 } @@ -682,7 +686,7 @@ Item { if (!windowDelegate.windowData) return; if (mouse.button === Qt.LeftButton && !windowDelegate.dragging) { - Hyprland.dispatch(`focuswindow address:${windowDelegate.windowData.address}`); + root.workspaceNavigated(windowDelegate.windowData.workspace.id); } else if (mouse.button === Qt.MiddleButton) { Hyprland.dispatch(`closewindow address:${windowDelegate.windowData.address}`); } From 14507289e41eda96c27f48d7146919cf56956a37 Mon Sep 17 00:00:00 2001 From: enhulsman Date: Wed, 25 Feb 2026 16:32:11 +0100 Subject: [PATCH 2/3] fix(overview): eliminate hover flicker on window previews MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Animate border.color instead of border.width on hover. The width 0→2 animation caused geometry changes that produced a brief visual glitch on the screencopy preview. --- modules/widgets/overview/OverviewWindow.qml | 16 ++++++++-------- modules/widgets/overview/ScrollingWorkspace.qml | 15 +++++++++++---- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/modules/widgets/overview/OverviewWindow.qml b/modules/widgets/overview/OverviewWindow.qml index 1f1ee1c1..ca1c36e2 100644 --- a/modules/widgets/overview/OverviewWindow.qml +++ b/modules/widgets/overview/OverviewWindow.qml @@ -147,8 +147,8 @@ Item { anchors.fill: parent radius: root.calculatedRadius color: pressed ? Colors.surfaceBright : hovered ? Colors.surface : Colors.background - border.color: root.isSearchSelected ? Colors.tertiary : root.isSearchMatch ? Styling.srItem("overprimary") : Styling.srItem("overprimary") - border.width: root.isSearchSelected ? 3 : root.isSearchMatch ? 2 : (hovered ? 2 : 0) + border.color: (root.isSearchSelected || root.isSearchMatch || hovered) ? (root.isSearchSelected ? Colors.tertiary : Styling.srItem("overprimary")) : "transparent" + border.width: root.isSearchSelected ? 3 : 2 visible: !windowPreview.hasContent || !Config.performance.windowPreview Behavior on color { @@ -158,9 +158,9 @@ Item { } } - Behavior on border.width { + Behavior on border.color { enabled: Config.animDuration > 0 - NumberAnimation { + ColorAnimation { duration: Config.animDuration / 2 } } @@ -187,14 +187,14 @@ Item { anchors.fill: parent radius: root.calculatedRadius color: pressed ? Qt.rgba(Colors.surfaceContainerHighest.r, Colors.surfaceContainerHighest.g, Colors.surfaceContainerHighest.b, 0.5) : hovered ? Qt.rgba(Colors.surfaceContainer.r, Colors.surfaceContainer.g, Colors.surfaceContainer.b, 0.2) : "transparent" - border.color: root.isSearchSelected ? Colors.tertiary : root.isSearchMatch ? Styling.srItem("overprimary") : Styling.srItem("overprimary") - border.width: root.isSearchSelected ? 3 : root.isSearchMatch ? 2 : (hovered ? 2 : 0) + border.color: (root.isSearchSelected || root.isSearchMatch || hovered) ? (root.isSearchSelected ? Colors.tertiary : Styling.srItem("overprimary")) : "transparent" + border.width: root.isSearchSelected ? 3 : 2 visible: windowPreview.hasContent && Config.performance.windowPreview z: 5 - Behavior on border.width { + Behavior on border.color { enabled: Config.animDuration > 0 - NumberAnimation { + ColorAnimation { duration: Config.animDuration / 2 } } diff --git a/modules/widgets/overview/ScrollingWorkspace.qml b/modules/widgets/overview/ScrollingWorkspace.qml index 304595c4..8fec52d1 100644 --- a/modules/widgets/overview/ScrollingWorkspace.qml +++ b/modules/widgets/overview/ScrollingWorkspace.qml @@ -389,8 +389,8 @@ Item { anchors.fill: parent radius: windowDelegate.calculatedRadius color: windowDelegate.dragging ? Colors.surfaceBright : windowDelegate.hovered ? Colors.surface : Colors.background - border.color: windowDelegate.isSelected ? Colors.tertiary : windowDelegate.isMatched ? Styling.srItem("overprimary") : Styling.srItem("overprimary") - border.width: windowDelegate.isSelected ? 3 : windowDelegate.isMatched ? 2 : (windowDelegate.hovered ? 2 : 0) + border.color: (windowDelegate.isSelected || windowDelegate.isMatched || windowDelegate.hovered) ? (windowDelegate.isSelected ? Colors.tertiary : Styling.srItem("overprimary")) : "transparent" + border.width: windowDelegate.isSelected ? 3 : 2 visible: !Config.performance.windowPreview Behavior on color { @@ -399,6 +399,13 @@ Item { duration: Config.animDuration / 2 } } + + Behavior on border.color { + enabled: Config.animDuration > 0 + ColorAnimation { + duration: Config.animDuration / 2 + } + } } // Icon @@ -422,8 +429,8 @@ Item { anchors.fill: parent radius: windowDelegate.calculatedRadius color: windowDelegate.dragging ? Qt.rgba(Colors.surfaceContainerHighest.r, Colors.surfaceContainerHighest.g, Colors.surfaceContainerHighest.b, 0.5) : windowDelegate.hovered ? Qt.rgba(Colors.surfaceContainer.r, Colors.surfaceContainer.g, Colors.surfaceContainer.b, 0.2) : "transparent" - border.color: windowDelegate.isSelected ? Colors.tertiary : windowDelegate.isMatched ? Styling.srItem("overprimary") : Styling.srItem("overprimary") - border.width: windowDelegate.isSelected ? 3 : windowDelegate.isMatched ? 2 : (windowDelegate.hovered ? 2 : 0) + border.color: (windowDelegate.isSelected || windowDelegate.isMatched || windowDelegate.hovered) ? (windowDelegate.isSelected ? Colors.tertiary : Styling.srItem("overprimary")) : "transparent" + border.width: windowDelegate.isSelected ? 3 : 2 visible: Config.performance.windowPreview && (windowDelegate.hovered || windowDelegate.dragging || windowDelegate.isMatched || windowDelegate.isSelected) z: 5 } From 5ac1ec2d259b822c97e5c3802b99d92e17b57ad8 Mon Sep 17 00:00:00 2001 From: enhulsman Date: Wed, 25 Feb 2026 16:07:21 +0100 Subject: [PATCH 3/3] feat(overview): show windows from all monitors Remove the monitor filter from both grid and scrolling overview layouts so windows from all monitors are visible in each screen's overview. Cross-monitor windows are scaled proportionally using uniform scaling to preserve aspect ratio within ScreencopyView letterboxing. Same- monitor windows get scale 1.0 (behavior identical to before). Changes: - Remove monitor filter from filteredWindowData (grid) and workspaceWindows (scrolling) - Add per-window crossScaleX/Y/Uniform and centering offsets - Look up each window's actual monitor for position calculation - Add solid background fill to prevent white bleed-through - Pass monitors list to ScrollingWorkspace delegate - Fix contentBounds to use crossScaleUniform for position --- modules/widgets/overview/Overview.qml | 26 +++++++-- modules/widgets/overview/OverviewWindow.qml | 15 +++-- .../widgets/overview/ScrollingOverview.qml | 1 + .../widgets/overview/ScrollingWorkspace.qml | 57 +++++++++++++++---- 4 files changed, 80 insertions(+), 19 deletions(-) diff --git a/modules/widgets/overview/Overview.qml b/modules/widgets/overview/Overview.qml index 98eae35a..d1989dcc 100644 --- a/modules/widgets/overview/Overview.qml +++ b/modules/widgets/overview/Overview.qml @@ -304,16 +304,15 @@ Item { implicitWidth: workspaceColumnLayout.implicitWidth implicitHeight: workspaceColumnLayout.implicitHeight - // Pre-filter windows for this monitor and workspace group + // Pre-filter windows for workspace group (all monitors) readonly property var filteredWindowData: { const minWs = overviewRoot.workspaceGroup * overviewRoot.workspacesShown; const maxWs = (overviewRoot.workspaceGroup + 1) * overviewRoot.workspacesShown; - const monId = overviewRoot.monitorId; const toplevels = ToplevelManager.toplevels.values; return overviewRoot.windowList.filter(win => { const wsId = win?.workspace?.id; - return wsId > minWs && wsId <= maxWs && win.monitor === monId; + return wsId > minWs && wsId <= maxWs; }).map(win => ({ windowData: win, toplevel: toplevels.find(t => `0x${t.HyprlandToplevel.address}` === win.address) || null @@ -329,9 +328,28 @@ Item { windowData: modelData.windowData toplevel: modelData.toplevel scale: overviewRoot.scale + crossScaleX: { + if (modelData.windowData.monitor === overviewRoot.monitorId) return 1.0; + const winMon = overviewRoot.monitors.find(m => m.id === modelData.windowData.monitor); + if (!winMon) return 1.0; + const ovW = (overviewRoot.monitorData?.width || 1920) / (overviewRoot.monitorData?.scale || 1.0); + const winW = (winMon.width || 1920) / (winMon.scale || 1.0); + return ovW / winW; + } + crossScaleY: { + if (modelData.windowData.monitor === overviewRoot.monitorId) return 1.0; + const winMon = overviewRoot.monitors.find(m => m.id === modelData.windowData.monitor); + if (!winMon) return 1.0; + const ovH = (overviewRoot.monitorData?.height || 1080) / (overviewRoot.monitorData?.scale || 1.0); + const winH = (winMon.height || 1080) / (winMon.scale || 1.0); + return ovH / winH; + } availableWorkspaceWidth: overviewRoot.workspaceImplicitWidth availableWorkspaceHeight: overviewRoot.workspaceImplicitHeight - monitorData: overviewRoot.monitorData + monitorData: { + const winMon = overviewRoot.monitors.find(m => m.id === modelData.windowData.monitor); + return winMon ?? overviewRoot.monitorData; + } barPosition: overviewRoot.barPosition barReserved: overviewRoot.barReserved diff --git a/modules/widgets/overview/OverviewWindow.qml b/modules/widgets/overview/OverviewWindow.qml index ca1c36e2..fa9473a0 100644 --- a/modules/widgets/overview/OverviewWindow.qml +++ b/modules/widgets/overview/OverviewWindow.qml @@ -29,6 +29,13 @@ Item { property string barPosition: "top" property int barReserved: 0 + // Cross-monitor scale correction (set by parent for windows on different monitors) + property real crossScaleX: 1.0 + property real crossScaleY: 1.0 + readonly property real crossScaleUniform: Math.min(crossScaleX, crossScaleY) + readonly property real crossCenterX: crossScaleX > 0 ? availableWorkspaceWidth * (1 - crossScaleUniform / crossScaleX) / 2 : 0 + readonly property real crossCenterY: crossScaleY > 0 ? availableWorkspaceHeight * (1 - crossScaleUniform / crossScaleY) / 2 : 0 + // Search highlighting property bool isSearchMatch: false property bool isSearchSelected: false @@ -46,7 +53,7 @@ Item { let base = (windowData?.at?.[0] || 0) - (monitorData?.x || 0); if (barPosition === "left") base -= barReserved; - return Math.round(Math.max(base * scale, 0) + xOffset); + return Math.round(Math.max(base * scale * crossScaleUniform, 0) + xOffset + crossCenterX); } readonly property real initY: { if (useOverridePosition && overrideY >= 0) @@ -54,10 +61,10 @@ Item { let base = (windowData?.at?.[1] || 0) - (monitorData?.y || 0); if (barPosition === "top") base -= barReserved; - return Math.round(Math.max(base * scale, 0) + yOffset); + return Math.round(Math.max(base * scale * crossScaleUniform, 0) + yOffset + crossCenterY); } - readonly property real targetWindowWidth: Math.round((windowData?.size[0] || 100) * scale) - readonly property real targetWindowHeight: Math.round((windowData?.size[1] || 100) * scale) + readonly property real targetWindowWidth: Math.round((windowData?.size[0] || 100) * scale * crossScaleUniform) + readonly property real targetWindowHeight: Math.round((windowData?.size[1] || 100) * scale * crossScaleUniform) readonly property bool compactMode: targetWindowHeight < 60 || targetWindowWidth < 60 readonly property string iconPath: AppSearch.guessIcon(windowData?.class || "") readonly property int calculatedRadius: Styling.radius(-2) diff --git a/modules/widgets/overview/ScrollingOverview.qml b/modules/widgets/overview/ScrollingOverview.qml index d4d6d779..e18b3622 100644 --- a/modules/widgets/overview/ScrollingOverview.qml +++ b/modules/widgets/overview/ScrollingOverview.qml @@ -274,6 +274,7 @@ Item { barPosition: scrollingOverviewRoot.barPosition barReserved: scrollingOverviewRoot.barReserved windowList: scrollingOverviewRoot.windowList + monitors: scrollingOverviewRoot.monitors isActive: scrollingOverviewRoot.trackedWorkspaceId === workspaceId activeBorderColor: scrollingOverviewRoot.activeBorderColor focusedWindowAddress: scrollingOverviewRoot.focusedWindowAddress diff --git a/modules/widgets/overview/ScrollingWorkspace.qml b/modules/widgets/overview/ScrollingWorkspace.qml index 8fec52d1..ae0d641c 100644 --- a/modules/widgets/overview/ScrollingWorkspace.qml +++ b/modules/widgets/overview/ScrollingWorkspace.qml @@ -33,6 +33,7 @@ Item { property int draggingTargetWorkspace: -1 property Item dragOverlay: null property Item overviewRoot: null + property var monitors: [] signal workspaceNavigated(int wsId) // Callbacks for search matching (set by parent) @@ -50,10 +51,10 @@ Item { readonly property real viewportWidth: workspaceWidth / 3 readonly property real viewportOffset: viewportWidth // Offset to center third - // Filter windows for this workspace and monitor + // Filter windows for this workspace (all monitors) readonly property var workspaceWindows: { return windowList.filter(win => { - return win?.workspace?.id === workspaceId && win.monitor === monitorId; + return win?.workspace?.id === workspaceId; }); } @@ -73,11 +74,16 @@ Item { for (const win of workspaceWindows) { // Calculate window position the same way as in the delegate - let baseX = (win?.at?.[0] || 0) - (monitorData?.x || 0); + const winMon = root.monitors.find(m => m.id === win?.monitor) ?? monitorData; + let baseX = (win?.at?.[0] || 0) - (winMon?.x || 0); if (barPosition === "left") baseX -= barReserved; - const scaledX = baseX * scale_; - const winWidth = (win?.size?.[0] || 100) * scale_; + const isCrossMon = win?.monitor !== root.monitorId; + const crossScX = isCrossMon ? ((monitorData?.width || 1920) / (monitorData?.scale || 1.0)) / ((winMon?.width || 1920) / (winMon?.scale || 1.0)) : 1.0; + const crossScY = isCrossMon ? ((monitorData?.height || 1080) / (monitorData?.scale || 1.0)) / ((winMon?.height || 1080) / (winMon?.scale || 1.0)) : 1.0; + const crossScU = Math.min(crossScX, crossScY); + const scaledX = baseX * scale_ * crossScU; + const winWidth = (win?.size?.[0] || 100) * scale_ * crossScU; minX = Math.min(minX, scaledX); maxX = Math.max(maxX, scaledX + winWidth); @@ -162,6 +168,13 @@ Item { anchors.fill: parent clip: true + // Solid fallback behind wallpaper (prevents white bleed-through for cross-monitor tiles) + Rectangle { + anchors.fill: parent + radius: Styling.radius(1) + color: Colors.background + } + // Wallpaper background TintedWallpaper { id: workspaceWallpaper @@ -291,25 +304,47 @@ Item { property real overrideBaseY: -1 property bool useOverridePosition: false + // Per-window monitor data for cross-monitor windows + readonly property var windowMonData: { + if (windowData?.monitor === root.monitorId) return root.monitorData; + const m = root.monitors.find(mon => mon.id === windowData?.monitor); + return m ?? root.monitorData; + } + readonly property real crossScaleX: { + if (windowData?.monitor === root.monitorId) return 1.0; + const ovW = (root.monitorData?.width || 1920) / (root.monitorData?.scale || 1.0); + const winW = (windowMonData?.width || 1920) / (windowMonData?.scale || 1.0); + return ovW / winW; + } + readonly property real crossScaleY: { + if (windowData?.monitor === root.monitorId) return 1.0; + const ovH = (root.monitorData?.height || 1080) / (root.monitorData?.scale || 1.0); + const winH = (windowMonData?.height || 1080) / (windowMonData?.scale || 1.0); + return ovH / winH; + } + readonly property real crossScaleUniform: Math.min(crossScaleX, crossScaleY) + readonly property real crossCenterX: crossScaleX > 0 ? ((root.monitorData?.width || 1920) / (root.monitorData?.scale || 1.0)) * root.scale_ * (1 - crossScaleUniform / crossScaleX) / 2 : 0 + readonly property real crossCenterY: crossScaleY > 0 ? ((root.monitorData?.height || 1080) / (root.monitorData?.scale || 1.0)) * root.scale_ * (1 - crossScaleUniform / crossScaleY) / 2 : 0 + // Position calculations relative to center viewport readonly property real baseX: { if (useOverridePosition && overrideBaseX >= 0) return overrideBaseX; - let base = (windowData?.at?.[0] || 0) - (monitorData?.x || 0); + let base = (windowData?.at?.[0] || 0) - (windowMonData?.x || 0); if (barPosition === "left") base -= barReserved; - return (base * scale_) + root.viewportOffset + root.horizontalScrollOffset; + return (base * scale_ * crossScaleUniform) + root.viewportOffset + root.horizontalScrollOffset + crossCenterX; } readonly property real baseY: { if (useOverridePosition && overrideBaseY >= 0) return overrideBaseY; - let base = (windowData?.at?.[1] || 0) - (monitorData?.y || 0); + let base = (windowData?.at?.[1] || 0) - (windowMonData?.y || 0); if (barPosition === "top") base -= barReserved; - return Math.max(base * scale_, 0); + return Math.max(base * scale_ * crossScaleUniform, 0) + crossCenterY; } - readonly property real targetWidth: Math.round((windowData?.size[0] || 100) * scale_) - readonly property real targetHeight: Math.round((windowData?.size[1] || 100) * scale_) + readonly property real targetWidth: Math.round((windowData?.size[0] || 100) * scale_ * crossScaleUniform) + readonly property real targetHeight: Math.round((windowData?.size[1] || 100) * scale_ * crossScaleUniform) readonly property bool compactMode: targetHeight < 60 || targetWidth < 60 readonly property string iconPath: AppSearch.guessIcon(windowData?.class || "") readonly property int calculatedRadius: Styling.radius(-2)