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.
This commit is contained in:
Marco Allegretti 2026-04-13 12:32:54 +02:00
parent 0cbd933e71
commit 220be94b63
3 changed files with 124 additions and 12 deletions

View file

@ -78,6 +78,11 @@ void FavouritesModel::removeEntry(int row)
} }
bool FavouritesModel::addApplication(const QString &storageId) bool FavouritesModel::addApplication(const QString &storageId)
{
return addApplicationAt(m_delegates.size(), storageId);
}
bool FavouritesModel::addApplicationAt(int row, const QString &storageId)
{ {
if (containsApplication(storageId)) { if (containsApplication(storageId)) {
return false; return false;
@ -90,7 +95,7 @@ bool FavouritesModel::addApplication(const QString &storageId)
auto app = std::make_shared<FolioApplication>(service); auto app = std::make_shared<FolioApplication>(service);
auto delegate = std::make_shared<FolioDelegate>(app); auto delegate = std::make_shared<FolioDelegate>(app);
return addEntry(m_delegates.size(), delegate); return addEntry(row, delegate);
} }
bool FavouritesModel::containsApplication(const QString &storageId) const bool FavouritesModel::containsApplication(const QString &storageId) const

View file

@ -43,6 +43,7 @@ public:
Q_INVOKABLE void removeEntry(int row); Q_INVOKABLE void removeEntry(int row);
Q_INVOKABLE bool addApplication(const QString &storageId); 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 bool containsApplication(const QString &storageId) const;
Q_INVOKABLE void moveEntry(int fromRow, int toRow); Q_INVOKABLE void moveEntry(int fromRow, int toRow);
bool canAddEntry(int row, std::shared_ptr<FolioDelegate> delegate); bool canAddEntry(int row, std::shared_ptr<FolioDelegate> delegate);

View file

@ -62,6 +62,65 @@ MouseArea {
return Math.max(0, Math.min(repeater.count - 1, dragReorderIndex + shift)) 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) // Home button (convergence mode, left end)
Rectangle { Rectangle {
id: homeButton id: homeButton
@ -226,7 +285,9 @@ MouseArea {
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad } 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 y: isLocationBottom ? (parent.height - height) / 2 : parent.height / 2 - centerPosition - root.dockCellHeight
implicitWidth: root.dockCellWidth implicitWidth: root.dockCellWidth
@ -743,6 +804,17 @@ MouseArea {
opacity: 0.4 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 { Repeater {
id: taskRepeater id: taskRepeater
model: root.convergenceMode ? tasksModel : null model: root.convergenceMode ? tasksModel : null
@ -754,6 +826,7 @@ MouseArea {
required property var model required property var model
readonly property bool isLocationBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom readonly property bool isLocationBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
readonly property string taskStorageId: root.runningTaskStorageId(taskDelegate.model)
// Position after all favourites // Position after all favourites
property double fromCenterValue: (repeater.count + taskDelegate.index) - (root.totalItemCount / 2) 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 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 y: isLocationBottom ? (parent.height - height) / 2 : parent.height / 2 - centerPosition - root.dockCellHeight
z: root.taskPinDragIndex === taskDelegate.index ? 2 : 0
implicitWidth: root.dockCellWidth implicitWidth: root.dockCellWidth
implicitHeight: root.dockCellHeight implicitHeight: root.dockCellHeight
@ -789,6 +863,45 @@ MouseArea {
active: taskMouseArea.containsMouse 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) // Window indicator dots (one per sibling window of the same app)
Row { Row {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@ -873,20 +986,13 @@ MouseArea {
id: taskContextMenu id: taskContextMenu
popupType: T.Popup.Window popupType: T.Popup.Window
property string taskStorageId: {
var id = taskDelegate.model.AppId || ""
if (id && !id.endsWith(".desktop"))
id += ".desktop"
return id
}
PC3.MenuItem { PC3.MenuItem {
icon.name: "window-pin" icon.name: "window-pin"
text: i18n("Pin to Dock") text: i18n("Pin to Dock")
// repeater.count dependency forces re-evaluation when favourites change // 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 enabled: !folio.FolioSettings.lockLayout
onClicked: folio.FavouritesModel.addApplication(taskContextMenu.taskStorageId) onClicked: folio.FavouritesModel.addApplication(taskDelegate.taskStorageId)
} }
PC3.MenuItem { PC3.MenuItem {
icon.name: taskDelegate.model.IsMinimized ? "window-restore" : "window-minimize" icon.name: taskDelegate.model.IsMinimized ? "window-restore" : "window-minimize"