mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-06-11 00:47:22 +00:00
Improve touchpad scroll handling for convergence workflows while avoiding gesture conflicts. - make SwipeArea handle angleDelta-based touchpad wheel streams and phase-less sequences safely - wire topbar ActionDrawerOpenSurface to virtual desktop info and horizontal workspace switching logic - add dock pager wheel switching in FavouritesBar for the vertical wheel axis environments deliver - prevent wallpaper/home surface touchpad scroll from opening app drawer in convergence - extend convergence invariant checks for the new gesture paths
2181 lines
91 KiB
QML
2181 lines
91 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 ? Math.ceil(virtualDesktopInfo.numberOfDesktops / 2) : 0
|
|
readonly property int pagerRightCount: showPager ? virtualDesktopInfo.numberOfDesktops - pagerLeftCount : 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 + pagerRightCount * pagerButtonWidth : 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]) : ""
|
|
}
|
|
for (let i = 0; i < pagerRightCount; ++i) {
|
|
let bx = root.width - navButtonWidth - root.searchButtonWidth - root.trashButtonWidth - (pagerRightCount - i) * pagerButtonWidth
|
|
if (x >= bx && x < bx + pagerButtonWidth) {
|
|
let di = pagerLeftCount + i
|
|
return (ids && di < ids.length) ? String(ids[di]) : ""
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
function runningTaskStorageId(taskModel) {
|
|
var id = taskModel ? taskModel.AppId || "" : ""
|
|
if (id && !id.endsWith(".desktop"))
|
|
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: {
|
|
let first = repeater.itemAt(0)
|
|
if (first) { first.keyboardFocus(); return }
|
|
let firstTask = taskRepeater.itemAt(0)
|
|
if (firstTask) { firstTask.forceActiveFocus(); return }
|
|
searchButton.forceActiveFocus()
|
|
}
|
|
|
|
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: {
|
|
let lastTask = taskRepeater.itemAt(taskRepeater.count - 1)
|
|
if (lastTask) { lastTask.forceActiveFocus(); return }
|
|
let lastFav = repeater.itemAt(repeater.count - 1)
|
|
if (lastFav) { lastFav.keyboardFocus(); return }
|
|
desktopButton.forceActiveFocus()
|
|
}
|
|
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: left wing (desktops 1 .. ceil(N/2)) ----
|
|
Repeater {
|
|
id: leftPagerRepeater
|
|
model: root.pagerLeftCount
|
|
|
|
delegate: Item {
|
|
id: leftDesktopBtn
|
|
required property int index
|
|
|
|
readonly property string desktopId: {
|
|
let ids = virtualDesktopInfo.desktopIds
|
|
return (ids && index < ids.length) ? String(ids[index]) : ""
|
|
}
|
|
readonly property bool isCurrent: desktopId !== "" && String(desktopId) === String(virtualDesktopInfo.currentDesktop)
|
|
readonly property bool isDragTarget: {
|
|
if (root.taskPinDragIndex < 0) return false
|
|
let cx = root.taskBaseX(root.taskPinDragIndex) + root.dockCellWidth / 2 + root.taskPinDragOffset
|
|
return root.pagerButtonDesktopAt(cx) === desktopId
|
|
}
|
|
|
|
x: root.navButtonWidth + root.desktopButtonWidth + 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: 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 if (leftDesktopBtn.desktopId) {
|
|
root.folio.activateVirtualDesktop(leftDesktopBtn.desktopId)
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|