diff --git a/Components/CustomTooltip.qml b/Components/CustomTooltip.qml new file mode 100644 index 0000000..2211849 --- /dev/null +++ b/Components/CustomTooltip.qml @@ -0,0 +1,23 @@ +import QtQuick +import QtQuick.Controls +import qs.Components + +ToolTip { + id: root + property bool extraVisibleCondition: true + property bool alternativeVisibleCondition: false + readonly property bool internalVisibleCondition: (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition + verticalPadding: 5 + horizontalPadding: 10 + background: null + + visible: internalVisibleCondition + + contentItem: CustomTooltipContent { + id: contentItem + text: root.text + shown: root.internalVisibleCondition + horizontalPadding: root.horizontalPadding + verticalPadding: root.verticalPadding + } +} diff --git a/Components/CustomTooltipContent.qml b/Components/CustomTooltipContent.qml new file mode 100644 index 0000000..6a399cb --- /dev/null +++ b/Components/CustomTooltipContent.qml @@ -0,0 +1,48 @@ +import QtQuick +import qs.Components +import qs.Modules +import qs.Config + +Item { + id: root + required property string text + property bool shown: false + property real horizontalPadding: 10 + property real verticalPadding: 5 + implicitWidth: tooltipTextObject.implicitWidth + 2 * root.horizontalPadding + implicitHeight: tooltipTextObject.implicitHeight + 2 * root.verticalPadding + + property bool isVisible: backgroundRectangle.implicitHeight > 0 + + Rectangle { + id: backgroundRectangle + anchors { + bottom: root.bottom + horizontalCenter: root.horizontalCenter + } + color: DynamicColors.tPalette.m3inverseSurface ?? "#3C4043" + radius: 8 + opacity: shown ? 1 : 0 + implicitWidth: shown ? (tooltipTextObject.implicitWidth + 2 * root.horizontalPadding) : 0 + implicitHeight: shown ? (tooltipTextObject.implicitHeight + 2 * root.verticalPadding) : 0 + clip: true + + Behavior on implicitWidth { + Anim {} + } + Behavior on implicitHeight { + Anim {} + } + Behavior on opacity { + Anim {} + } + + CustomText { + id: tooltipTextObject + anchors.centerIn: parent + text: root.text + color: DynamicColors.palette.m3inverseOnSurface ?? "#FFFFFF" + wrapMode: Text.Wrap + } + } +} diff --git a/Config/Config.qml b/Config/Config.qml index cca5987..862e10b 100644 --- a/Config/Config.qml +++ b/Config/Config.qml @@ -24,6 +24,7 @@ Singleton { property alias animScale: adapter.animScale property alias lock: adapter.lock property alias idle: adapter.idle + property alias overview: adapter.overview FileView { id: root @@ -56,6 +57,7 @@ Singleton { property real animScale: 1.0 property LockConf lock: LockConf {} property IdleTimeout idle: IdleTimeout {} + property Overview overview: Overview {} } } } diff --git a/Config/Overview.qml b/Config/Overview.qml new file mode 100644 index 0000000..6f81758 --- /dev/null +++ b/Config/Overview.qml @@ -0,0 +1,8 @@ +import Quickshell.Io + +JsonObject { + property int rows: 2 + property int columns: 5 + property real scale: 0.16 + property bool enable: false +} diff --git a/Effects/ShadowRect.qml b/Effects/ShadowRect.qml index 883de43..92889c4 100644 --- a/Effects/ShadowRect.qml +++ b/Effects/ShadowRect.qml @@ -4,6 +4,7 @@ import QtQuick.Effects Item { id: root property real radius + Rectangle { id: shadowRect anchors.fill: root diff --git a/Modules/Network/NetworkPopout.qml b/Modules/Network/NetworkPopout.qml new file mode 100644 index 0000000..76ea531 --- /dev/null +++ b/Modules/Network/NetworkPopout.qml @@ -0,0 +1,9 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import QtQuick.Layouts +import qs.Modules + +ColumnLayout { +} diff --git a/Modules/WSOverview/ColorUtils.qml b/Modules/WSOverview/ColorUtils.qml new file mode 100644 index 0000000..6162df1 --- /dev/null +++ b/Modules/WSOverview/ColorUtils.qml @@ -0,0 +1,68 @@ +pragma Singleton +import Quickshell + +Singleton { + id: root + + function colorWithHueOf(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + var hue = c2.hsvHue; + var sat = c1.hsvSaturation; + var val = c1.hsvValue; + var alpha = c1.a; + return Qt.hsva(hue, sat, val, alpha); + } + + function colorWithSaturationOf(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + var hue = c1.hsvHue; + var sat = c2.hsvSaturation; + var val = c1.hsvValue; + var alpha = c1.a; + return Qt.hsva(hue, sat, val, alpha); + } + + function colorWithLightness(color, lightness) { + var c = Qt.color(color); + return Qt.hsla(c.hslHue, c.hslSaturation, lightness, c.a); + } + + function colorWithLightnessOf(color1, color2) { + var c2 = Qt.color(color2); + return colorWithLightness(color1, c2.hslLightness); + } + + function adaptToAccent(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + var hue = c2.hslHue; + var sat = c2.hslSaturation; + var light = c1.hslLightness; + var alpha = c1.a; + return Qt.hsla(hue, sat, light, alpha); + } + + function mix(color1, color2, percentage = 0.5) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + return Qt.rgba( + percentage * c1.r + (1 - percentage) * c2.r, + percentage * c1.g + (1 - percentage) * c2.g, + percentage * c1.b + (1 - percentage) * c2.b, + percentage * c1.a + (1 - percentage) * c2.a + ); + } + + function transparentize(color, percentage = 1) { + var c = Qt.color(color); + return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage)); + } + + function applyAlpha(color, alpha) { + var c = Qt.color(color); + var a = Math.max(0, Math.min(1, alpha)); + return Qt.rgba(c.r, c.g, c.b, a); + } +} diff --git a/Modules/WSOverview/HyprlandData.qml b/Modules/WSOverview/HyprlandData.qml new file mode 100644 index 0000000..371bf54 --- /dev/null +++ b/Modules/WSOverview/HyprlandData.qml @@ -0,0 +1,134 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland + +Singleton { + id: root + property var windowList: [] + property var addresses: [] + property var windowByAddress: ({}) + property var workspaces: [] + property var workspaceIds: [] + property var workspaceById: ({}) + property var activeWorkspace: null + property var monitors: [] + property var layers: ({}) + + function updateWindowList() { + getClients.running = true; + } + + function updateLayers() { + getLayers.running = true; + } + + function updateMonitors() { + getMonitors.running = true; + } + + function updateWorkspaces() { + getWorkspaces.running = true; + getActiveWorkspace.running = true; + } + + function updateAll() { + updateWindowList(); + updateMonitors(); + updateLayers(); + updateWorkspaces(); + } + + function biggestWindowForWorkspace(workspaceId) { + const windowsInThisWorkspace = HyprlandData.windowList.filter(w => w.workspace.id == workspaceId); + return windowsInThisWorkspace.reduce((maxWin, win) => { + const maxArea = (maxWin?.size?.[0] ?? 0) * (maxWin?.size?.[1] ?? 0); + const winArea = (win?.size?.[0] ?? 0) * (win?.size?.[1] ?? 0); + return winArea > maxArea ? win : maxWin; + }, null); + } + + Component.onCompleted: { + updateAll(); + } + + Connections { + target: Hyprland + + function onRawEvent(event) { + updateAll() + } + } + + Process { + id: getClients + command: ["hyprctl", "clients", "-j"] + stdout: StdioCollector { + id: clientsCollector + onStreamFinished: { + root.windowList = JSON.parse(clientsCollector.text) + let tempWinByAddress = {}; + for (var i = 0; i < root.windowList.length; ++i) { + var win = root.windowList[i]; + tempWinByAddress[win.address] = win; + } + root.windowByAddress = tempWinByAddress; + root.addresses = root.windowList.map(win => win.address); + } + } + } + + Process { + id: getMonitors + command: ["hyprctl", "monitors", "-j"] + stdout: StdioCollector { + id: monitorsCollector + onStreamFinished: { + root.monitors = JSON.parse(monitorsCollector.text); + } + } + } + + Process { + id: getLayers + command: ["hyprctl", "layers", "-j"] + stdout: StdioCollector { + id: layersCollector + onStreamFinished: { + root.layers = JSON.parse(layersCollector.text); + } + } + } + + Process { + id: getWorkspaces + command: ["hyprctl", "workspaces", "-j"] + stdout: StdioCollector { + id: workspacesCollector + onStreamFinished: { + root.workspaces = JSON.parse(workspacesCollector.text); + let tempWorkspaceById = {}; + for (var i = 0; i < root.workspaces.length; ++i) { + var ws = root.workspaces[i]; + tempWorkspaceById[ws.id] = ws; + } + root.workspaceById = tempWorkspaceById; + root.workspaceIds = root.workspaces.map(ws => ws.id); + } + } + } + + Process { + id: getActiveWorkspace + command: ["hyprctl", "activeworkspace", "-j"] + stdout: StdioCollector { + id: activeWorkspaceCollector + onStreamFinished: { + root.activeWorkspace = JSON.parse(activeWorkspaceCollector.text); + } + } + } +} diff --git a/Modules/WSOverview/Overview.qml b/Modules/WSOverview/Overview.qml new file mode 100644 index 0000000..803763f --- /dev/null +++ b/Modules/WSOverview/Overview.qml @@ -0,0 +1,147 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland +import qs.Config + +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) + screen: modelData + visible: false + + WlrLayershell.namespace: "quickshell:overview" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + color: "transparent" + + mask: Region { + item: keyHandler + } + + anchors { + top: true + bottom: true + left: true + right: true + } + + HyprlandFocusGrab { + id: grab + windows: [root] + property bool canBeActive: root.monitorIsFocused + active: false + onCleared: () => { + if (!active) + root.visible = false; + } + } + + implicitWidth: columnLayout.implicitWidth + implicitHeight: columnLayout.implicitHeight + + Item { + id: keyHandler + anchors.fill: parent + visible: root.visible + focus: root.visible + + Keys.onPressed: event => { + // close: Escape or Enter + if (event.key === Qt.Key_Escape || event.key === Qt.Key_Return) { + root.visible = false; + event.accepted = true; + return; + } + + // Helper: compute current group bounds + const workspacesPerGroup = Config.overview.rows * Config.overview.columns; + const currentId = Hyprland.focusedMonitor?.activeWorkspace?.id ?? 1; + const currentGroup = Math.floor((currentId - 1) / workspacesPerGroup); + const minWorkspaceId = currentGroup * workspacesPerGroup + 1; + const maxWorkspaceId = minWorkspaceId + workspacesPerGroup - 1; + + let targetId = null; + + // Arrow keys and vim-style hjkl + if (event.key === Qt.Key_Left || event.key === Qt.Key_H) { + targetId = currentId - 1; + if (targetId < minWorkspaceId) targetId = maxWorkspaceId; + } else if (event.key === Qt.Key_Right || event.key === Qt.Key_L) { + targetId = currentId + 1; + if (targetId > maxWorkspaceId) targetId = minWorkspaceId; + } else if (event.key === Qt.Key_Up || event.key === Qt.Key_K) { + targetId = currentId - Config.overview.columns; + if (targetId < minWorkspaceId) targetId += workspacesPerGroup; + } else if (event.key === Qt.Key_Down || event.key === Qt.Key_J) { + targetId = currentId + Config.overview.columns; + if (targetId > maxWorkspaceId) targetId -= workspacesPerGroup; + } + + // 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) { + Hyprland.dispatch("workspace " + targetId); + event.accepted = true; + } + } + } + + ColumnLayout { + id: columnLayout + visible: root.visible + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + topMargin: 100 + } + + Loader { + id: overviewLoader + active: true && (Config.overview.enable ?? true) + sourceComponent: OverviewWidget { + panelWindow: root + visible: true + } + } + } + IpcHandler { + target: "overview" + + function toggle() { + root.visible = !root.visible; + } + function close() { + root.visible = false; + } + function open() { + root.visible = true; + } + } + } + } +} diff --git a/Modules/WSOverview/OverviewWidget.qml b/Modules/WSOverview/OverviewWidget.qml new file mode 100644 index 0000000..899768a --- /dev/null +++ b/Modules/WSOverview/OverviewWidget.qml @@ -0,0 +1,298 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import qs.Config +import qs.Modules +import qs.Components +import qs.Effects + +Item { + id: root + required property var panelWindow + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen) + readonly property var toplevels: ToplevelManager.toplevels + readonly property int workspacesShown: Config.overview.rows * Config.overview.columns + readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.name == monitor.name) + property var windows: HyprlandData.windowList + property var windowByAddress: HyprlandData.windowByAddress + property var windowAddresses: HyprlandData.addresses + property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor?.id) + property real scale: Config.overview.scale + property color activeBorderColor: Appearance.colors.colSecondary + + property real workspaceImplicitWidth: (monitorData?.transform % 2 === 1) ? + ((monitor.height / monitor.scale - (monitorData?.reserved?.[0] ?? 0) - (monitorData?.reserved?.[2] ?? 0)) * root.scale) : + ((monitor.width / monitor.scale - (monitorData?.reserved?.[0] ?? 0) - (monitorData?.reserved?.[2] ?? 0)) * root.scale) + property real workspaceImplicitHeight: (monitorData?.transform % 2 === 1) ? + ((monitor.width / monitor.scale - (monitorData?.reserved?.[1] ?? 0) - (monitorData?.reserved?.[3] ?? 0)) * root.scale) : + ((monitor.height / monitor.scale - (monitorData?.reserved?.[1] ?? 0) - (monitorData?.reserved?.[3] ?? 0)) * root.scale) + + property real workspaceNumberMargin: 80 + property real workspaceNumberSize: 250 * monitor.scale + property int workspaceZ: 0 + property int windowZ: 1 + property int windowDraggingZ: 99999 + property real workspaceSpacing: 5 + + property int draggingFromWorkspace: -1 + property int draggingTargetWorkspace: -1 + + implicitWidth: overviewBackground.implicitWidth + Appearance.sizes.elevationMargin * 2 + implicitHeight: overviewBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 + + property Component windowComponent: OverviewWindow {} + property list windowWidgets: [] + + ShadowRect { + anchors.fill: overviewBackground + } + + Rectangle { // Background + id: overviewBackground + property real padding: 10 + anchors.fill: parent + anchors.margins: Appearance.sizes.elevationMargin + + implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2 + implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2 + radius: Appearance.rounding.screenRounding * root.scale + padding + color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.colors.colLayer0Border + + ColumnLayout { // Workspaces + id: workspaceColumnLayout + + z: root.workspaceZ + anchors.centerIn: parent + spacing: workspaceSpacing + Repeater { + model: Config.overview.rows + delegate: RowLayout { + id: row + property int rowIndex: index + spacing: workspaceSpacing + + Repeater { // Workspace repeater + model: Config.overview.columns + Rectangle { // Workspace + id: workspace + property int colIndex: index + property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * Config.overview.columns + colIndex + 1 + property color defaultWorkspaceColor: DynamicColors.tPalette.m3surfaceContainerLow + property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1) + property color hoveredBorderColor: DynamicColors.tPalette.m3surfaceContainer + property bool hoveredWhileDragging: false + + implicitWidth: root.workspaceImplicitWidth + implicitHeight: root.workspaceImplicitHeight + color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor + radius: 8 + border.width: 2 + border.color: hoveredWhileDragging ? hoveredBorderColor : "transparent" + + CustomText { + anchors.centerIn: parent + text: workspaceValue + color: DynamicColor.tPalette.m3onSurfaceVariant + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + MouseArea { + id: workspaceArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: { + if (root.draggingTargetWorkspace === -1) { + root.panelWindow.visible = false + Hyprland.dispatch(`workspace ${workspaceValue}`) + } + } + } + + DropArea { + anchors.fill: parent + onEntered: { + root.draggingTargetWorkspace = workspaceValue + if (root.draggingFromWorkspace == root.draggingTargetWorkspace) return; + hoveredWhileDragging = true + } + onExited: { + hoveredWhileDragging = false + if (root.draggingTargetWorkspace == workspaceValue) root.draggingTargetWorkspace = -1 + } + } + } + } + } + } + } + + Item { // Windows & focused workspace indicator + id: windowSpace + anchors.centerIn: parent + implicitWidth: workspaceColumnLayout.implicitWidth + implicitHeight: workspaceColumnLayout.implicitHeight + + Repeater { // Window repeater + model: ScriptModel { + values: { + return ToplevelManager.toplevels.values.filter((toplevel) => { + const address = `0x${toplevel.HyprlandToplevel.address}` + var win = windowByAddress[address] + const inWorkspaceGroup = (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown) + return inWorkspaceGroup; + }).sort((a, b) => { + // Proper stacking order based on Hyprland's window properties + const addrA = `0x${a.HyprlandToplevel.address}` + const addrB = `0x${b.HyprlandToplevel.address}` + const winA = windowByAddress[addrA] + const winB = windowByAddress[addrB] + + // 1. Pinned windows are always on top + if (winA?.pinned !== winB?.pinned) { + return winA?.pinned ? 1 : -1 + } + + // 2. Floating windows above tiled windows + if (winA?.floating !== winB?.floating) { + return winA?.floating ? 1 : -1 + } + + // 3. Within same category, sort by focus history + // Lower focusHistoryID = more recently focused = higher in stack + return (winB?.focusHistoryID ?? 0) - (winA?.focusHistoryID ?? 0) + }) + } + } + delegate: OverviewWindow { + id: window + required property var modelData + required property int index + property var overviewRoot: root.panelWindow + property int monitorId: windowData?.monitor + property var monitor: HyprlandData.monitors.find(m => m.id === monitorId) + property var address: `0x${modelData.HyprlandToplevel.address}` + windowData: windowByAddress[address] + toplevel: modelData + monitorData: monitor + + // Calculate scale relative to window's source monitor + property real sourceMonitorWidth: (monitor?.transform % 2 === 1) ? + (monitor?.height ?? 1920) / (monitor?.scale ?? 1) - (monitor?.reserved?.[0] ?? 0) - (monitor?.reserved?.[2] ?? 0) : + (monitor?.width ?? 1920) / (monitor?.scale ?? 1) - (monitor?.reserved?.[0] ?? 0) - (monitor?.reserved?.[2] ?? 0) + property real sourceMonitorHeight: (monitor?.transform % 2 === 1) ? + (monitor?.width ?? 1080) / (monitor?.scale ?? 1) - (monitor?.reserved?.[1] ?? 0) - (monitor?.reserved?.[3] ?? 0) : + (monitor?.height ?? 1080) / (monitor?.scale ?? 1) - (monitor?.reserved?.[1] ?? 0) - (monitor?.reserved?.[3] ?? 0) + + // Scale windows to fit the workspace size, accounting for different monitor sizes + scale: Math.min( + root.workspaceImplicitWidth / sourceMonitorWidth, + root.workspaceImplicitHeight / sourceMonitorHeight + ) + + availableWorkspaceWidth: root.workspaceImplicitWidth + availableWorkspaceHeight: root.workspaceImplicitHeight + widgetMonitorId: root.monitor.id + + property bool atInitPosition: (initX == x && initY == y) + + property int workspaceColIndex: (windowData?.workspace.id - 1) % Config.overview.columns + property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / Config.overview.columns) + xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex + yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex + + Timer { + id: updateWindowPosition + interval: Config.options.hacks.arbitraryRaceConditionDelay + repeat: false + running: false + onTriggered: { + window.x = Math.round(Math.max((windowData?.at[0] - (monitor?.x ?? 0) - (monitorData?.reserved?.[0] ?? 0)) * root.scale, 0) + xOffset) + window.y = Math.round(Math.max((windowData?.at[1] - (monitor?.y ?? 0) - (monitorData?.reserved?.[1] ?? 0)) * root.scale, 0) + yOffset) + } + } + + z: atInitPosition ? (root.windowZ + index) : root.windowDraggingZ + Drag.hotSpot.x: targetWindowWidth / 2 + Drag.hotSpot.y: targetWindowHeight / 2 + MouseArea { + id: dragArea + anchors.fill: parent + hoverEnabled: true + onEntered: hovered = true + onExited: hovered = false + acceptedButtons: Qt.LeftButton | Qt.MiddleButton + drag.target: parent + onPressed: (mouse) => { + root.draggingFromWorkspace = windowData?.workspace.id + window.pressed = true + window.Drag.active = true + window.Drag.source = window + window.Drag.hotSpot.x = mouse.x + window.Drag.hotSpot.y = mouse.y + } + onReleased: { + const targetWorkspace = root.draggingTargetWorkspace + window.pressed = false + window.Drag.active = false + root.draggingFromWorkspace = -1 + if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) { + Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${window.windowData?.address}`) + updateWindowPosition.restart() + } + else { + window.x = window.initX + window.y = window.initY + } + } + onClicked: (event) => { + if (!windowData) return; + + if (event.button === Qt.LeftButton) { + GlobalStates.overviewOpen = false + Hyprland.dispatch(`focuswindow address:${windowData.address}`) + event.accepted = true + } else if (event.button === Qt.MiddleButton) { + Hyprland.dispatch(`closewindow address:${windowData.address}`) + event.accepted = true + } + } + + CustomTooltip { + extraVisibleCondition: false + alternativeVisibleCondition: dragArea.containsMouse && !window.Drag.active + text: `${windowData?.title ?? "Unknown"}\n[${windowData?.class ?? "unknown"}] ${windowData?.xwayland ? "[XWayland] " : ""}` + } + } + } + } + + Rectangle { + id: focusedWorkspaceIndicator + property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown) + property int activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / Config.overview.columns) + property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % Config.overview.columns + x: (root.workspaceImplicitWidth + workspaceSpacing) * activeWorkspaceColIndex + y: (root.workspaceImplicitHeight + workspaceSpacing) * activeWorkspaceRowIndex + z: root.windowZ + width: root.workspaceImplicitWidth + height: root.workspaceImplicitHeight + color: "transparent" + radius: Appearance.rounding.screenRounding * root.scale + border.width: 2 + border.color: root.activeBorderColor + Behavior on x { + Anim {} + } + Behavior on y { + Anim {} + } + } + } + } +} diff --git a/Modules/WSOverview/OverviewWindow.qml b/Modules/WSOverview/OverviewWindow.qml new file mode 100644 index 0000000..00f9cc7 --- /dev/null +++ b/Modules/WSOverview/OverviewWindow.qml @@ -0,0 +1,103 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Config +import qs.Modules + +Item { // Window + id: root + + required property var overviewRoot + + property var toplevel + property var windowData + property var monitorData + property var scale + property var availableWorkspaceWidth + property var availableWorkspaceHeight + property bool restrictToWorkspace: true + property real initX: Math.max(((windowData?.at[0] ?? 0) - (monitorData?.x ?? 0) - (monitorData?.reserved?.[0] ?? 0)) * root.scale, 0) + xOffset + property real initY: Math.max(((windowData?.at[1] ?? 0) - (monitorData?.y ?? 0) - (monitorData?.reserved?.[1] ?? 0)) * root.scale, 0) + yOffset + property real xOffset: 0 + property real yOffset: 0 + property int widgetMonitorId: 0 + + property var targetWindowWidth: (windowData?.size[0] ?? 100) * scale + property var targetWindowHeight: (windowData?.size[1] ?? 100) * scale + property bool hovered: false + property bool pressed: false + + property var iconToWindowRatio: 0.25 + property var xwaylandIndicatorToIconRatio: 0.35 + property var iconToWindowRatioCompact: 0.45 + property var entry: DesktopEntries.heuristicLookup(windowData?.class) + property var iconPath: Quickshell.iconPath(entry?.icon ?? windowData?.class ?? "application-x-executable", "image-missing") + property bool compactMode: false + + property bool indicateXWayland: windowData?.xwayland ?? false + + x: initX + y: initY + width: Math.min((windowData?.size[0] ?? 100) * root.scale, availableWorkspaceWidth) + height: Math.min((windowData?.size[1] ?? 100) * root.scale, availableWorkspaceHeight) + opacity: (windowData?.monitor ?? -1) == widgetMonitorId ? 1 : 0.4 + + clip: true + + Behavior on x { + Anim {} + } + Behavior on y { + Anim {} + } + Behavior on width { + Anim {} + } + Behavior on height { + Anim {} + } + + ScreencopyView { + id: windowPreview + anchors.fill: parent + captureSource: root.overviewRoot.visible ? root.toplevel : null + live: true + + Rectangle { + anchors.fill: parent + radius: 8 + color: pressed ? ColorUtils.transparentize(Appearance.colors.colLayer2Active, 0.5) : + hovered ? ColorUtils.transparentize(Appearance.colors.colLayer2Hover, 0.7) : + ColorUtils.transparentize(Appearance.colors.colLayer2) + border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.7) + border.width : 1 + } + + ColumnLayout { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + spacing: 8 + + Image { + id: windowIcon + property var iconSize: { + return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1); + } + Layout.alignment: Qt.AlignHCenter + source: root.iconPath + width: iconSize + height: iconSize + sourceSize: Qt.size(iconSize, iconSize) + + Behavior on width { + Anim {} + } + Behavior on height { + Anim {} + } + } + } + } +} diff --git a/shell.qml b/shell.qml index 6b28ebf..7da699d 100644 --- a/shell.qml +++ b/shell.qml @@ -3,6 +3,7 @@ import Quickshell import qs.Modules import qs.Modules.Lock +import qs.Modules.WSOverview import qs.Helpers Scope { @@ -21,4 +22,6 @@ Scope { NotificationCenter { } + + Overview {} }