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