import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Hyprland import "../../common" import "../../services" import "." Scope { id: overviewScope Variants { id: overviewVariants model: Quickshell.screens PanelWindow { id: root required property var modelData readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) property bool blurEnabled: Config.options.overview.effects.enableBlur property bool backdropEnabled: Config.options.overview.effects.enableBackdrop property real backdropOpacity: Math.max(0, Math.min(1, Config.options.overview.effects.backdropOpacity)) property bool closeOnFocusLoss: Config.options.overview.closeOnFocusLoss ?? true screen: modelData visible: GlobalStates.overviewOpen WlrLayershell.namespace: blurEnabled ? "quickshell:overview-blur" : "quickshell:overview" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive color: "transparent" anchors { top: true bottom: true left: true right: true } HyprlandFocusGrab { id: grab windows: [root] property bool canBeActive: root.monitorIsFocused active: false onCleared: () => { // Only the monitor that owns the grab may close the overview if (root.closeOnFocusLoss && !active && canBeActive) GlobalStates.overviewOpen = false; } } Connections { target: GlobalStates function onOverviewOpenChanged() { if (GlobalStates.overviewOpen) { delayedGrabTimer.start(); } } } // Re-evaluate grab ownership when focused monitor changes Connections { target: Hyprland function onFocusedMonitorChanged() { if (!GlobalStates.overviewOpen) return; // Transfer the grab to the newly focused monitor if (root.monitorIsFocused && !grab.active) { grab.active = true; } else if (!root.monitorIsFocused && grab.active) { grab.active = false; } } } Timer { id: delayedGrabTimer interval: Config.options.hacks.arbitraryRaceConditionDelay repeat: false onTriggered: { if (!grab.canBeActive) return; grab.active = GlobalStates.overviewOpen; } } // Keep the layershell surface full-screen so backdrop/blur are not constrained by content size. implicitWidth: screen.width implicitHeight: screen.height Item { id: keyHandler anchors.fill: parent visible: GlobalStates.overviewOpen focus: GlobalStates.overviewOpen z: 0 Rectangle { id: backdropLayer anchors.fill: parent visible: root.backdropEnabled color: "#000000" opacity: root.backdropOpacity z: 0 } MouseArea { id: outsideClickCatcher anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton enabled: root.closeOnFocusLoss && GlobalStates.overviewOpen z: 0 onPressed: mouse => { GlobalStates.overviewOpen = false; mouse.accepted = true; } } Keys.onPressed: event => { // close: Escape or Enter if (event.key === Qt.Key_Escape || event.key === Qt.Key_Return) { GlobalStates.overviewOpen = false; event.accepted = true; return; } // Helper: compute current group bounds const workspacesPerGroup = Config.options.overview.rows * Config.options.overview.columns; const currentId = Hyprland.focusedMonitor?.activeWorkspace?.id ?? 1; const useWorkspaceMap = Config.options.overview.useWorkspaceMap; const workspaceMap = Config.options.overview.workspaceMap ?? []; const focusedMonitorId = Hyprland.focusedMonitor?.id ?? root.monitor?.id ?? 0; const workspaceOffset = useWorkspaceMap ? Number(workspaceMap[focusedMonitorId] ?? 0) : 0; const currentGroup = Math.floor((currentId - workspaceOffset - 1) / workspacesPerGroup); const minWorkspaceId = currentGroup * workspacesPerGroup + 1 + workspaceOffset; const maxWorkspaceId = minWorkspaceId + workspacesPerGroup - 1; const rows = Config.options.overview.rows; const columns = Config.options.overview.columns; const reverseColumns = Config.options.overview.orderRightLeft; const reverseRows = Config.options.overview.orderBottomUp; const clampedIndex = Math.max(0, Math.min(workspacesPerGroup - 1, currentId - minWorkspaceId)); const currentNormalRow = Math.floor(clampedIndex / columns); const currentNormalColumn = clampedIndex % columns; function toVisualRow(normalRow) { return reverseRows ? (rows - normalRow - 1) : normalRow; } function toVisualColumn(normalColumn) { return reverseColumns ? (columns - normalColumn - 1) : normalColumn; } function toNormalRow(visualRow) { return reverseRows ? (rows - visualRow - 1) : visualRow; } function toNormalColumn(visualColumn) { return reverseColumns ? (columns - visualColumn - 1) : visualColumn; } let targetVisualRow = toVisualRow(currentNormalRow); let targetVisualColumn = toVisualColumn(currentNormalColumn); let targetId = null; // Arrow keys and vim-style hjkl if (event.key === Qt.Key_Left || event.key === Qt.Key_H) { targetVisualColumn = (targetVisualColumn - 1 + columns) % columns; } else if (event.key === Qt.Key_Right || event.key === Qt.Key_L) { targetVisualColumn = (targetVisualColumn + 1) % columns; } else if (event.key === Qt.Key_Up || event.key === Qt.Key_K) { targetVisualRow = (targetVisualRow - 1 + rows) % rows; } else if (event.key === Qt.Key_Down || event.key === Qt.Key_J) { targetVisualRow = (targetVisualRow + 1) % rows; } // Number keys: jump to workspace within the current group // 1-9 map to positions 1-9, 0 maps to position 10 else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) { const position = event.key - Qt.Key_0; // 1-9 if (position <= workspacesPerGroup) { targetId = minWorkspaceId + position - 1; } } else if (event.key === Qt.Key_0) { // 0 = 10th workspace in the group (if group has 10+ workspaces) if (workspacesPerGroup >= 10) { targetId = minWorkspaceId + 9; // 10th position = offset 9 } } if (targetId === null && ( event.key === Qt.Key_Left || event.key === Qt.Key_H || event.key === Qt.Key_Right || event.key === Qt.Key_L || event.key === Qt.Key_Up || event.key === Qt.Key_K || event.key === Qt.Key_Down || event.key === Qt.Key_J )) { const targetNormalRow = toNormalRow(targetVisualRow); const targetNormalColumn = toNormalColumn(targetVisualColumn); targetId = minWorkspaceId + targetNormalRow * columns + targetNormalColumn; } if (targetId !== null) { const clampedTarget = Math.max(minWorkspaceId, Math.min(maxWorkspaceId, targetId)); if (Hyprland.usingLua) { Hyprland.dispatch(`hl.dsp.focus({workspace = '${clampedTarget}'})`); } else { Hyprland.dispatch("workspace " + clampedTarget); } event.accepted = true; } } } ColumnLayout { id: columnLayout visible: GlobalStates.overviewOpen z: 1 anchors { horizontalCenter: parent.horizontalCenter top: parent.top topMargin: Config.options.position.topMargin } Loader { id: overviewLoader active: Config?.options.overview.enable ?? true sourceComponent: OverviewWidget { panelWindow: root visible: true } } } } } IpcHandler { target: "overview" function toggle() { GlobalStates.overviewOpen = !GlobalStates.overviewOpen; } function close() { GlobalStates.overviewOpen = false; } function open() { GlobalStates.overviewOpen = true; } } }