From af4ecc55ce965c8d70b10c11e35fe435ce5307c8 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 26 Apr 2025 21:24:08 +0200 Subject: [PATCH] overview: drag and drop to move windows --- .../modules/overview/OverviewWidget.qml | 166 ++++++++++++++---- .../modules/overview/OverviewWindow.qml | 41 ++--- .../modules/overview/SearchWidget.qml | 1 + .../quickshell/modules/session/Session.qml | 2 +- .../modules/sidebarRight/todo/TodoWidget.qml | 1 + .config/quickshell/services/HyprlandData.qml | 5 +- 6 files changed, 150 insertions(+), 66 deletions(-) diff --git a/.config/quickshell/modules/overview/OverviewWidget.qml b/.config/quickshell/modules/overview/OverviewWidget.qml index f586af2e..b20f4eea 100644 --- a/.config/quickshell/modules/overview/OverviewWidget.qml +++ b/.config/quickshell/modules/overview/OverviewWidget.qml @@ -24,8 +24,22 @@ Item { property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor.id) property real scale: ConfigOptions.overview.scale + property real workspaceImplicitWidth: (monitor.width - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale + property real workspaceImplicitHeight: (monitor.height - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale + property real workspaceNumberMargin: 80 property real workspaceNumberSize: 80 + property int workspaceZ: 0 + property int windowZ: 1 + property int windowDraggingZ: 99999 + property real workspaceSpacing: 5 + + property int draggingFromWorkspace: -1 + property int draggingTargetWorkspace: -1 + + onDraggingFromWorkspaceChanged: { + console.log("draggingTargetWorkspace", draggingFromWorkspace) + } implicitWidth: overviewBackground.implicitWidth + Appearance.sizes.elevationMargin * 2 implicitHeight: overviewBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 @@ -43,21 +57,23 @@ Item { anchors.fill: parent - implicitWidth: columnLayout.implicitWidth + 5 * 2 - implicitHeight: columnLayout.implicitHeight + 5 * 2 + implicitWidth: workspaceColumnLayout.implicitWidth + 5 * 2 + implicitHeight: workspaceColumnLayout.implicitHeight + 5 * 2 color: Appearance.colors.colLayer0 radius: Appearance.rounding.screenRounding * root.scale + 5 * 2 ColumnLayout { - id: columnLayout - anchors.centerIn: parent - spacing: 5 + id: workspaceColumnLayout + z: root.workspaceZ + anchors.centerIn: parent + spacing: workspaceSpacing Repeater { model: ConfigOptions.overview.numOfRows delegate: RowLayout { id: row property int rowIndex: index + spacing: workspaceSpacing Repeater { // Workspace repeater model: ConfigOptions.overview.numOfCols @@ -65,44 +81,127 @@ Item { id: workspace property int colIndex: index property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * ConfigOptions.overview.numOfCols + colIndex + 1 + property color defaultColor: Appearance.colors.colLayer1 // TODO: reconsider this color for a cleaner look - implicitWidth: (monitor.width - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale - implicitHeight: (monitor.height - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale - color: Appearance.colors.colLayer1 // TODO: reconsider this color for a cleaner look + implicitWidth: root.workspaceImplicitWidth + implicitHeight: root.workspaceImplicitHeight + color: defaultColor radius: Appearance.rounding.screenRounding * root.scale + border.width: 2 + border.color: "transparent" MouseArea { - id: mouseArea + id: workspaceArea anchors.fill: parent - onClicked: (event) => { - closeOverview.running = true - Hyprland.dispatch(`workspace ${workspace.workspaceValue}`) + acceptedButtons: Qt.LeftButton + onClicked: { + if (root.draggingTargetWorkspace === -1) { + closeOverview.running = true + Hyprland.dispatch(`workspace ${workspaceValue}`) + } } } - StyledText { - z: 9999 - anchors.left: parent.left - anchors.top: parent.top - anchors.leftMargin: root.workspaceNumberMargin * root.scale - anchors.topMargin: root.workspaceNumberMargin * root.scale - font.pixelSize: root.workspaceNumberSize * root.scale - color: Appearance.colors.colSubtext - text: workspaceValue + DropArea { + anchors.fill: parent + onEntered: { + root.draggingTargetWorkspace = workspaceValue + if (root.draggingFromWorkspace == root.draggingTargetWorkspace) return; + border.color = Appearance.colors.colLayer2Hover + workspace.color = Appearance.mix(defaultColor, Appearance.colors.colLayer1Hover, 0.1) + } + onExited: { + border.color = "transparent" + workspace.color = defaultColor + if (root.draggingTargetWorkspace == workspaceValue) root.draggingTargetWorkspace = -1 + } } - Repeater { // Window repeater - model: windowAddresses.filter((address) => { - var win = windowByAddress[address] - return (win?.workspace?.id === workspace.workspaceValue) - }) - delegate: OverviewWindow { - windowData: windowByAddress[modelData] - monitorData: root.monitorData - scale: root.scale - availableWorkspaceWidth: workspace.implicitWidth - availableWorkspaceHeight: workspace.implicitHeight - } + } + } + } + } + } + + Item { + id: windowSpace + anchors.centerIn: parent + implicitWidth: workspaceColumnLayout.implicitWidth + implicitHeight: workspaceColumnLayout.implicitHeight + + Repeater { // Window repeater + model: windowAddresses.filter((address) => { + var win = windowByAddress[address] + return (root.workspaceGroup * root.workspacesShown < win.workspace.id && win.workspace.id <= (root.workspaceGroup + 1) * root.workspacesShown) + }) + delegate: OverviewWindow { + id: window + windowData: windowByAddress[modelData] + monitorData: root.monitorData + scale: root.scale + availableWorkspaceWidth: root.workspaceImplicitWidth + availableWorkspaceHeight: root.workspaceImplicitHeight + + property bool atInitPosition: (initX == x && initY == y) + restrictToWorkspace: Drag.active || atInitPosition + + property int workspaceColIndex: (windowData?.workspace.id - 1) % ConfigOptions.overview.numOfCols + property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / ConfigOptions.overview.numOfCols) + xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex + yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex + + Timer { + id: updateWindowPosition + interval: ConfigOptions.hacks.arbitraryRaceConditionDelay + repeat: false + running: false + onTriggered: { + window.x = Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset + window.y = Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset + } + } + + z: atInitPosition ? root.windowZ : 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: { + root.draggingFromWorkspace = windowData?.workspace.id + window.pressed = true + window.Drag.active = true + window.Drag.source = window + } + 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) { + closeOverview.running = true + Hyprland.dispatch(`workspace ${windowData.workspace.id}`) + event.accepted = true + } else if (event.button === Qt.MiddleButton) { + Hyprland.dispatch(`closewindow address:${windowData.address}`) + event.accepted = true } } } @@ -112,6 +211,7 @@ Item { } DropShadow { + z: -9999 anchors.fill: overviewBackground horizontalOffset: 0 verticalOffset: 2 diff --git a/.config/quickshell/modules/overview/OverviewWindow.qml b/.config/quickshell/modules/overview/OverviewWindow.qml index f492ee15..13343342 100644 --- a/.config/quickshell/modules/overview/OverviewWindow.qml +++ b/.config/quickshell/modules/overview/OverviewWindow.qml @@ -18,6 +18,11 @@ Rectangle { // Window property var scale property var availableWorkspaceWidth property var availableWorkspaceHeight + property bool restrictToWorkspace: true + property real initX: Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset + property real initY: Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset + property real xOffset: 0 + property real yOffset: 0 property var targetWindowWidth: windowData?.size[0] * scale property var targetWindowHeight: windowData?.size[1] * scale @@ -29,12 +34,11 @@ Rectangle { // Window property var iconToWindowRatioCompact: 0.6 property var iconPath: Quickshell.iconPath(Icons.noKnowledgeIconGuess(windowData?.class)) property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth - - z: 1 - x: Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) - y: Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) - width: Math.min(windowData?.size[0] * root.scale, availableWorkspaceWidth - x) - height: Math.min(windowData?.size[1] * root.scale, availableWorkspaceHeight - y) + + x: initX + y: initY + width: Math.min(windowData?.size[0] * root.scale, (restrictToWorkspace ? windowData?.size[0] : availableWorkspaceWidth - x + xOffset)) + height: Math.min(windowData?.size[1] * root.scale, (restrictToWorkspace ? windowData?.size[1] : availableWorkspaceHeight - y + yOffset)) radius: Appearance.rounding.windowRounding * root.scale color: pressed ? Appearance.colors.colLayer2Active : hovered ? Appearance.colors.colLayer2Hover : Appearance.colors.colLayer2 @@ -72,29 +76,6 @@ Rectangle { // Window command: ["bash", "-c", "qs ipc call overview close &"] // Somehow has to by async to work? } - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - onEntered: root.hovered = true - onExited: root.hovered = false - onPressed: root.pressed = true - onReleased: root.pressed = false - acceptedButtons: Qt.LeftButton | Qt.MiddleButton - onClicked: (event) => { - if (!windowData) return; - - if (event.button === Qt.LeftButton) { - closeOverview.running = true - Hyprland.dispatch(`workspace ${windowData.workspace.id}`) - event.accepted = true - } else if (event.button === Qt.MiddleButton) { - Hyprland.dispatch(`closewindow address:${windowData.address}`) - event.accepted = true - } - } - } - ColumnLayout { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left @@ -116,7 +97,7 @@ Rectangle { // Window IconImage { id: xwaylandIndicator - visible: ConfigOptions.overview.showXwaylandIndicator && windowData?.xwayland + visible: (ConfigOptions.overview.showXwaylandIndicator && windowData?.xwayland) ?? false anchors.right: parent.right anchors.bottom: parent.bottom source: Quickshell.iconPath("xorg") diff --git a/.config/quickshell/modules/overview/SearchWidget.qml b/.config/quickshell/modules/overview/SearchWidget.qml index 73a2d96d..ab694f6f 100644 --- a/.config/quickshell/modules/overview/SearchWidget.qml +++ b/.config/quickshell/modules/overview/SearchWidget.qml @@ -215,6 +215,7 @@ Item { // Wrapper focus: root.panelWindow.visible || GlobalStates.overviewOpen Layout.rightMargin: 15 padding: 15 + renderType: Text.NativeRendering color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant selectedTextColor: Appearance.m3colors.m3onPrimary selectionColor: Appearance.m3colors.m3primary diff --git a/.config/quickshell/modules/session/Session.qml b/.config/quickshell/modules/session/Session.qml index a2be293d..947f44b1 100644 --- a/.config/quickshell/modules/session/Session.qml +++ b/.config/quickshell/modules/session/Session.qml @@ -233,7 +233,7 @@ Scope { } Process { id: logout - command: ["bash", "-c", "loginctl terminate-session $XDG_SESSION_ID"] + command: ["bash", "-c", "pkill Hyprland"] // loginctl terminate-session hangs SDDM } Process { id: hibernate diff --git a/.config/quickshell/modules/sidebarRight/todo/TodoWidget.qml b/.config/quickshell/modules/sidebarRight/todo/TodoWidget.qml index 7db2e1c5..6878754e 100644 --- a/.config/quickshell/modules/sidebarRight/todo/TodoWidget.qml +++ b/.config/quickshell/modules/sidebarRight/todo/TodoWidget.qml @@ -273,6 +273,7 @@ Item { Layout.rightMargin: 16 padding: 10 color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant + renderType: Text.NativeRendering selectedTextColor: Appearance.m3colors.m3onPrimary selectionColor: Appearance.m3colors.m3primary placeholderText: qsTr("Task description") diff --git a/.config/quickshell/services/HyprlandData.qml b/.config/quickshell/services/HyprlandData.qml index bc129465..5ca4cf6f 100644 --- a/.config/quickshell/services/HyprlandData.qml +++ b/.config/quickshell/services/HyprlandData.qml @@ -43,11 +43,12 @@ Singleton { stdout: SplitParser { onRead: (data) => { root.windowList = JSON.parse(data) - root.windowByAddress = {} + let tempWinByAddress = {} for (var i = 0; i < root.windowList.length; ++i) { var win = root.windowList[i] - root.windowByAddress[win.address] = win + tempWinByAddress[win.address] = win } + root.windowByAddress = tempWinByAddress root.addresses = root.windowList.map((win) => win.address) } }