shift-shell/containments/homescreens/folio/qml/FavouritesBar.qml
Marco Allegretti b80c878487 Make dock pager follow keyboard focus order
Keep keyboard navigation aligned with the compact desktop pager group. Right from the Desktop button now enters the pager, pager buttons expose accessible activation, and focus continues into dock content after the last desktop.
2026-05-29 11:11:59 +02:00

2216 lines
92 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 QtCore
import Qt.labs.folderlistmodel
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.plasma.private.mobileshell.windowplugin as WindowPlugin
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
property bool suppressRunningTasks: false
signal delegateDragRequested(var item)
// Convergence mode: show running apps alongside favourites
readonly property bool convergenceMode: ShellSettings.Settings.convergenceModeEnabled
readonly property bool showRunningTasks: convergenceMode && !suppressRunningTasks
readonly property int totalItemCount: repeater.count + (showRunningTasks ? taskRepeater.count : 0)
readonly property int longAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialDefault)
readonly property int shortAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
// In convergence mode, size icons to fit the dock bar instead of using page grid cells
property real dockCellWidth: convergenceMode ? root.height : folio.HomeScreenState.pageCellWidth
property real dockCellHeight: convergenceMode ? root.height : folio.HomeScreenState.pageCellHeight
Behavior on dockCellWidth { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration } }
Behavior on dockCellHeight { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration } }
// Navigation buttons width (used to offset center positioning)
property real navButtonWidth: convergenceMode ? root.height : 0
property real dockItemInset: convergenceMode ? Math.max(2, Kirigami.Units.smallSpacing / 2) : 0
property real dockIconSize: Math.min(root.height * 0.56, Kirigami.Units.iconSizes.large)
Behavior on navButtonWidth { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration } }
Behavior on dockItemInset { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration } }
Behavior on dockIconSize { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration } }
function dockItemColor(pressed, hovered, active) {
if (pressed) {
return Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.18)
}
if (active) {
return Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, hovered ? 0.18 : 0.12)
}
if (hovered) {
return Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
}
return "transparent"
}
// Visible spacer between pinned favourites and running tasks
readonly property bool showSpacer: showRunningTasks && repeater.count > 0 && taskRepeater.count > 0
property real spacerWidth: showSpacer ? Kirigami.Units.largeSpacing * 2 : 0
Behavior on spacerWidth {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration }
}
// 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))
}
// 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 !== ""
// Virtual desktop pager (convergence mode, 2+ desktops)
readonly property bool showPager: convergenceMode && virtualDesktopInfo.numberOfDesktops > 1
property real pagerButtonWidth: showPager ? Math.min(root.height, Kirigami.Units.gridUnit * 2.5) : 0
property int pagerWheelDelta: 0
property bool pagerWheelLocked: false
readonly property int pagerLeftCount: showPager ? virtualDesktopInfo.numberOfDesktops : 0
readonly property int pagerRightCount: 0
property real desktopButtonWidth: convergenceMode ? root.height : 0
property real trashButtonWidth: convergenceMode ? root.height : 0
property real searchButtonWidth: convergenceMode ? root.height : 0
readonly property real leftControlsWidth: convergenceMode ? navButtonWidth + desktopButtonWidth + pagerLeftCount * pagerButtonWidth : 0
readonly property real rightControlsWidth: convergenceMode ? navButtonWidth + searchButtonWidth + trashButtonWidth : 0
readonly property real dockCenterX: convergenceMode
? leftControlsWidth + (root.width - leftControlsWidth - rightControlsWidth) / 2
: root.width / 2
Behavior on pagerButtonWidth { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration } }
Behavior on desktopButtonWidth { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration } }
Behavior on trashButtonWidth { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration } }
Behavior on searchButtonWidth { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration } }
property Item pendingDockToolTipItem: null
property Item activeDockToolTipItem: null
Timer {
id: dockToolTipTimer
interval: Kirigami.Units.toolTipDelay
onTriggered: {
root.activeDockToolTipItem = root.pendingDockToolTipItem
root.pendingDockToolTipItem = null
dockToolTipHideTimer.restart()
}
}
Timer {
id: dockToolTipHideTimer
interval: Kirigami.Units.toolTipDelay * 3
onTriggered: root.hideDockToolTip(root.activeDockToolTipItem)
}
Timer {
id: pagerWheelEndTimer
interval: 160
onTriggered: {
root.pagerWheelDelta = 0
root.pagerWheelLocked = false
}
}
function requestDockToolTip(item) {
activeDockToolTipItem = null
pendingDockToolTipItem = item
dockToolTipHideTimer.stop()
dockToolTipTimer.restart()
}
function hideDockToolTip(item) {
if (!item || pendingDockToolTipItem === item || activeDockToolTipItem === item) {
pendingDockToolTipItem = null
activeDockToolTipItem = null
dockToolTipTimer.stop()
dockToolTipHideTimer.stop()
}
}
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)
}
function pagerDesktopNameForId(desktopId) {
let ids = virtualDesktopInfo.desktopIds
if (!ids) {
return ""
}
for (let i = 0; i < ids.length; ++i) {
if (String(ids[i]) === String(desktopId)) {
return root.pagerDesktopName(i)
}
}
return ""
}
function menuDesktopIds(isOnAllDesktops) {
let ids = virtualDesktopInfo.desktopIds
if (!ids || ids.length <= 1) {
return []
}
let result = []
for (let i = 0; i < ids.length; ++i) {
if (isOnAllDesktops || String(ids[i]) !== String(virtualDesktopInfo.currentDesktop)) {
result.push(ids[i])
}
}
return result
}
function desktopIndexForId(desktopId) {
let ids = virtualDesktopInfo.desktopIds
if (!ids) {
return -1
}
for (let i = 0; i < ids.length; ++i) {
if (String(ids[i]) === String(desktopId)) {
return i
}
}
return -1
}
function activateAdjacentDesktop(direction) {
let ids = virtualDesktopInfo.desktopIds
if (!ids || ids.length <= 1) {
return
}
let currentIndex = root.desktopIndexForId(virtualDesktopInfo.currentDesktop)
if (currentIndex < 0) {
return
}
let targetIndex = Math.max(0, Math.min(ids.length - 1, currentIndex + direction))
if (targetIndex !== currentIndex) {
root.folio.activateVirtualDesktop(String(ids[targetIndex]))
}
}
function handlePagerWheel(wheel) {
if (!root.showPager) {
return
}
const axisDelta = wheel.angleDelta.y || -wheel.angleDelta.x
if (axisDelta === 0) {
return
}
pagerWheelEndTimer.restart()
if (root.pagerWheelLocked) {
return
}
root.pagerWheelDelta += axisDelta * (wheel.inverted ? -1 : 1)
if (root.pagerWheelDelta >= 120) {
root.pagerWheelLocked = true
root.pagerWheelDelta = 0
root.activateAdjacentDesktop(-1)
} else if (root.pagerWheelDelta <= -120) {
root.pagerWheelLocked = true
root.pagerWheelDelta = 0
root.activateAdjacentDesktop(1)
}
}
function dynamicTilingMoveToDesktopAction(desktopId) {
let index = root.desktopIndexForId(desktopId)
if (index < 0) {
return ""
}
return "move-to-desktop:" + String(desktopId) + "|" + String(index + 1)
}
function taskActivities(taskModel) {
let activities = taskModel.Activities
return activities ? activities : []
}
function taskWindowId(taskModel) {
let winIds = taskModel.WinIdList
return winIds && winIds.length === 1 ? String(winIds[0]) : ""
}
function activityName(activityId) {
let name = activityInfo.activityName(activityId)
return name && name.length > 0 ? name : i18n("Activity")
}
function menuActivityIds(taskActivities) {
let runningActivities = activityInfo.runningActivities()
if (!runningActivities || runningActivities.length <= 1) {
return []
}
let result = []
for (let i = 0; i < runningActivities.length; ++i) {
let activityId = runningActivities[i]
if (taskActivities.length === 1 && String(taskActivities[0]) === String(activityId)) {
continue
}
result.push(activityId)
}
return result
}
// 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 + desktopButtonWidth + i * pagerButtonWidth
if (x >= bx && x < bx + pagerButtonWidth)
return (ids && i < ids.length) ? String(ids[i]) : ""
}
return ""
}
function focusFirstDockContent() {
let firstPager = leftPagerRepeater.itemAt(0)
if (firstPager) { firstPager.forceActiveFocus(); return }
root.focusAfterPager()
}
function focusAfterPager() {
let first = repeater.itemAt(0)
if (first) { first.keyboardFocus(); return }
let firstTask = taskRepeater.itemAt(0)
if (firstTask) { firstTask.forceActiveFocus(); return }
searchButton.forceActiveFocus()
}
function focusBeforeSearch() {
let lastTask = taskRepeater.itemAt(taskRepeater.count - 1)
if (lastTask) { lastTask.forceActiveFocus(); return }
let lastFav = repeater.itemAt(repeater.count - 1)
if (lastFav) { lastFav.keyboardFocus(); return }
let lastPager = leftPagerRepeater.itemAt(leftPagerRepeater.count - 1)
if (lastPager) { lastPager.forceActiveFocus(); return }
desktopButton.forceActiveFocus()
}
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
visible: root.convergenceMode || opacity > 0
enabled: root.convergenceMode
opacity: root.convergenceMode ? 1 : 0
activeFocusOnTab: root.convergenceMode
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: root.navButtonWidth
color: "transparent"
Behavior on opacity {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
}
Accessible.role: Accessible.Button
Accessible.name: i18n("Home")
Accessible.onPressAction: MobileShellState.ShellDBusClient.openHomeScreen()
Keys.onReturnPressed: MobileShellState.ShellDBusClient.openHomeScreen()
Keys.onEnterPressed: MobileShellState.ShellDBusClient.openHomeScreen()
Keys.onSpacePressed: MobileShellState.ShellDBusClient.openHomeScreen()
Keys.onRightPressed: {
desktopButton.forceActiveFocus()
}
KeyboardHighlight {
anchors.fill: parent
visible: homeButton.activeFocus
}
MobileShell.MotionStateLayer {
anchors.fill: parent
anchors.margins: root.dockItemInset
radius: Kirigami.Units.cornerRadius
hovered: homeMouseArea.containsMouse
pressed: homeMouseArea.containsPress
pressedOpacity: 0.18
}
Kirigami.Icon {
anchors.centerIn: parent
width: root.dockIconSize
height: width
source: "start-here-shift"
active: homeMouseArea.containsMouse
isMask: true
color: Kirigami.Theme.textColor
}
MouseArea {
id: homeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: root.convergenceMode ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: MobileShellState.ShellDBusClient.openHomeScreen()
}
}
// Peek at Desktop button (convergence mode, next to Home)
Rectangle {
id: desktopButton
visible: root.convergenceMode || opacity > 0
enabled: root.convergenceMode
opacity: root.convergenceMode ? 1 : 0
activeFocusOnTab: root.convergenceMode
x: root.navButtonWidth
y: 0
width: root.desktopButtonWidth
height: root.height
color: "transparent"
function toggleDesktop() {
WindowPlugin.WindowUtil.requestShowingDesktop(!WindowPlugin.WindowUtil.isShowingDesktop)
}
Behavior on opacity {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
}
Accessible.role: Accessible.Button
Accessible.name: WindowPlugin.WindowUtil.isShowingDesktop ? i18n("Stop Peeking at Desktop") : i18n("Peek at Desktop")
Accessible.onPressAction: desktopButton.toggleDesktop()
Keys.onReturnPressed: desktopButton.toggleDesktop()
Keys.onEnterPressed: desktopButton.toggleDesktop()
Keys.onSpacePressed: desktopButton.toggleDesktop()
Keys.onLeftPressed: homeButton.forceActiveFocus()
Keys.onRightPressed: root.focusFirstDockContent()
KeyboardHighlight {
anchors.fill: parent
visible: desktopButton.activeFocus
}
MobileShell.MotionStateLayer {
anchors.fill: parent
anchors.margins: root.dockItemInset
radius: Kirigami.Units.cornerRadius
hovered: desktopMouseArea.containsMouse
pressed: desktopMouseArea.containsPress
active: WindowPlugin.WindowUtil.isShowingDesktop
pressedOpacity: 0.18
}
Kirigami.Icon {
anchors.centerIn: parent
width: root.dockIconSize
height: width
source: "user-desktop"
active: desktopMouseArea.containsMouse || WindowPlugin.WindowUtil.isShowingDesktop
isMask: true
color: Kirigami.Theme.textColor
}
PC3.ToolTip {
visible: root.activeDockToolTipItem === desktopButton && desktopMouseArea.containsMouse && !desktopMouseArea.containsPress && !desktopContextMenu.opened
text: desktopButton.Accessible.name
}
MouseArea {
id: desktopMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: root.convergenceMode ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: (mouse) => {
root.hideDockToolTip(desktopButton)
if (mouse.button === Qt.RightButton) {
desktopContextMenu.open()
} else {
desktopButton.toggleDesktop()
}
}
onPressAndHold: {
root.hideDockToolTip(desktopButton)
desktopContextMenu.open()
haptics.buttonVibrate()
}
onContainsMouseChanged: {
if (containsMouse) {
root.requestDockToolTip(desktopButton)
} else {
root.hideDockToolTip(desktopButton)
}
}
onPressedChanged: {
if (pressed) {
root.hideDockToolTip(desktopButton)
}
}
}
PC3.Menu {
id: desktopContextMenu
popupType: T.Popup.Window
PC3.MenuItem {
icon.name: "user-desktop"
text: desktopButton.Accessible.name
onTriggered: desktopButton.toggleDesktop()
}
PC3.MenuItem {
icon.name: "window-minimize-symbolic"
text: i18n("Minimize All Windows")
enabled: !WindowPlugin.WindowUtil.isShowingDesktop
onTriggered: root.folio.triggerMinimizeAll()
}
}
}
// Overview button (convergence mode, right end)
Rectangle {
id: overviewButton
visible: root.convergenceMode || opacity > 0
enabled: root.convergenceMode
opacity: root.convergenceMode ? 1 : 0
activeFocusOnTab: root.convergenceMode
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
width: root.navButtonWidth
color: "transparent"
Behavior on opacity {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
}
Accessible.role: Accessible.Button
Accessible.name: i18n("Overview")
Accessible.onPressAction: root.folio.triggerOverview()
Keys.onReturnPressed: root.folio.triggerOverview()
Keys.onEnterPressed: root.folio.triggerOverview()
Keys.onSpacePressed: root.folio.triggerOverview()
Keys.onLeftPressed: {
searchButton.forceActiveFocus()
}
KeyboardHighlight {
anchors.fill: parent
visible: overviewButton.activeFocus
}
MobileShell.MotionStateLayer {
anchors.fill: parent
anchors.margins: root.dockItemInset
radius: Kirigami.Units.cornerRadius
hovered: overviewMouseArea.containsMouse
pressed: overviewMouseArea.containsPress
pressedOpacity: 0.18
}
Kirigami.Icon {
anchors.centerIn: parent
width: root.dockIconSize
height: width
source: "activities"
active: overviewMouseArea.containsMouse
isMask: true
color: Kirigami.Theme.textColor
}
MouseArea {
id: overviewMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: root.convergenceMode ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.folio.triggerOverview()
}
}
// Search button (convergence mode, immediately left of Overview)
Rectangle {
id: searchButton
visible: root.convergenceMode || opacity > 0
enabled: root.convergenceMode
opacity: root.convergenceMode ? 1 : 0
activeFocusOnTab: root.convergenceMode
x: root.width - root.navButtonWidth - root.searchButtonWidth
y: 0
width: root.searchButtonWidth
height: root.height
color: "transparent"
Behavior on opacity {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
}
Accessible.role: Accessible.Button
Accessible.name: i18n("Search")
Accessible.onPressAction: root.folio.HomeScreenState.openSearchWidget()
Keys.onReturnPressed: root.folio.HomeScreenState.openSearchWidget()
Keys.onEnterPressed: root.folio.HomeScreenState.openSearchWidget()
Keys.onSpacePressed: root.folio.HomeScreenState.openSearchWidget()
Keys.onLeftPressed: root.focusBeforeSearch()
Keys.onRightPressed: overviewButton.forceActiveFocus()
KeyboardHighlight {
anchors.fill: parent
visible: searchButton.activeFocus
}
MobileShell.MotionStateLayer {
anchors.fill: parent
anchors.margins: root.dockItemInset
radius: Kirigami.Units.cornerRadius
hovered: searchMouseArea.containsMouse
pressed: searchMouseArea.containsPress
pressedOpacity: 0.18
}
Kirigami.Icon {
anchors.centerIn: parent
width: root.dockIconSize
height: width
source: "search"
active: searchMouseArea.containsMouse
isMask: true
color: Kirigami.Theme.textColor
}
PC3.ToolTip {
visible: root.activeDockToolTipItem === searchButton && searchMouseArea.containsMouse && !searchMouseArea.containsPress
text: i18n("Search")
}
MouseArea {
id: searchMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: root.convergenceMode ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
root.hideDockToolTip(searchButton)
root.folio.HomeScreenState.openSearchWidget()
}
onContainsMouseChanged: {
if (containsMouse) {
root.requestDockToolTip(searchButton)
} else {
root.hideDockToolTip(searchButton)
}
}
onPressedChanged: {
if (pressed) {
root.hideDockToolTip(searchButton)
}
}
}
}
// ---- Virtual desktop pager: compact group next to the Desktop button ----
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 + root.desktopButtonWidth + index * root.pagerButtonWidth
y: 0
width: root.pagerButtonWidth
height: root.height
activeFocusOnTab: root.showPager
function activateDesktop() {
if (leftDesktopBtn.desktopId) {
root.folio.activateVirtualDesktop(leftDesktopBtn.desktopId)
}
}
Accessible.role: Accessible.Button
Accessible.name: root.pagerDesktopName(leftDesktopBtn.index)
Accessible.onPressAction: leftDesktopBtn.activateDesktop()
Keys.onReturnPressed: leftDesktopBtn.activateDesktop()
Keys.onEnterPressed: leftDesktopBtn.activateDesktop()
Keys.onSpacePressed: leftDesktopBtn.activateDesktop()
Keys.onLeftPressed: {
let previous = leftPagerRepeater.itemAt(index - 1)
if (previous) { previous.forceActiveFocus(); return }
desktopButton.forceActiveFocus()
}
Keys.onRightPressed: {
let next = leftPagerRepeater.itemAt(index + 1)
if (next) { next.forceActiveFocus(); return }
root.focusAfterPager()
}
KeyboardHighlight {
anchors.fill: parent
visible: leftDesktopBtn.activeFocus
}
MobileShell.MotionStateLayer {
anchors.fill: parent
anchors.margins: root.dockItemInset
radius: Kirigami.Units.cornerRadius
hovered: leftPagerHover.containsMouse
pressed: leftPagerHover.containsPress
active: leftDesktopBtn.isCurrent || leftDesktopBtn.isDragTarget
pressedOpacity: 0.18
activeOpacity: 0.18
activeHoverOpacity: 0.25
}
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: root.activeDockToolTipItem === leftDesktopBtn && leftPagerHover.containsMouse && !leftPagerHover.containsPress && !leftPagerContextMenu.opened
text: root.pagerDesktopName(leftDesktopBtn.index)
}
MouseArea {
id: leftPagerHover
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onWheel: (wheel) => root.handlePagerWheel(wheel)
onClicked: (mouse) => {
root.hideDockToolTip(leftDesktopBtn)
if (mouse.button === Qt.RightButton) {
leftPagerContextMenu.open()
} else {
leftDesktopBtn.activateDesktop()
}
}
onPressAndHold: {
root.hideDockToolTip(leftDesktopBtn)
leftPagerContextMenu.open()
haptics.buttonVibrate()
}
onContainsMouseChanged: {
if (containsMouse) {
root.requestDockToolTip(leftDesktopBtn)
} else {
root.hideDockToolTip(leftDesktopBtn)
}
}
onPressedChanged: {
if (pressed) {
root.hideDockToolTip(leftDesktopBtn)
}
}
}
PC3.Menu {
id: leftPagerContextMenu
popupType: T.Popup.Window
PC3.MenuItem {
icon.name: "list-add"
text: i18n("Add Virtual Desktop")
onTriggered: root.folio.createVirtualDesktop()
}
PC3.MenuItem {
icon.name: "list-remove"
text: i18n("Remove Virtual Desktop")
enabled: virtualDesktopInfo.numberOfDesktops > 1
onTriggered: root.folio.removeLastVirtualDesktop()
}
Controls.MenuSeparator {}
PC3.MenuItem {
icon.name: "preferences-desktop-virtual"
text: i18n("Configure Virtual Desktops…")
onTriggered: MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_kwin_virtualdesktops")
}
}
}
}
// ---- 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.searchButtonWidth - root.trashButtonWidth - (root.pagerRightCount - index) * root.pagerButtonWidth
y: 0
width: root.pagerButtonWidth
height: root.height
MobileShell.MotionStateLayer {
anchors.fill: parent
anchors.margins: root.dockItemInset
radius: Kirigami.Units.cornerRadius
hovered: rightPagerHover.containsMouse
pressed: rightPagerHover.containsPress
active: rightDesktopBtn.isCurrent || rightDesktopBtn.isDragTarget
pressedOpacity: 0.18
activeOpacity: 0.18
activeHoverOpacity: 0.25
}
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: root.activeDockToolTipItem === rightDesktopBtn && rightPagerHover.containsMouse && !rightPagerHover.containsPress && !rightPagerContextMenu.opened
text: root.pagerDesktopName(rightDesktopBtn.desktopIndex)
}
MouseArea {
id: rightPagerHover
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onWheel: (wheel) => root.handlePagerWheel(wheel)
onClicked: (mouse) => {
root.hideDockToolTip(rightDesktopBtn)
if (mouse.button === Qt.RightButton) {
rightPagerContextMenu.open()
} else if (rightDesktopBtn.desktopId) {
root.folio.activateVirtualDesktop(rightDesktopBtn.desktopId)
}
}
onPressAndHold: {
root.hideDockToolTip(rightDesktopBtn)
rightPagerContextMenu.open()
haptics.buttonVibrate()
}
onContainsMouseChanged: {
if (containsMouse) {
root.requestDockToolTip(rightDesktopBtn)
} else {
root.hideDockToolTip(rightDesktopBtn)
}
}
onPressedChanged: {
if (pressed) {
root.hideDockToolTip(rightDesktopBtn)
}
}
}
PC3.Menu {
id: rightPagerContextMenu
popupType: T.Popup.Window
PC3.MenuItem {
icon.name: "list-add"
text: i18n("Add Virtual Desktop")
onTriggered: root.folio.createVirtualDesktop()
}
PC3.MenuItem {
icon.name: "list-remove"
text: i18n("Remove Virtual Desktop")
enabled: virtualDesktopInfo.numberOfDesktops > 1
onTriggered: root.folio.removeLastVirtualDesktop()
}
Controls.MenuSeparator {}
PC3.MenuItem {
icon.name: "preferences-desktop-virtual"
text: i18n("Configure Virtual Desktops…")
onTriggered: MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_kwin_virtualdesktops")
}
}
}
}
// ---- Trash button (convergence mode, sits between the right pager wing and the Overview button) ----
// Watches ~/.local/share/Trash/files to detect empty/full state.
// FolderListModel reacts to directory changes automatically.
FolderListModel {
id: trashFilesModel
folder: StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Trash/files"
showFiles: true
showDirs: true
showDotAndDotDot: false
}
// Confirmation dialog for "Empty Trash" — parented to the homescreen so it
// is sized correctly and floats above all dock content.
Loader {
id: emptyTrashDialogLoader
parent: root.homeScreen
anchors.fill: parent
active: false
function open() {
active = true;
item.open();
}
sourceComponent: Kirigami.PromptDialog {
title: i18n("Empty Trash")
subtitle: i18n("Permanently delete all items in the trash? This action cannot be undone.")
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.Cancel
onAccepted: root.folio.emptyTrash()
onClosed: emptyTrashDialogLoader.active = false
}
}
Rectangle {
id: trashButton
visible: root.convergenceMode || opacity > 0
enabled: root.convergenceMode
opacity: root.convergenceMode ? 1 : 0
activeFocusOnTab: root.convergenceMode
x: root.width - root.navButtonWidth - root.searchButtonWidth - root.trashButtonWidth
y: 0
width: root.trashButtonWidth
height: root.height
color: "transparent"
Behavior on opacity {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
}
Accessible.role: Accessible.Button
Accessible.name: i18n("Trash")
Accessible.onPressAction: Qt.openUrlExternally("trash:/")
Keys.onReturnPressed: Qt.openUrlExternally("trash:/")
Keys.onEnterPressed: Qt.openUrlExternally("trash:/")
Keys.onSpacePressed: Qt.openUrlExternally("trash:/")
KeyboardHighlight {
anchors.fill: parent
visible: trashButton.activeFocus
}
MobileShell.MotionStateLayer {
anchors.fill: parent
anchors.margins: root.dockItemInset
radius: Kirigami.Units.cornerRadius
hovered: trashMouseArea.containsMouse
pressed: trashMouseArea.containsPress
pressedOpacity: 0.18
}
Kirigami.Icon {
anchors.centerIn: parent
width: root.dockIconSize
height: width
source: trashFilesModel.count > 0 ? "user-trash-full" : "user-trash"
active: trashMouseArea.containsMouse
isMask: true
color: Kirigami.Theme.textColor
}
PC3.ToolTip {
visible: root.activeDockToolTipItem === trashButton && trashMouseArea.containsMouse && !trashMouseArea.containsPress && !trashContextMenu.opened
text: trashFilesModel.count > 0
? i18np("Trash — 1 item", "Trash — %1 items", trashFilesModel.count)
: i18n("Trash")
}
MouseArea {
id: trashMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
root.hideDockToolTip(trashButton)
if (mouse.button === Qt.RightButton) {
trashContextMenu.open()
} else {
Qt.openUrlExternally("trash:/")
}
}
onContainsMouseChanged: {
if (containsMouse) {
root.requestDockToolTip(trashButton)
} else {
root.hideDockToolTip(trashButton)
}
}
onPressedChanged: {
if (pressed) {
root.hideDockToolTip(trashButton)
}
}
}
PC3.Menu {
id: trashContextMenu
popupType: T.Popup.Window
PC3.MenuItem {
icon.name: "folder-open"
text: i18n("Open Trash")
onTriggered: Qt.openUrlExternally("trash:/")
}
PC3.MenuItem {
icon.name: "trash-empty"
text: i18n("Empty Trash")
enabled: trashFilesModel.count > 0
onTriggered: emptyTrashDialogLoader.open()
}
}
}
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) => {
if (mouse.button === Qt.RightButton) {
if (convergenceMode) {
root.homeScreen.showDesktopContextMenu();
} else {
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 {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration }
}
// 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
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration }
}
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
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 prevDelegate = repeater.itemAt(delegate.index - 1);
if (prevDelegate) {
prevDelegate.keyboardFocus();
event.accepted = true;
} else if (root.convergenceMode) {
desktopButton.forceActiveFocus();
event.accepted = true;
}
}
break;
case Qt.Key_Right:
if (isLocationBottom) {
let nextDelegate = repeater.itemAt(delegate.index + 1);
if (nextDelegate) {
nextDelegate.keyboardFocus();
event.accepted = true;
} else if (root.convergenceMode) {
let firstTask = taskRepeater.itemAt(0);
if (firstTask) {
firstTask.forceActiveFocus();
} else {
searchButton.forceActiveFocus();
}
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: !root.convergenceMode
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: !root.convergenceMode
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.showing = true
}
}
Timer {
id: thumbnailHideTimer
interval: 300
onTriggered: {
thumbnailPopup.showing = 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
property bool showing: false
function open() { showing = true }
function close() { showing = false }
readonly property bool opened: showing
visible: showing || fadeAnim.running
opacity: showing ? 1 : 0
Behavior on opacity {
MobileShell.MotionNumberAnimation {
id: fadeAnim
type: MobileShell.Motion.EffectsFast
duration: root.shortAnimationDuration
}
}
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 win = root.Window.window
var screenLeft = win && win.screen ? win.screen.virtualX : 0
var screenRight = screenLeft + (win && win.screen ? win.screen.width : Screen.width)
// The dock window is full-width, anchored to the screen's left edge.
// targetDelegate.x is dock-local, so the global center of the icon is:
var globalCenter = screenLeft + targetDelegate.x + targetDelegate.width / 2
return Math.max(screenLeft, Math.min(screenRight - width, globalCenter - width / 2))
}
y: {
var win = root.Window.window
var screenTop = win && win.screen ? win.screen.virtualY : 0
var screenBottom = screenTop + (win && win.screen ? win.screen.height : Screen.height)
// Dock is bottom-anchored; its top edge is at screenBottom - dock window height.
var dockTop = screenBottom - (win ? win.height : root.height)
return Math.max(screenTop, dockTop - height - Kirigami.Units.smallSpacing)
}
onShowingChanged: {
if (!showing && !fadeAnim.running) {
windowIds = []
targetDelegate = null
taskIndex = -1
isGroup = false
}
}
Connections {
target: fadeAnim
function onRunningChanged() {
if (!fadeAnim.running && !thumbnailPopup.showing) {
thumbnailPopup.windowIds = []
thumbnailPopup.targetDelegate = null
thumbnailPopup.taskIndex = -1
thumbnailPopup.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
cursorShape: Qt.PointingHandCursor
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
cursorShape: Qt.PointingHandCursor
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-symbolic"
isMask: true
color: "white"
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: Qt.rgba(0, 0, 0, 0.55)
z: -1
}
}
}
PC3.Label {
width: parent.width
text: thumbEntry.childTitle
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 1
}
}
}
}
}
}
}
// Separator between pinned favourites and running tasks
Rectangle {
id: dockSpacer
visible: root.showSpacer
x: (repeater.count - root.totalItemCount / 2) * root.dockCellWidth + root.dockCenterX - width / 2
y: parent.height * 0.28
width: Math.round(Kirigami.Units.devicePixelRatio)
height: parent.height * 0.44
color: Kirigami.Theme.textColor
opacity: 0.22
}
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.showRunningTasks ? tasksModel : null
delegate: Item {
id: taskDelegate
required property int index
required property var model
activeFocusOnTab: root.convergenceMode
readonly property bool isLocationBottom: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom
readonly property string taskStorageId: root.runningTaskStorageId(taskDelegate.model)
readonly property bool isGroupParent: taskDelegate.model.IsGroupParent === true
readonly property bool dynamicTilingActive: root.convergenceMode && ShellSettings.Settings.dynamicTilingEnabled
readonly property bool showFreeGeometryActions: !taskDelegate.isGroupParent && !taskDelegate.dynamicTilingActive
readonly property bool canRequestDynamicTiling: taskDelegate.dynamicTilingActive && !taskDelegate.isGroupParent && root.taskWindowId(taskDelegate.model) !== ""
readonly property int dynamicTilingWindowStateSerial: ShellSettings.Settings.dynamicTilingWindowStateSerial
readonly property bool dynamicTilingMaximized: taskDelegate.canRequestDynamicTiling
&& taskDelegate.dynamicTilingWindowStateSerial >= 0
&& ShellSettings.Settings.isDynamicTilingWindowMaximized(root.taskWindowId(taskDelegate.model))
readonly property bool canChangeVirtualDesktops: taskDelegate.model.IsVirtualDesktopsChangeable === true
readonly property bool canChangeActivities: activityInfo.numberOfRunningActivities > 1 && !taskDelegate.isGroupParent
readonly property bool dynamicTilingAllActivities: taskDelegate.canRequestDynamicTiling && root.taskActivities(taskDelegate.model).length === 0
readonly property bool canShowActivityScopeAction: taskDelegate.canChangeActivities && (!taskDelegate.canRequestDynamicTiling || taskDelegate.dynamicTilingAllActivities)
readonly property bool canShowActivityMoveActions: taskDelegate.canChangeActivities && !taskDelegate.canRequestDynamicTiling
Accessible.role: Accessible.Button
Accessible.name: taskDelegate.model.display || ""
Accessible.onPressAction: taskDelegate.activateTask()
function activateTask() {
var winIds = taskDelegate.model.WinIdList
if (winIds && winIds.length > 1) {
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))
}
}
function requestMoveToDesktop(desktopId) {
if (taskDelegate.canRequestDynamicTiling) {
let action = root.dynamicTilingMoveToDesktopAction(desktopId)
if (action !== "") {
ShellSettings.Settings.requestDynamicTilingWindowAction(root.taskWindowId(taskDelegate.model), action)
return
}
}
tasksModel.requestVirtualDesktops(tasksModel.makeModelIndex(taskDelegate.index), [desktopId])
}
function requestMoveToNewDesktop() {
if (taskDelegate.canRequestDynamicTiling) {
ShellSettings.Settings.requestDynamicTilingWindowAction(root.taskWindowId(taskDelegate.model), "move-to-new-desktop")
return
}
tasksModel.requestNewVirtualDesktop(tasksModel.makeModelIndex(taskDelegate.index))
}
Keys.onReturnPressed: taskDelegate.activateTask()
Keys.onEnterPressed: taskDelegate.activateTask()
Keys.onSpacePressed: taskDelegate.activateTask()
Keys.onLeftPressed: {
let prev = taskRepeater.itemAt(taskDelegate.index - 1)
if (prev) { prev.forceActiveFocus(); return }
let lastFav = repeater.itemAt(repeater.count - 1)
if (lastFav) { lastFav.keyboardFocus(); return }
desktopButton.forceActiveFocus()
}
Keys.onRightPressed: {
let next = taskRepeater.itemAt(taskDelegate.index + 1)
if (next) { next.forceActiveFocus(); return }
searchButton.forceActiveFocus()
}
// Position after all favourites
property double fromCenterValue: (repeater.count + taskDelegate.index) - (root.totalItemCount / 2)
Behavior on fromCenterValue {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.SpatialDefault; duration: root.longAnimationDuration }
}
readonly property int centerPosition: (isLocationBottom ? root.dockCellWidth : root.dockCellHeight) * fromCenterValue
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
width: root.dockCellWidth
height: root.dockCellHeight
// Hover highlight background
Rectangle {
anchors.fill: parent
anchors.margins: root.dockItemInset
radius: Kirigami.Units.cornerRadius
color: root.dockItemColor(taskMouseArea.containsPress, taskMouseArea.containsMouse, taskDelegate.model.IsActive === true)
Behavior on color {
MobileShell.MotionColorAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
}
}
KeyboardHighlight {
anchors.fill: parent
visible: taskDelegate.activeFocus
}
// Task icon
Kirigami.Icon {
anchors.centerIn: parent
width: root.dockIconSize
height: width
source: taskDelegate.model.decoration
active: taskMouseArea.containsMouse
}
Rectangle {
visible: opacity > 0
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Kirigami.Units.smallSpacing
width: Kirigami.Units.smallSpacing * 3
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio))
radius: height / 2
color: Kirigami.Theme.highlightColor
opacity: taskDelegate.dynamicTilingMaximized ? 0.95 : 0
Behavior on opacity {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
}
}
DragHandler {
id: taskDragHandler
target: null
xAxis.enabled: true
yAxis.enabled: false
// 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) {
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 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) {
taskDelegate.requestMoveToDesktop(pagerDesktop)
} else if (root.taskPinCanDrop && !folio.FavouritesModel.containsApplication(root.taskPinStorageId)) {
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
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing / 2
Repeater {
model: {
var ids = taskDelegate.model.WinIdList
return Math.max(1, ids ? ids.length : 0)
}
Rectangle {
width: taskDelegate.model.IsActive === true ? Kirigami.Units.smallSpacing * 3 : Kirigami.Units.smallSpacing * 1.5
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio))
radius: height / 2
color: Kirigami.Theme.highlightColor
opacity: taskDelegate.model.IsActive === true ? 1.0 : 0.45
Behavior on width {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration }
}
}
}
}
// Click to activate, middle-click to close, hover for thumbnail preview
MouseArea {
id: taskMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: root.convergenceMode ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.MiddleButton) {
thumbnailPopup.close()
tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index));
return
}
if (mouse.button === Qt.RightButton) {
thumbnailPopup.close()
thumbnailShowTimer.stop()
taskContextMenu.open();
} else {
taskDelegate.activateTask()
}
}
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
PC3.MenuItem {
icon.name: "window-new"
text: i18n("Open New Window")
visible: taskDelegate.model.CanLaunchNewInstance === true
height: visible ? implicitHeight : 0
onClicked: tasksModel.requestNewInstance(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem {
icon.name: "window-pin"
text: i18n("Pin to Dock")
// repeater.count dependency forces re-evaluation when favourites change
visible: taskDelegate.taskStorageId !== "" && repeater.count >= 0 && !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId)
height: visible ? implicitHeight : 0
enabled: !folio.FolioSettings.lockLayout
onClicked: folio.FavouritesModel.addApplication(taskDelegate.taskStorageId)
}
Controls.MenuSeparator {
visible: taskDelegate.model.CanLaunchNewInstance === true
|| (taskDelegate.taskStorageId !== "" && repeater.count >= 0 && !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId))
height: visible ? implicitHeight : 0
}
Controls.MenuSeparator {
visible: taskDelegate.canRequestDynamicTiling
height: visible ? implicitHeight : 0
}
PC3.MenuItem {
icon.name: "window"
text: i18n("Float Window")
visible: taskDelegate.canRequestDynamicTiling
height: visible ? implicitHeight : 0
onClicked: ShellSettings.Settings.requestDynamicTilingWindowAction(root.taskWindowId(taskDelegate.model), "float")
}
PC3.MenuItem {
icon.name: "view-grid-symbolic"
text: i18n("Tile Window")
visible: taskDelegate.canRequestDynamicTiling
height: visible ? implicitHeight : 0
onClicked: ShellSettings.Settings.requestDynamicTilingWindowAction(root.taskWindowId(taskDelegate.model), "tile")
}
PC3.MenuItem {
icon.name: taskDelegate.dynamicTilingMaximized ? "window-restore" : "window-maximize"
text: taskDelegate.dynamicTilingMaximized ? i18n("Restore") : i18n("Maximize")
visible: taskDelegate.canRequestDynamicTiling
height: visible ? implicitHeight : 0
enabled: taskDelegate.model.IsMaximizable === true
onClicked: ShellSettings.Settings.requestDynamicTilingWindowAction(root.taskWindowId(taskDelegate.model), "maximize-toggle")
}
PC3.MenuItem {
icon.name: "transform-move"
text: i18n("Move")
visible: taskDelegate.showFreeGeometryActions
height: visible ? implicitHeight : 0
enabled: taskDelegate.model.IsMovable === true
onClicked: tasksModel.requestMove(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem {
icon.name: "transform-scale"
text: i18n("Resize")
visible: taskDelegate.showFreeGeometryActions
height: visible ? implicitHeight : 0
enabled: taskDelegate.model.IsResizable === true
onClicked: tasksModel.requestResize(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem {
icon.name: taskDelegate.model.IsMinimized ? "window-restore" : "window-minimize"
text: taskDelegate.model.IsMinimized ? i18n("Restore") : i18n("Minimize")
enabled: taskDelegate.model.IsMinimizable === true
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.showFreeGeometryActions
height: visible ? implicitHeight : 0
enabled: taskDelegate.model.IsMaximizable === true
onClicked: tasksModel.requestToggleMaximized(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem {
icon.name: "window-keep-above"
text: taskDelegate.model.IsKeepAbove ? i18n("Do Not Keep Above Others") : i18n("Keep Above Others")
visible: taskDelegate.showFreeGeometryActions
height: visible ? implicitHeight : 0
onClicked: tasksModel.requestToggleKeepAbove(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem {
icon.name: "window-keep-below"
text: taskDelegate.model.IsKeepBelow ? i18n("Do Not Keep Below Others") : i18n("Keep Below Others")
visible: taskDelegate.showFreeGeometryActions
height: visible ? implicitHeight : 0
onClicked: tasksModel.requestToggleKeepBelow(tasksModel.makeModelIndex(taskDelegate.index))
}
PC3.MenuItem {
icon.name: "view-fullscreen"
text: taskDelegate.model.IsFullScreen ? i18n("Leave Fullscreen") : i18n("Fullscreen")
visible: taskDelegate.showFreeGeometryActions
height: visible ? implicitHeight : 0
enabled: taskDelegate.model.IsFullScreenable === true
onClicked: tasksModel.requestToggleFullScreen(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")
}
enabled: taskDelegate.model.IsClosable === true
onClicked: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index))
}
Controls.MenuSeparator {
visible: taskDelegate.canChangeVirtualDesktops
height: visible ? implicitHeight : 0
}
PC3.MenuItem {
icon.name: "virtual-desktops"
text: taskDelegate.model.IsOnAllVirtualDesktops ? i18n("Show Only on Current Desktop") : i18n("Show on All Desktops")
visible: taskDelegate.canChangeVirtualDesktops
&& virtualDesktopInfo.numberOfDesktops > 1
&& (!taskDelegate.dynamicTilingActive || taskDelegate.model.IsOnAllVirtualDesktops === true)
height: visible ? implicitHeight : 0
onClicked: {
if (taskDelegate.dynamicTilingActive && taskDelegate.model.IsOnAllVirtualDesktops === true) {
taskDelegate.requestMoveToDesktop(virtualDesktopInfo.currentDesktop)
return
}
tasksModel.requestVirtualDesktops(tasksModel.makeModelIndex(taskDelegate.index),
taskDelegate.model.IsOnAllVirtualDesktops ? [virtualDesktopInfo.currentDesktop] : [])
}
}
Instantiator {
model: root.showPager && taskDelegate.canChangeVirtualDesktops ? root.menuDesktopIds(taskDelegate.model.IsOnAllVirtualDesktops === true) : []
delegate: PC3.MenuItem {
required property var modelData
text: i18n("Move to %1", root.pagerDesktopNameForId(modelData))
onTriggered: taskDelegate.requestMoveToDesktop(modelData)
}
onObjectAdded: (idx, obj) => taskContextMenu.insertItem(taskContextMenu.count, obj)
onObjectRemoved: (idx, obj) => taskContextMenu.removeItem(obj)
}
PC3.MenuItem {
icon.name: "list-add"
text: i18n("Move to New Desktop")
visible: taskDelegate.canChangeVirtualDesktops
height: visible ? implicitHeight : 0
onClicked: taskDelegate.requestMoveToNewDesktop()
}
Controls.MenuSeparator {
visible: taskDelegate.canShowActivityScopeAction || taskDelegate.canShowActivityMoveActions
height: visible ? implicitHeight : 0
}
PC3.MenuItem {
icon.name: "activities"
text: root.taskActivities(taskDelegate.model).length === 0 ? i18n("Show Only on Current Activity") : i18n("Show on All Activities")
visible: taskDelegate.canShowActivityScopeAction
height: visible ? implicitHeight : 0
onClicked: tasksModel.requestActivities(tasksModel.makeModelIndex(taskDelegate.index),
root.taskActivities(taskDelegate.model).length === 0 ? [activityInfo.currentActivity] : [])
}
Instantiator {
model: taskDelegate.canShowActivityMoveActions ? root.menuActivityIds(root.taskActivities(taskDelegate.model)) : []
delegate: PC3.MenuItem {
required property var modelData
icon.name: activityInfo.activityIcon(modelData)
text: i18n("Move to %1", root.activityName(modelData))
onTriggered: tasksModel.requestActivities(
tasksModel.makeModelIndex(taskDelegate.index), [modelData])
}
onObjectAdded: (idx, obj) => taskContextMenu.insertItem(taskContextMenu.count, obj)
onObjectRemoved: (idx, obj) => taskContextMenu.removeItem(obj)
}
}
}
}
}