overview: drag and drop to move windows

This commit is contained in:
end-4 2025-04-26 21:24:08 +02:00
parent 6f6b3876fb
commit af4ecc55ce
6 changed files with 150 additions and 66 deletions

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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)
}
}