From 220be94b63e03ce48e21bf817932f3a983694743 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Mon, 13 Apr 2026 12:32:54 +0200 Subject: [PATCH] Allow dragging running dock tasks to pin them Running task icons in the convergence dock can now be dragged into the favourites section to pin the app at the drop position. Add indexed insertion to FavouritesModel so pinning can place the new favourite at a specific slot instead of always appending. Show a placeholder while dragging and reuse the existing addApplication path for the final pin action. --- .../homescreens/folio/favouritesmodel.cpp | 7 +- .../homescreens/folio/favouritesmodel.h | 1 + .../homescreens/folio/qml/FavouritesBar.qml | 128 ++++++++++++++++-- 3 files changed, 124 insertions(+), 12 deletions(-) diff --git a/containments/homescreens/folio/favouritesmodel.cpp b/containments/homescreens/folio/favouritesmodel.cpp index 7c3f8bc2..e813e0c0 100644 --- a/containments/homescreens/folio/favouritesmodel.cpp +++ b/containments/homescreens/folio/favouritesmodel.cpp @@ -78,6 +78,11 @@ void FavouritesModel::removeEntry(int row) } bool FavouritesModel::addApplication(const QString &storageId) +{ + return addApplicationAt(m_delegates.size(), storageId); +} + +bool FavouritesModel::addApplicationAt(int row, const QString &storageId) { if (containsApplication(storageId)) { return false; @@ -90,7 +95,7 @@ bool FavouritesModel::addApplication(const QString &storageId) auto app = std::make_shared(service); auto delegate = std::make_shared(app); - return addEntry(m_delegates.size(), delegate); + return addEntry(row, delegate); } bool FavouritesModel::containsApplication(const QString &storageId) const diff --git a/containments/homescreens/folio/favouritesmodel.h b/containments/homescreens/folio/favouritesmodel.h index fde7db51..6eea8309 100644 --- a/containments/homescreens/folio/favouritesmodel.h +++ b/containments/homescreens/folio/favouritesmodel.h @@ -43,6 +43,7 @@ public: Q_INVOKABLE void removeEntry(int row); Q_INVOKABLE bool addApplication(const QString &storageId); + Q_INVOKABLE bool addApplicationAt(int row, const QString &storageId); Q_INVOKABLE bool containsApplication(const QString &storageId) const; Q_INVOKABLE void moveEntry(int fromRow, int toRow); bool canAddEntry(int row, std::shared_ptr delegate); diff --git a/containments/homescreens/folio/qml/FavouritesBar.qml b/containments/homescreens/folio/qml/FavouritesBar.qml index 160a4eb7..aa4b407a 100644 --- a/containments/homescreens/folio/qml/FavouritesBar.qml +++ b/containments/homescreens/folio/qml/FavouritesBar.qml @@ -62,6 +62,65 @@ MouseArea { return Math.max(0, Math.min(repeater.count - 1, dragReorderIndex + shift)) } + // Drag-to-pin state for running tasks in convergence mode. + property int taskPinDragIndex: -1 + property real taskPinDragOffset: 0 + property int taskPinTargetIndex: -1 + property string taskPinStorageId: "" + readonly property bool taskPinCanDrop: taskPinTargetIndex !== -1 && taskPinStorageId !== "" + + function runningTaskStorageId(taskModel) { + var id = taskModel ? taskModel.AppId || "" : "" + if (id && !id.endsWith(".desktop")) + id += ".desktop" + return id + } + + function favouriteBaseX(index) { + return index * root.dockCellWidth - (root.totalItemCount / 2) * root.dockCellWidth + root.dockCenterX - root.spacerWidth / 2 + } + + function taskBaseX(index) { + return (repeater.count + index) * root.dockCellWidth - (root.totalItemCount / 2) * root.dockCellWidth + root.dockCenterX + root.spacerWidth / 2 + } + + function clearTaskPinDrag() { + root.taskPinDragIndex = -1 + root.taskPinDragOffset = 0 + root.taskPinTargetIndex = -1 + root.taskPinStorageId = "" + } + + function updateTaskPinTarget() { + if (root.taskPinDragIndex === -1 || root.taskPinStorageId === "" || folio.FolioSettings.lockLayout || folio.FavouritesModel.containsApplication(root.taskPinStorageId)) { + root.taskPinTargetIndex = -1 + return + } + + var draggedCenterX = root.taskBaseX(root.taskPinDragIndex) + root.dockCellWidth / 2 + root.taskPinDragOffset + var firstTaskCenterX = root.taskBaseX(0) + root.dockCellWidth / 2 + + if (draggedCenterX >= firstTaskCenterX) { + root.taskPinTargetIndex = -1 + return + } + + if (repeater.count === 0) { + root.taskPinTargetIndex = 0 + return + } + + for (let index = 0; index < repeater.count; ++index) { + let favouriteCenterX = root.favouriteBaseX(index) + root.dockCellWidth / 2 + if (draggedCenterX < favouriteCenterX) { + root.taskPinTargetIndex = index + return + } + } + + root.taskPinTargetIndex = repeater.count + } + // Home button (convergence mode, left end) Rectangle { id: homeButton @@ -226,7 +285,9 @@ MouseArea { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad } } - x: (isLocationBottom ? centerPosition + root.dockCenterX - root.spacerWidth / 2 : (parent.width - width) / 2) + dragVisualShift + property real taskPinVisualShift: root.taskPinCanDrop && delegate.index >= root.taskPinTargetIndex ? root.dockCellWidth : 0 + + x: (isLocationBottom ? root.favouriteBaseX(delegate.index) : (parent.width - width) / 2) + dragVisualShift + taskPinVisualShift y: isLocationBottom ? (parent.height - height) / 2 : parent.height / 2 - centerPosition - root.dockCellHeight implicitWidth: root.dockCellWidth @@ -743,6 +804,17 @@ MouseArea { opacity: 0.4 } + PlaceholderDelegate { + id: taskPinPlaceholder + visible: root.taskPinCanDrop + folio: root.folio + width: root.dockCellWidth + height: root.dockCellHeight + x: root.favouriteBaseX(root.taskPinTargetIndex) + y: (parent.height - height) / 2 + z: 1 + } + Repeater { id: taskRepeater model: root.convergenceMode ? tasksModel : null @@ -754,6 +826,7 @@ MouseArea { required property var model readonly property bool isLocationBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom + readonly property string taskStorageId: root.runningTaskStorageId(taskDelegate.model) // Position after all favourites property double fromCenterValue: (repeater.count + taskDelegate.index) - (root.totalItemCount / 2) @@ -763,8 +836,9 @@ MouseArea { readonly property int centerPosition: (isLocationBottom ? root.dockCellWidth : root.dockCellHeight) * fromCenterValue - x: isLocationBottom ? centerPosition + root.dockCenterX + root.spacerWidth / 2 : (parent.width - width) / 2 + x: isLocationBottom ? root.taskBaseX(taskDelegate.index) + (root.taskPinDragIndex === taskDelegate.index ? root.taskPinDragOffset : 0) : (parent.width - width) / 2 y: isLocationBottom ? (parent.height - height) / 2 : parent.height / 2 - centerPosition - root.dockCellHeight + z: root.taskPinDragIndex === taskDelegate.index ? 2 : 0 implicitWidth: root.dockCellWidth implicitHeight: root.dockCellHeight @@ -789,6 +863,45 @@ MouseArea { active: taskMouseArea.containsMouse } + DragHandler { + id: taskDragHandler + target: null + xAxis.enabled: true + yAxis.enabled: false + enabled: root.convergenceMode && taskDelegate.isLocationBottom && !folio.FolioSettings.lockLayout && taskDelegate.taskStorageId !== "" && !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId) + + onActiveChanged: { + if (active) { + thumbnailPopup.close() + thumbnailShowTimer.stop() + thumbnailHideTimer.stop() + root.hoveredTaskIndex = -1 + root.taskPinDragIndex = taskDelegate.index + root.taskPinDragOffset = 0 + root.taskPinTargetIndex = -1 + root.taskPinStorageId = taskDelegate.taskStorageId + } else if (root.taskPinDragIndex === taskDelegate.index) { + if (root.taskPinCanDrop) { + folio.FavouritesModel.addApplicationAt(root.taskPinTargetIndex, root.taskPinStorageId) + } + root.clearTaskPinDrag() + } + } + + onTranslationChanged: { + if (root.taskPinDragIndex === taskDelegate.index) { + root.taskPinDragOffset = translation.x + root.updateTaskPinTarget() + } + } + + onCanceled: { + if (root.taskPinDragIndex === taskDelegate.index) { + root.clearTaskPinDrag() + } + } + } + // Window indicator dots (one per sibling window of the same app) Row { anchors.bottom: parent.bottom @@ -873,20 +986,13 @@ MouseArea { id: taskContextMenu popupType: T.Popup.Window - property string taskStorageId: { - var id = taskDelegate.model.AppId || "" - if (id && !id.endsWith(".desktop")) - id += ".desktop" - return id - } - PC3.MenuItem { icon.name: "window-pin" text: i18n("Pin to Dock") // repeater.count dependency forces re-evaluation when favourites change - visible: taskContextMenu.taskStorageId !== "" && repeater.count >= 0 && !folio.FavouritesModel.containsApplication(taskContextMenu.taskStorageId) + visible: taskDelegate.taskStorageId !== "" && repeater.count >= 0 && !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId) enabled: !folio.FolioSettings.lockLayout - onClicked: folio.FavouritesModel.addApplication(taskContextMenu.taskStorageId) + onClicked: folio.FavouritesModel.addApplication(taskDelegate.taskStorageId) } PC3.MenuItem { icon.name: taskDelegate.model.IsMinimized ? "window-restore" : "window-minimize"