From a0bad0507f1d96aeb75f767da294885c950fdd5c Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Fri, 1 May 2026 10:26:46 +0200 Subject: [PATCH] Add virtual desktop pager to convergence dock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show compact numbered desktop buttons in the dock bar when two or more virtual desktops exist. The buttons are split symmetrically to the left and right of the centred app-icon cluster, matching Plasma Desktop pager behaviour. Click a button to switch desktops. Drag a running-task icon from the dock and release it over a desktop button to move that window to the target desktop. Right-click a task icon for a "Move to …" submenu when the pager is visible. The feature is a no-op on single-desktop sessions. --- .../homescreens/folio/qml/FavouritesBar.qml | 187 +++++++++++++++++- 1 file changed, 185 insertions(+), 2 deletions(-) diff --git a/containments/homescreens/folio/qml/FavouritesBar.qml b/containments/homescreens/folio/qml/FavouritesBar.qml index 7fb04007..458103f2 100644 --- a/containments/homescreens/folio/qml/FavouritesBar.qml +++ b/containments/homescreens/folio/qml/FavouritesBar.qml @@ -86,6 +86,38 @@ MouseArea { property string taskPinStorageId: "" readonly property bool taskPinCanDrop: taskPinTargetIndex !== -1 && taskPinStorageId !== "" + // Virtual desktop pager (convergence mode, 2+ desktops) + readonly property bool showPager: convergenceMode && virtualDesktopInfo.numberOfDesktops > 1 + readonly property real pagerButtonWidth: showPager ? Math.min(root.height, Kirigami.Units.gridUnit * 2.5) : 0 + readonly property int pagerLeftCount: showPager ? Math.ceil(virtualDesktopInfo.numberOfDesktops / 2) : 0 + readonly property int pagerRightCount: showPager ? virtualDesktopInfo.numberOfDesktops - pagerLeftCount : 0 + + function pagerDesktopName(index) { + let names = virtualDesktopInfo.desktopNames + if (names && index < names.length && String(names[index]).length > 0) + return String(names[index]) + return i18n("Desktop %1", index + 1) + } + + // Returns the desktop ID of the pager button under screen-space x, or "" + function pagerButtonDesktopAt(x) { + if (!showPager) return "" + let ids = virtualDesktopInfo.desktopIds + for (let i = 0; i < pagerLeftCount; ++i) { + let bx = navButtonWidth + i * pagerButtonWidth + if (x >= bx && x < bx + pagerButtonWidth) + return (ids && i < ids.length) ? String(ids[i]) : "" + } + for (let i = 0; i < pagerRightCount; ++i) { + let bx = root.width - navButtonWidth - (pagerRightCount - i) * pagerButtonWidth + if (x >= bx && x < bx + pagerButtonWidth) { + let di = pagerLeftCount + i + return (ids && di < ids.length) ? String(ids[di]) : "" + } + } + return "" + } + function runningTaskStorageId(taskModel) { var id = taskModel ? taskModel.AppId || "" : "" if (id && !id.endsWith(".desktop")) @@ -256,6 +288,131 @@ MouseArea { } } + // ---- Virtual desktop pager: left wing (desktops 1 .. ceil(N/2)) ---- + Repeater { + id: leftPagerRepeater + model: root.pagerLeftCount + + delegate: Item { + id: leftDesktopBtn + required property int index + + readonly property string desktopId: { + let ids = virtualDesktopInfo.desktopIds + return (ids && index < ids.length) ? String(ids[index]) : "" + } + readonly property bool isCurrent: desktopId !== "" && String(desktopId) === String(virtualDesktopInfo.currentDesktop) + readonly property bool isDragTarget: { + if (root.taskPinDragIndex < 0) return false + let cx = root.taskBaseX(root.taskPinDragIndex) + root.dockCellWidth / 2 + root.taskPinDragOffset + return root.pagerButtonDesktopAt(cx) === desktopId + } + + x: root.navButtonWidth + index * root.pagerButtonWidth + y: 0 + width: root.pagerButtonWidth + height: root.height + + Rectangle { + anchors.fill: parent + anchors.margins: root.dockItemInset + radius: Kirigami.Units.cornerRadius + color: leftDesktopBtn.isCurrent || leftDesktopBtn.isDragTarget + ? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, + leftPagerHover.containsMouse || leftDesktopBtn.isDragTarget ? 0.25 : 0.18) + : root.dockItemColor(leftPagerHover.containsPress, leftPagerHover.containsMouse, false) + Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } } + } + + PC3.Label { + anchors.centerIn: parent + text: (leftDesktopBtn.index + 1).toString() + color: leftDesktopBtn.isCurrent ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor + font.pixelSize: Math.round(parent.height * 0.3) + font.bold: leftDesktopBtn.isCurrent + } + + PC3.ToolTip { + visible: leftPagerHover.containsMouse + text: root.pagerDesktopName(leftDesktopBtn.index) + } + + MouseArea { + id: leftPagerHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (leftDesktopBtn.desktopId) + root.folio.activateVirtualDesktop(leftDesktopBtn.desktopId) + } + } + } + } + + // ---- Virtual desktop pager: right wing (desktops ceil(N/2)+1 .. N) ---- + Repeater { + id: rightPagerRepeater + model: root.pagerRightCount + + delegate: Item { + id: rightDesktopBtn + required property int index + + readonly property int desktopIndex: root.pagerLeftCount + index + readonly property string desktopId: { + let ids = virtualDesktopInfo.desktopIds + return (ids && desktopIndex < ids.length) ? String(ids[desktopIndex]) : "" + } + readonly property bool isCurrent: desktopId !== "" && String(desktopId) === String(virtualDesktopInfo.currentDesktop) + readonly property bool isDragTarget: { + if (root.taskPinDragIndex < 0) return false + let cx = root.taskBaseX(root.taskPinDragIndex) + root.dockCellWidth / 2 + root.taskPinDragOffset + return root.pagerButtonDesktopAt(cx) === desktopId + } + + x: root.width - root.navButtonWidth - (root.pagerRightCount - index) * root.pagerButtonWidth + y: 0 + width: root.pagerButtonWidth + height: root.height + + Rectangle { + anchors.fill: parent + anchors.margins: root.dockItemInset + radius: Kirigami.Units.cornerRadius + color: rightDesktopBtn.isCurrent || rightDesktopBtn.isDragTarget + ? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, + rightPagerHover.containsMouse || rightDesktopBtn.isDragTarget ? 0.25 : 0.18) + : root.dockItemColor(rightPagerHover.containsPress, rightPagerHover.containsMouse, false) + Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } } + } + + PC3.Label { + anchors.centerIn: parent + text: (rightDesktopBtn.desktopIndex + 1).toString() + color: rightDesktopBtn.isCurrent ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor + font.pixelSize: Math.round(parent.height * 0.3) + font.bold: rightDesktopBtn.isCurrent + } + + PC3.ToolTip { + visible: rightPagerHover.containsMouse + text: root.pagerDesktopName(rightDesktopBtn.desktopIndex) + } + + MouseArea { + id: rightPagerHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (rightDesktopBtn.desktopId) + root.folio.activateVirtualDesktop(rightDesktopBtn.desktopId) + } + } + } + } + TaskManager.VirtualDesktopInfo { id: virtualDesktopInfo } @@ -1037,7 +1194,9 @@ MouseArea { target: null xAxis.enabled: true yAxis.enabled: false - enabled: root.convergenceMode && taskDelegate.isLocationBottom && !folio.FolioSettings.lockLayout && taskDelegate.taskStorageId !== "" && !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId) + // Enable for unpinned tasks (pin-to-dock drag) and for ALL tasks + // when the pager is showing so windows can be dragged to a desktop button. + enabled: root.convergenceMode && taskDelegate.isLocationBottom && !folio.FolioSettings.lockLayout && taskDelegate.taskStorageId !== "" && (root.showPager || !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId)) onActiveChanged: { if (active) { @@ -1050,7 +1209,12 @@ MouseArea { root.taskPinTargetIndex = -1 root.taskPinStorageId = taskDelegate.taskStorageId } else if (root.taskPinDragIndex === taskDelegate.index) { - if (root.taskPinCanDrop) { + // If released over a pager button, move the window to that desktop. + let finalCenterX = root.taskBaseX(taskDelegate.index) + root.dockCellWidth / 2 + root.taskPinDragOffset + let pagerDesktop = root.pagerButtonDesktopAt(finalCenterX) + if (pagerDesktop && taskDelegate.model.IsVirtualDesktopsChangeable === true) { + tasksModel.requestVirtualDesktops(tasksModel.makeModelIndex(taskDelegate.index), [pagerDesktop]) + } else if (root.taskPinCanDrop && !folio.FavouritesModel.containsApplication(root.taskPinStorageId)) { folio.FavouritesModel.addApplicationAt(root.taskPinTargetIndex, root.taskPinStorageId) } root.clearTaskPinDrag() @@ -1172,6 +1336,25 @@ MouseArea { } onClicked: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index)) } + + Controls.MenuSeparator { + visible: root.showPager && taskDelegate.model.IsVirtualDesktopsChangeable === true + } + + Instantiator { + model: root.showPager && taskDelegate.model.IsVirtualDesktopsChangeable === true + ? virtualDesktopInfo.desktopIds : [] + delegate: PC3.MenuItem { + required property int index + required property var modelData + text: i18n("Move to %1", root.pagerDesktopName(index)) + enabled: String(modelData) !== String(virtualDesktopInfo.currentDesktop) + onTriggered: tasksModel.requestVirtualDesktops( + tasksModel.makeModelIndex(taskDelegate.index), [modelData]) + } + onObjectAdded: (idx, obj) => taskContextMenu.insertItem(taskContextMenu.count, obj) + onObjectRemoved: (idx, obj) => taskContextMenu.removeItem(obj) + } } } }