Add virtual desktop pager to convergence dock

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.
This commit is contained in:
Marco Allegretti 2026-05-01 10:26:46 +02:00
parent 160bc97621
commit a0bad0507f

View file

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