shift-shell/containments/homescreens/folio/qml/FavouritesBar.qml
Marco Allegretti 9bccbeede6 Add drag-reorder for dock favourites
Click-and-drag reordering of favourites bar items in convergence
mode.  The existing touch-based press-and-hold drag is kept for
mobile; in convergence mode, press-and-hold opens the context
menu only.

DelegateTouchArea owns the exclusive mouse grab at the C++ level,
so drag detection (threshold crossing + delta signals) is added
there rather than using a QML DragHandler.  Displaced items
animate into their new positions while the dragged item follows
the cursor.

Expose FavouritesModel::moveEntry as Q_INVOKABLE so QML can
persist the reorder.
2026-04-12 14:15:13 +02:00

889 lines
36 KiB
QML

// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Layouts 1.1
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.taskmanager as TaskManager
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.kirigami as Kirigami
import QtQuick.Controls as Controls
import QtQuick.Templates as T
import "./private"
import "./delegate"
MouseArea {
id: root
property Folio.HomeScreen folio
property MobileShell.MaskManager maskManager
property var homeScreen
signal delegateDragRequested(var item)
// Convergence mode: show running apps alongside favourites
readonly property bool convergenceMode: ShellSettings.Settings.convergenceModeEnabled
readonly property int totalItemCount: repeater.count + (convergenceMode ? taskRepeater.count : 0)
// In convergence mode, size icons to fit the dock bar instead of using page grid cells
readonly property real dockCellWidth: convergenceMode ? root.height : folio.HomeScreenState.pageCellWidth
readonly property real dockCellHeight: convergenceMode ? root.height : folio.HomeScreenState.pageCellHeight
// Navigation buttons width (used to offset center positioning)
readonly property real navButtonWidth: convergenceMode ? root.height : 0
// Center x for dock items (offset between nav buttons in convergence mode)
readonly property real dockCenterX: convergenceMode
? navButtonWidth + (root.width - 2 * navButtonWidth) / 2
: root.width / 2
// Thumbnail popup hover tracking
property int hoveredTaskIndex: -1
// Drag-reorder state (convergence mode only)
property int dragReorderIndex: -1
property real dragReorderOffset: 0
readonly property int dragTargetIndex: {
if (dragReorderIndex === -1) return -1
let shift = Math.round(dragReorderOffset / dockCellWidth)
return Math.max(0, Math.min(repeater.count - 1, dragReorderIndex + shift))
}
// Home button (convergence mode, left end)
Rectangle {
id: homeButton
visible: root.convergenceMode
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: root.navButtonWidth
color: homeMouseArea.containsPress
? Qt.rgba(255, 255, 255, 0.2)
: (homeMouseArea.containsMouse ? Qt.rgba(255, 255, 255, 0.1) : "transparent")
radius: Kirigami.Units.cornerRadius
Kirigami.Icon {
anchors.centerIn: parent
width: Math.min(parent.width, parent.height) * 0.75
height: width
source: "start-here-kde"
active: homeMouseArea.containsMouse
}
MouseArea {
id: homeMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: MobileShellState.ShellDBusClient.openHomeScreen()
}
}
// Overview button (convergence mode, right end)
Rectangle {
id: overviewButton
visible: root.convergenceMode
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
width: root.navButtonWidth
color: overviewMouseArea.containsPress
? Qt.rgba(255, 255, 255, 0.2)
: (overviewMouseArea.containsMouse ? Qt.rgba(255, 255, 255, 0.1) : "transparent")
radius: Kirigami.Units.cornerRadius
Kirigami.Icon {
anchors.centerIn: parent
width: Math.min(parent.width, parent.height) * 0.75
height: width
source: "view-grid-symbolic"
active: overviewMouseArea.containsMouse
}
MouseArea {
id: overviewMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.folio.triggerOverview()
}
}
TaskManager.VirtualDesktopInfo {
id: virtualDesktopInfo
}
TaskManager.ActivityInfo {
id: activityInfo
}
TaskManager.TasksModel {
id: tasksModel
filterByVirtualDesktop: true
filterByActivity: true
filterNotMaximized: false
filterByScreen: true
filterHidden: false
virtualDesktop: virtualDesktopInfo.currentDesktop
activity: activityInfo.currentActivity
groupMode: TaskManager.TasksModel.GroupApplications
}
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressAndHold: {
folio.HomeScreenState.openSettingsView();
haptics.buttonVibrate();
}
onClicked: (mouse) => {
// Right-click opens settings view (wallpaper/widgets), same as long-press
if (mouse.button === Qt.RightButton) {
folio.HomeScreenState.openSettingsView();
}
}
onDoubleClicked: {
if (folio.FolioSettings.doubleTapToLock) {
deviceLock.triggerLock();
}
}
onActiveFocusChanged: {
if (activeFocus) {
// Focus on first delegate when favorites bar focused
let firstDelegate = repeater.itemAt(0);
if (!firstDelegate) {
return;
}
firstDelegate.keyboardFocus();
}
}
MobileShell.HapticsEffect {
id: haptics
}
MobileShell.DeviceLock {
id: deviceLock
}
Repeater {
id: repeater
model: folio.FavouritesModel
delegate: Item {
id: delegate
readonly property var delegateModel: model.delegate
readonly property int index: model.index
readonly property var dragState: folio.HomeScreenState.dragState
readonly property bool isDropPositionThis: dragState.candidateDropPosition.location === Folio.DelegateDragPosition.Favourites &&
dragState.candidateDropPosition.favouritesPosition === delegate.index
readonly property bool isAppHoveredOver: folio.HomeScreenState.isDraggingDelegate &&
dragState.dropDelegate &&
dragState.dropDelegate.type === Folio.FolioDelegate.Application &&
isDropPositionThis
readonly property bool isLocationBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
// get the normalized index position value from the center so we can animate it
property double fromCenterValue: model.index - (root.totalItemCount / 2)
Behavior on fromCenterValue {
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad; }
}
// multiply the 'fromCenterValue' by the cell size to get the actual position
readonly property int centerPosition: (isLocationBottom ? root.dockCellWidth : root.dockCellHeight) * fromCenterValue
// Visual shift during drag-reorder: dragged item follows cursor,
// displaced items slide to make room.
property real dragVisualShift: {
if (root.dragReorderIndex === -1) return 0
if (delegate.index === root.dragReorderIndex) return root.dragReorderOffset
let targetIdx = root.dragTargetIndex
let myIdx = delegate.index
let dragIdx = root.dragReorderIndex
let cellW = root.dockCellWidth
if (targetIdx > dragIdx && myIdx > dragIdx && myIdx <= targetIdx) return -cellW
if (targetIdx < dragIdx && myIdx >= targetIdx && myIdx < dragIdx) return cellW
return 0
}
Behavior on dragVisualShift {
enabled: root.dragReorderIndex !== -1 && delegate.index !== root.dragReorderIndex
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
}
x: (isLocationBottom ? centerPosition + root.dockCenterX : (parent.width - width) / 2) + dragVisualShift
y: isLocationBottom ? (parent.height - height) / 2 : parent.height / 2 - centerPosition - root.dockCellHeight
implicitWidth: root.dockCellWidth
implicitHeight: root.dockCellHeight
width: root.dockCellWidth
height: root.dockCellHeight
// Keyboard navigation to other delegates
Keys.onPressed: (event) => {
switch (event.key) {
case Qt.Key_Up:
if (!isLocationBottom) {
let nextDelegate = repeater.itemAt(delegate.index - 1);
if (nextDelegate) {
nextDelegate.keyboardFocus();
event.accepted = true;
}
}
break;
case Qt.Key_Down:
if (!isLocationBottom) {
let nextDelegate = repeater.itemAt(delegate.index + 1);
if (nextDelegate) {
nextDelegate.keyboardFocus();
event.accepted = true;
}
}
break;
case Qt.Key_Left:
if (isLocationBottom) {
let nextDelegate = repeater.itemAt(delegate.index - 1);
if (nextDelegate) {
nextDelegate.keyboardFocus();
event.accepted = true;
}
}
break;
case Qt.Key_Right:
if (isLocationBottom) {
let nextDelegate = repeater.itemAt(delegate.index + 1);
if (nextDelegate) {
nextDelegate.keyboardFocus();
event.accepted = true;
}
}
break;
}
}
function keyboardFocus() {
if (loader.item) {
loader.item.keyboardFocus();
}
}
Loader {
id: loader
anchors.fill: parent
sourceComponent: {
if (delegate.delegateModel.type === Folio.FolioDelegate.Application) {
return appComponent;
} else if (delegate.delegateModel.type === Folio.FolioDelegate.Folder) {
return folderComponent;
} else {
// ghost entry
return placeholderComponent;
}
}
}
Component {
id: placeholderComponent
// square that shows when hovering over a spot to drop a delegate on (ghost entry)
PlaceholderDelegate {
id: dragDropFeedback
folio: root.folio
width: root.dockCellWidth
height: root.dockCellHeight
}
}
Component {
id: appComponent
AppDelegate {
id: appDelegate
folio: root.folio
maskManager: root.maskManager
application: delegate.delegateModel.application
name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.application.name : ""
shadow: true
turnToFolder: delegate.isAppHoveredOver
turnToFolderAnimEnabled: folio.HomeScreenState.isDraggingDelegate
// do not show if the drop animation is running to this delegate
visible: !(root.homeScreen.dropAnimationRunning && delegate.isDropPositionThis)
// don't show label in drag and drop mode
labelOpacity: delegate.opacity
// Convergence drag-reorder: click-and-drag to reorder
onDraggingChanged: {
if (root.convergenceMode && !folio.FolioSettings.lockLayout) {
if (appDelegate.dragging) {
contextMenu.close()
root.dragReorderIndex = delegate.index
root.dragReorderOffset = 0
} else {
let from = root.dragReorderIndex
let to = root.dragTargetIndex
root.dragReorderIndex = -1
root.dragReorderOffset = 0
if (from !== -1 && to !== -1 && from !== to) {
folio.FavouritesModel.moveEntry(from, to)
}
}
}
}
onDragMoved: (deltaX) => {
if (root.convergenceMode && !folio.FolioSettings.lockLayout) {
root.dragReorderOffset = deltaX
}
}
onPressAndHold: {
// prevent editing if lock layout is enabled
if (folio.FolioSettings.lockLayout) return;
// In convergence mode, drag-reorder is handled by DragHandler;
// only open the context menu on press-and-hold.
if (!root.convergenceMode) {
let mappedCoords = root.homeScreen.prepareStartDelegateDrag(delegate.delegateModel, appDelegate.delegateItem);
folio.HomeScreenState.startDelegateFavouritesDrag(
mappedCoords.x,
mappedCoords.y,
appDelegate.pressPosition.x,
appDelegate.pressPosition.y,
delegate.index
);
}
contextMenu.open();
haptics.buttonVibrate();
}
onPressAndHoldReleased: {
// cancel the event if the delegate is not dragged
if (!root.convergenceMode && folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate) {
homeScreen.cancelDelegateDrag();
}
}
onRightMousePress: {
contextMenu.open();
}
ContextMenuLoader {
id: contextMenu
menuPopupType: root.convergenceMode ? T.Popup.Window : T.Popup.Item
// close menu when drag starts
Connections {
target: folio.HomeScreenState
function onDelegateDragStarted() {
contextMenu.close();
}
}
actions: [
Kirigami.Action {
icon.name: delegate.delegateModel.application.icon
text: i18n("Launch")
onTriggered: appDelegate.launchApp()
},
Kirigami.Action {
icon.name: "emblem-favorite"
text: i18n("Remove from Dock")
enabled: !folio.FolioSettings.lockLayout
onTriggered: folio.FavouritesModel.removeEntry(delegate.index)
}
]
}
}
}
Component {
id: folderComponent
AppFolderDelegate {
id: appFolderDelegate
folio: root.folio
maskManager: root.maskManager
shadow: true
folder: delegate.delegateModel.folder
name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.folder.name : ""
// do not show if the drop animation is running to this delegate, and the drop delegate is a folder
visible: !(root.homeScreen.dropAnimationRunning &&
delegate.isDropPositionThis &&
delegate.dragState.dropDelegate.type === Folio.FolioDelegate.Folder)
appHoveredOver: delegate.isAppHoveredOver
// don't show label in drag and drop mode
labelOpacity: delegate.opacity
onAfterClickAnimation: {
const pos = homeScreen.prepareFolderOpen(appFolderDelegate.contentItem);
folio.HomeScreenState.openFolder(pos.x, pos.y, delegate.delegateModel.folder);
}
// Convergence drag-reorder: click-and-drag to reorder
onDraggingChanged: {
if (root.convergenceMode && !folio.FolioSettings.lockLayout) {
if (appFolderDelegate.dragging) {
contextMenu.close()
root.dragReorderIndex = delegate.index
root.dragReorderOffset = 0
} else {
let from = root.dragReorderIndex
let to = root.dragTargetIndex
root.dragReorderIndex = -1
root.dragReorderOffset = 0
if (from !== -1 && to !== -1 && from !== to) {
folio.FavouritesModel.moveEntry(from, to)
}
}
}
}
onDragMoved: (deltaX) => {
if (root.convergenceMode && !folio.FolioSettings.lockLayout) {
root.dragReorderOffset = deltaX
}
}
onPressAndHold: {
// prevent editing if lock layout is enabled
if (folio.FolioSettings.lockLayout) return;
if (!root.convergenceMode) {
let mappedCoords = root.homeScreen.prepareStartDelegateDrag(delegate.delegateModel, appFolderDelegate.delegateItem);
folio.HomeScreenState.startDelegateFavouritesDrag(
mappedCoords.x,
mappedCoords.y,
appFolderDelegate.pressPosition.x,
appFolderDelegate.pressPosition.y,
delegate.index
);
}
contextMenu.open();
haptics.buttonVibrate();
}
onPressAndHoldReleased: {
// cancel the event if the delegate is not dragged
if (!root.convergenceMode && folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate) {
root.homeScreen.cancelDelegateDrag();
}
}
onRightMousePress: {
contextMenu.open();
}
ContextMenuLoader {
id: contextMenu
menuPopupType: root.convergenceMode ? T.Popup.Window : T.Popup.Item
// close menu when drag starts
Connections {
target: folio.HomeScreenState
function onDelegateDragStarted() {
contextMenu.close();
}
}
actions: [
Kirigami.Action {
icon.name: "emblem-favorite"
text: i18n("Remove")
onTriggered: deleteDialog.open()
}
]
ConfirmDeleteFolderDialogLoader {
id: deleteDialog
parent: root.homeScreen
onAccepted: folio.FavouritesModel.removeEntry(delegate.index)
}
}
}
}
}
}
// Running-app task icons (convergence mode only)
Timer {
id: thumbnailShowTimer
interval: Kirigami.Units.toolTipDelay
onTriggered: {
thumbnailPopup.visible = true
}
}
Timer {
id: thumbnailHideTimer
interval: 300
onTriggered: {
thumbnailPopup.visible = false
root.hoveredTaskIndex = -1
}
}
Window {
id: thumbnailPopup
property var targetDelegate: null
property int taskIndex: -1
property var windowIds: []
property bool isGroup: false
property bool popupHovered: false
function open() { visible = true }
function close() { visible = false }
readonly property bool opened: visible
flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowDoesNotAcceptFocus
color: "transparent"
readonly property real thumbWidth: windowIds.length <= 1
? Kirigami.Units.gridUnit * 16
: Kirigami.Units.gridUnit * 12
width: Math.max(Kirigami.Units.gridUnit * 8,
windowIds.length * thumbWidth
+ Math.max(0, windowIds.length - 1) * Kirigami.Units.smallSpacing
+ 2 * Kirigami.Units.smallSpacing)
height: popupContent.implicitHeight + 2 * Kirigami.Units.smallSpacing
// Position above the hovered dock icon, in global coordinates
x: {
if (!targetDelegate) return 0
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
return Math.max(0, delegateGlobal.x + (targetDelegate.width - width) / 2)
}
y: {
if (!targetDelegate) return 0
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
return delegateGlobal.y - height - Kirigami.Units.smallSpacing
}
onVisibleChanged: {
if (!visible) {
windowIds = []
targetDelegate = null
taskIndex = -1
isGroup = false
}
}
Rectangle {
anchors.fill: parent
color: Kirigami.Theme.backgroundColor
border.color: Qt.rgba(
Kirigami.Theme.textColor.r,
Kirigami.Theme.textColor.g,
Kirigami.Theme.textColor.b, 0.2)
border.width: 1
radius: Kirigami.Units.cornerRadius
// HoverHandler for popup-level hover tracking (does not
// consume mouse events, so clicks still reach delegates).
HoverHandler {
id: popupHoverHandler
onHoveredChanged: {
thumbnailPopup.popupHovered = hovered
if (hovered) {
thumbnailHideTimer.stop()
} else if (root.hoveredTaskIndex < 0) {
thumbnailHideTimer.restart()
}
}
}
Row {
id: popupContent
anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Repeater {
model: thumbnailPopup.windowIds.length
delegate: MouseArea {
id: thumbEntry
width: thumbnailPopup.thumbWidth
height: thumbColumn.implicitHeight
hoverEnabled: true
readonly property string childUuid: thumbnailPopup.windowIds[index] || ""
readonly property string childTitle: {
if (!thumbnailPopup.isGroup)
return tasksModel.data(tasksModel.makeModelIndex(thumbnailPopup.taskIndex), 0) || ""
return tasksModel.data(tasksModel.makeModelIndex(thumbnailPopup.taskIndex, index), 0) || ""
}
onClicked: {
var idx = thumbnailPopup.isGroup
? tasksModel.makeModelIndex(thumbnailPopup.taskIndex, index)
: tasksModel.makeModelIndex(thumbnailPopup.taskIndex)
tasksModel.requestActivate(idx)
thumbnailPopup.close()
}
Rectangle {
anchors.fill: parent
radius: Kirigami.Units.cornerRadius
color: thumbEntry.containsMouse
? Qt.rgba(Kirigami.Theme.highlightColor.r,
Kirigami.Theme.highlightColor.g,
Kirigami.Theme.highlightColor.b, 0.15)
: "transparent"
}
Column {
id: thumbColumn
width: parent.width
spacing: Kirigami.Units.smallSpacing
Item {
width: parent.width
height: width * 9 / 16
Loader {
id: thumbPipeWireLoader
active: thumbnailPopup.visible
&& thumbEntry.childUuid !== ""
anchors.fill: parent
sourceComponent: PipeWireThumbnail {
windowUuid: thumbEntry.childUuid
}
}
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.huge
height: width
source: thumbnailPopup.targetDelegate
? thumbnailPopup.targetDelegate.model.decoration
: ""
visible: !thumbPipeWireLoader.item
|| !thumbPipeWireLoader.item.hasThumbnail
}
MouseArea {
id: closeButton
width: Kirigami.Units.iconSizes.small
height: width
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Kirigami.Units.smallSpacing
z: 1
visible: thumbEntry.containsMouse
onClicked: {
var idx = thumbnailPopup.isGroup
? tasksModel.makeModelIndex(thumbnailPopup.taskIndex, index)
: tasksModel.makeModelIndex(thumbnailPopup.taskIndex)
tasksModel.requestClose(idx)
if (thumbnailPopup.windowIds.length <= 1) {
thumbnailPopup.close()
}
}
Kirigami.Icon {
anchors.fill: parent
source: "window-close"
}
}
}
PC3.Label {
width: parent.width
text: thumbEntry.childTitle
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 1
}
}
}
}
}
}
}
Repeater {
id: taskRepeater
model: root.convergenceMode ? tasksModel : null
delegate: Item {
id: taskDelegate
required property int index
required property var model
readonly property bool isLocationBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
// Position after all favourites
property double fromCenterValue: (repeater.count + taskDelegate.index) - (root.totalItemCount / 2)
Behavior on fromCenterValue {
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad; }
}
readonly property int centerPosition: (isLocationBottom ? root.dockCellWidth : root.dockCellHeight) * fromCenterValue
x: isLocationBottom ? centerPosition + root.dockCenterX : (parent.width - width) / 2
y: isLocationBottom ? (parent.height - height) / 2 : parent.height / 2 - centerPosition - root.dockCellHeight
implicitWidth: root.dockCellWidth
implicitHeight: root.dockCellHeight
width: root.dockCellWidth
height: root.dockCellHeight
// Hover highlight background
Rectangle {
anchors.fill: parent
radius: Kirigami.Units.cornerRadius
color: taskMouseArea.containsPress
? Qt.rgba(255, 255, 255, 0.2)
: (taskMouseArea.containsMouse ? Qt.rgba(255, 255, 255, 0.1) : "transparent")
}
// Task icon
Kirigami.Icon {
anchors.centerIn: parent
width: Math.min(parent.width, parent.height) * 0.6
height: width
source: taskDelegate.model.decoration
active: taskMouseArea.containsMouse
}
// Window indicator dots (one per sibling window of the same app)
Row {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: Kirigami.Units.smallSpacing / 2
spacing: Kirigami.Units.smallSpacing / 2
Repeater {
model: {
var ids = taskDelegate.model.WinIdList
return ids ? ids.length : 1
}
Rectangle {
width: Kirigami.Units.smallSpacing * 1.5
height: width
radius: width / 2
color: Kirigami.Theme.highlightColor
opacity: taskDelegate.model.IsActive === true ? 1.0 : 0.4
}
}
}
// Click to activate, hover for thumbnail preview
MouseArea {
id: taskMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
thumbnailPopup.close()
thumbnailShowTimer.stop()
taskContextMenu.open();
} else {
var winIds = taskDelegate.model.WinIdList
if (winIds && winIds.length > 1) {
// Multiple windows: toggle thumbnail popup
if (thumbnailPopup.opened && thumbnailPopup.taskIndex === taskDelegate.index) {
thumbnailPopup.close()
} else {
thumbnailPopup.targetDelegate = taskDelegate
thumbnailPopup.taskIndex = taskDelegate.index
thumbnailPopup.windowIds = winIds
thumbnailPopup.isGroup = taskDelegate.model.IsGroupParent === true
thumbnailPopup.open()
}
} else {
thumbnailPopup.close()
tasksModel.requestActivate(tasksModel.makeModelIndex(taskDelegate.index));
}
}
}
onContainsMouseChanged: {
if (containsMouse) {
thumbnailHideTimer.stop()
thumbnailPopup.targetDelegate = taskDelegate
thumbnailPopup.taskIndex = taskDelegate.index
var winIds = taskDelegate.model.WinIdList
thumbnailPopup.windowIds = winIds ? winIds : []
thumbnailPopup.isGroup = taskDelegate.model.IsGroupParent === true
root.hoveredTaskIndex = taskDelegate.index
if (!thumbnailPopup.opened) {
thumbnailShowTimer.restart()
}
} else {
root.hoveredTaskIndex = -1
if (!thumbnailPopup.popupHovered) {
thumbnailShowTimer.stop()
thumbnailHideTimer.restart()
}
}
}
}
PC3.Menu {
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)
enabled: !folio.FolioSettings.lockLayout
onClicked: folio.FavouritesModel.addApplication(taskContextMenu.taskStorageId)
}
PC3.MenuItem {
icon.name: taskDelegate.model.IsMinimized ? "window-restore" : "window-minimize"
text: taskDelegate.model.IsMinimized ? i18n("Restore") : i18n("Minimize")
onClicked: tasksModel.requestToggleMinimized(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem {
icon.name: taskDelegate.model.IsMaximized ? "window-restore" : "window-maximize"
text: taskDelegate.model.IsMaximized ? i18n("Restore") : i18n("Maximize")
visible: taskDelegate.model.IsGroupParent !== true
onClicked: tasksModel.requestToggleMaximized(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem {
icon.name: "window-close"
text: {
var ids = taskDelegate.model.WinIdList
return (ids && ids.length > 1) ? i18n("Close All") : i18n("Close")
}
onClicked: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index))
}
}
}
}
}