// SPDX-FileCopyrightText: 2026 Marco Allegretti // SPDX-License-Identifier: EUPL-1.2 import QtQuick import QtQuick.Layouts import org.kde.kirigami as Kirigami import org.kde.plasma.components 3.0 as PC3 import org.kde.plasma.private.mobileshell as MobileShell 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 Item { id: root required property var folio readonly property bool hasTasks: allTasksModel.count > 0 readonly property int shortAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast) property bool sortByName: false property int dragTargetDesktopIndex: -1 property string pendingMoveTaskKey: "" property string pendingMoveTargetName: "" signal taskActivated() function taskStorageId(taskModel) { var id = taskModel ? taskModel.AppId || "" : "" if (id && !id.endsWith(".desktop")) { id += ".desktop" } return id } function taskKey(taskModel) { const winIds = taskModel && taskModel.WinIdList ? taskModel.WinIdList : [] if (winIds.length > 0) { var key = "" for (var i = 0; i < winIds.length; ++i) { key += String(winIds[i]) + "|" } return key } return String(taskModel ? taskModel.AppId || "" : "") + "|" + String(taskModel ? taskModel.display || "" : "") } function taskWindowId(taskModel) { const winIds = taskModel && taskModel.WinIdList ? taskModel.WinIdList : [] return winIds.length === 1 ? String(winIds[0]) : "" } function dynamicTilingMoveToDesktopAction(desktopId, desktopIndex) { if (String(desktopId).length === 0 || desktopIndex < 0) { return "" } return "move-to-desktop:" + String(desktopId) + "|" + String(desktopIndex + 1) } function markTaskMove(taskKey, desktopIndex) { pendingMoveTaskKey = taskKey pendingMoveTargetName = desktopName(desktopIndex) pendingMoveResetTimer.restart() } function mixColor(base, overlay, ratio) { return Qt.rgba( base.r + (overlay.r - base.r) * ratio, base.g + (overlay.g - base.g) * ratio, base.b + (overlay.b - base.b) * ratio, base.a + (overlay.a - base.a) * ratio) } function desktopName(index) { const names = virtualDesktopInfo.desktopNames if (names && names.length > index && String(names[index]).length > 0) { return String(names[index]) } return "Desktop " + (index + 1) } function isCurrentDesktop(desktopId) { return String(desktopId) === String(virtualDesktopInfo.currentDesktop) } Timer { id: pendingMoveResetTimer interval: 1200 onTriggered: { root.pendingMoveTaskKey = "" root.pendingMoveTargetName = "" } } TaskManager.VirtualDesktopInfo { id: virtualDesktopInfo } TaskManager.ActivityInfo { id: activityInfo } TaskManager.TasksModel { id: allTasksModel filterByVirtualDesktop: false filterByActivity: true filterNotMaximized: false filterByScreen: true filterHidden: false activity: activityInfo.currentActivity groupMode: TaskManager.TasksModel.GroupApplications } TaskManager.TasksModel { id: tasksModel filterByVirtualDesktop: true filterByActivity: true filterNotMaximized: false filterByScreen: true filterHidden: false virtualDesktop: virtualDesktopInfo.currentDesktop activity: activityInfo.currentActivity groupMode: TaskManager.TasksModel.GroupApplications sortMode: root.sortByName ? TaskManager.TasksModel.SortAlpha : TaskManager.TasksModel.SortLastActivated } Rectangle { id: panelShadow anchors.fill: panelBackground anchors.topMargin: 2 radius: panelBackground.radius color: Qt.rgba(0, 0, 0, 0.35) } Rectangle { id: panelBackground anchors.fill: parent radius: Kirigami.Units.cornerRadius color: Kirigami.Theme.backgroundColor border.width: 1 border.pixelAligned: false border.color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.14) } MouseArea { anchors.fill: parent } ColumnLayout { anchors.fill: parent anchors.margins: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing RowLayout { Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing PC3.Label { Layout.fillWidth: true text: i18n("Running") font.weight: Font.Medium elide: Text.ElideRight } Row { spacing: 1 Repeater { model: [ { label: i18n("Recent"), byName: false }, { label: i18n("Name"), byName: true } ] delegate: MouseArea { id: sortButton required property var modelData readonly property bool checked: root.sortByName === modelData.byName width: Math.max(Kirigami.Units.gridUnit * 3.5, label.implicitWidth + Kirigami.Units.smallSpacing * 3) height: Kirigami.Units.gridUnit * 1.6 hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: root.sortByName = modelData.byName Rectangle { anchors.fill: parent radius: Kirigami.Units.cornerRadius color: sortButton.checked ? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, sortButton.containsMouse ? 0.28 : 0.2) : sortButton.containsMouse ? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.08) : "transparent" } PC3.Label { id: label anchors.centerIn: parent text: sortButton.modelData.label font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85 color: sortButton.checked ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor } } } } } RowLayout { id: desktopStrip Layout.fillWidth: true visible: virtualDesktopInfo.numberOfDesktops > 1 spacing: Kirigami.Units.smallSpacing PC3.Label { text: i18n("Desktops") opacity: 0.7 font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85 } Item { id: desktopDropSurface Layout.fillWidth: true Layout.preferredHeight: Kirigami.Units.gridUnit * 2.4 function desktopIndexAt(x) { if (virtualDesktopInfo.numberOfDesktops <= 0) { return -1 } const localX = desktopRow.mapFromItem(desktopDropSurface, x, 0).x var nearestIndex = -1 var nearestDistance = Number.MAX_VALUE for (var i = 0; i < virtualDesktopInfo.numberOfDesktops; ++i) { const item = desktopRepeater.itemAt(i) if (!item) { continue } if (localX >= item.x && localX <= item.x + item.width) { return i } const center = item.x + item.width / 2 const distance = Math.abs(localX - center) if (distance < nearestDistance) { nearestIndex = i nearestDistance = distance } } return nearestIndex } Row { id: desktopRow anchors.fill: parent spacing: Kirigami.Units.smallSpacing Repeater { id: desktopRepeater model: virtualDesktopInfo.desktopIds delegate: MouseArea { id: desktopButton required property int index required property var modelData readonly property bool checked: root.isCurrentDesktop(modelData) readonly property string desktopLabel: root.desktopName(index) readonly property bool dragHovered: desktopDropArea.containsDrag && root.dragTargetDesktopIndex === index width: Math.max(Kirigami.Units.gridUnit * 5.5, (desktopRow.width / Math.max(1, virtualDesktopInfo.numberOfDesktops)) - Kirigami.Units.smallSpacing) height: desktopRow.height hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: root.folio.activateVirtualDesktop(modelData) Rectangle { anchors.fill: parent radius: Kirigami.Units.cornerRadius scale: desktopButton.dragHovered ? 1.03 : 1 color: desktopButton.checked ? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, desktopButton.containsMouse || desktopButton.dragHovered ? 0.32 : 0.24) : desktopButton.dragHovered ? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.18) : desktopButton.containsMouse ? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.08) : root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.045) border.width: 1 border.pixelAligned: false border.color: desktopButton.checked || desktopButton.dragHovered ? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.55) : root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.14) Behavior on color { MobileShell.MotionColorAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration } } Behavior on scale { MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration } } } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: Kirigami.Units.smallSpacing / 2 height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio)) radius: height / 2 visible: desktopButton.checked color: Kirigami.Theme.highlightColor } PC3.Label { anchors.centerIn: parent width: parent.width - Kirigami.Units.smallSpacing * 2 text: desktopButton.desktopLabel elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter font.weight: desktopButton.checked || desktopButton.dragHovered ? Font.Medium : Font.Normal font.pixelSize: Math.min(Kirigami.Theme.defaultFont.pixelSize, parent.height * 0.42) color: desktopButton.checked || desktopButton.dragHovered ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor } } } } DropArea { id: desktopDropArea anchors.fill: parent keys: ["folio-running-task"] onEntered: (drag) => { root.dragTargetDesktopIndex = desktopDropSurface.desktopIndexAt(drag.x) drag.accept(Qt.MoveAction) } onPositionChanged: (drag) => { root.dragTargetDesktopIndex = desktopDropSurface.desktopIndexAt(drag.x) drag.accept(Qt.MoveAction) } onExited: root.dragTargetDesktopIndex = -1 onDropped: (drop) => { const desktopIndex = desktopDropSurface.desktopIndexAt(drop.x) const desktopId = desktopIndex >= 0 ? virtualDesktopInfo.desktopIds[desktopIndex] : "" if (!drop.source || !drop.source.moveToDesktop || String(desktopId).length === 0) { root.dragTargetDesktopIndex = -1 return } drop.source.moveToDesktop(desktopId, desktopIndex) root.dragTargetDesktopIndex = -1 drop.accept(Qt.MoveAction) } } } } GridView { id: taskGrid Layout.fillWidth: true Layout.fillHeight: true clip: true model: tasksModel boundsBehavior: Flickable.StopAtBounds interactive: contentHeight > height readonly property int columns: Math.max(1, Math.floor(width / (Kirigami.Units.gridUnit * 14))) cellWidth: Math.floor(width / columns) cellHeight: Kirigami.Units.gridUnit * 10 delegate: Item { id: taskCard required property int index required property var model width: taskGrid.cellWidth - Kirigami.Units.smallSpacing height: taskGrid.cellHeight - Kirigami.Units.smallSpacing readonly property var modelIndex: tasksModel.makeModelIndex(index) readonly property var winIds: model.WinIdList ? model.WinIdList : [] readonly property int previewCount: Math.max(1, Math.min(2, winIds.length)) readonly property bool activeTask: model.IsActive === true readonly property bool minimizedTask: model.IsMinimized === true readonly property bool groupTask: model.IsGroupParent === true readonly property bool desktopsChangeable: model.IsVirtualDesktopsChangeable === true readonly property string storageId: root.taskStorageId(model) readonly property string taskKey: root.taskKey(model) readonly property string windowId: root.taskWindowId(model) readonly property bool dynamicTilingActive: ShellSettings.Settings.convergenceModeEnabled && ShellSettings.Settings.dynamicTilingEnabled readonly property bool canRequestDynamicTiling: dynamicTilingActive && !groupTask && windowId !== "" readonly property int dynamicTilingWindowStateSerial: ShellSettings.Settings.dynamicTilingWindowStateSerial readonly property bool dynamicTilingMaximized: canRequestDynamicTiling && dynamicTilingWindowStateSerial >= 0 && ShellSettings.Settings.isDynamicTilingWindowMaximized(windowId) readonly property bool maximizedTask: model.IsMaximized === true || dynamicTilingMaximized readonly property bool pinned: storageId !== "" && root.folio.FavouritesModel.containsApplication(storageId) readonly property bool pendingMove: root.pendingMoveTaskKey === taskKey function taskIndexForPreview(previewIndex) { return taskCard.groupTask ? tasksModel.makeModelIndex(taskCard.index, previewIndex) : taskCard.modelIndex } function titleForPreview(previewIndex) { if (!taskCard.groupTask) { return taskCard.model.display || "" } return tasksModel.data(tasksModel.makeModelIndex(taskCard.index, previewIndex), 0) || taskCard.model.display || "" } function activate(previewIndex) { tasksModel.requestActivate(taskIndexForPreview(previewIndex || 0)) root.taskActivated() } function moveToDesktop(desktopId, desktopIndex) { if (!taskCard.desktopsChangeable || String(desktopId).length === 0) { return } root.markTaskMove(taskCard.taskKey, desktopIndex) if (taskCard.canRequestDynamicTiling) { const action = root.dynamicTilingMoveToDesktopAction(desktopId, desktopIndex) if (action !== "") { ShellSettings.Settings.requestDynamicTilingWindowAction(taskCard.windowId, action) return } } tasksModel.requestVirtualDesktops(taskCard.modelIndex, [desktopId]) } Item { id: dragProxy parent: root width: taskCard.width height: taskCard.height z: 1000 visible: cardArea.drag.active opacity: 0.9 Drag.active: cardArea.drag.active Drag.hotSpot.x: cardArea.pressX Drag.hotSpot.y: cardArea.pressY Drag.keys: ["folio-running-task"] Drag.proposedAction: Qt.MoveAction Drag.source: taskCard Drag.supportedActions: Qt.MoveAction Rectangle { anchors.fill: parent radius: Kirigami.Units.cornerRadius color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.2) border.width: 1 border.pixelAligned: false border.color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.6) } RowLayout { anchors.fill: parent anchors.margins: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing Kirigami.Icon { Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium Layout.preferredHeight: Layout.preferredWidth source: taskCard.model.decoration } PC3.Label { Layout.fillWidth: true text: taskCard.model.display || "" elide: Text.ElideRight maximumLineCount: 1 } } } MouseArea { id: cardArea anchors.fill: parent hoverEnabled: true cursorShape: taskCard.desktopsChangeable ? Qt.OpenHandCursor : Qt.PointingHandCursor enabled: !taskCard.pendingMove property real pressX: width / 2 property real pressY: height / 2 property bool wasDragged: false drag.target: taskCard.desktopsChangeable ? dragProxy : undefined drag.threshold: Math.max(4, Kirigami.Units.smallSpacing) drag.smoothed: false onPressed: (mouse) => { wasDragged = false pressX = mouse.x pressY = mouse.y const pos = taskCard.mapToItem(root, 0, 0) dragProxy.x = pos.x dragProxy.y = pos.y } onPositionChanged: { if (drag.active) { wasDragged = true } } onReleased: { if (wasDragged) { dragProxy.Drag.drop() } } onClicked: { if (!wasDragged) { taskCard.activate(0) } } } Rectangle { anchors.fill: parent radius: Kirigami.Units.cornerRadius color: taskCard.activeTask ? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, cardArea.containsMouse ? 0.18 : 0.12) : cardArea.containsMouse ? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.08) : root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.04) border.width: 1 border.pixelAligned: false border.color: taskCard.activeTask ? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.5) : root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.12) Behavior on color { MobileShell.MotionColorAnimation { type: MobileShell.Motion.EffectsFast; duration: root.shortAnimationDuration } } } ColumnLayout { anchors.fill: parent anchors.margins: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing Row { id: previewRow Layout.fillWidth: true Layout.preferredHeight: Kirigami.Units.gridUnit * 5 spacing: Kirigami.Units.smallSpacing Repeater { model: taskCard.previewCount delegate: MouseArea { id: previewArea required property int index readonly property string childUuid: taskCard.winIds.length > index ? taskCard.winIds[index] : "" width: (previewRow.width - previewRow.spacing * (taskCard.previewCount - 1)) / taskCard.previewCount height: previewRow.height hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: taskCard.activate(index) Rectangle { anchors.fill: parent radius: Kirigami.Units.cornerRadius color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, previewArea.containsMouse ? 0.1 : 0.06) border.width: 1 border.pixelAligned: false border.color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.14) } Loader { id: thumbnailLoader anchors.fill: parent anchors.margins: 1 active: previewArea.childUuid !== "" && root.visible sourceComponent: PipeWireThumbnail { windowUuid: previewArea.childUuid } } Kirigami.Icon { anchors.centerIn: parent width: Kirigami.Units.iconSizes.large height: width source: taskCard.model.decoration visible: !thumbnailLoader.item || !thumbnailLoader.item.hasThumbnail } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: titleLabel.implicitHeight + Kirigami.Units.smallSpacing radius: Kirigami.Units.cornerRadius color: Qt.rgba(0, 0, 0, 0.48) visible: taskCard.previewCount > 1 PC3.Label { id: titleLabel anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Kirigami.Units.smallSpacing text: taskCard.titleForPreview(previewArea.index) elide: Text.ElideRight maximumLineCount: 1 font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75 } } } } } RowLayout { Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing Kirigami.Icon { Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium Layout.preferredHeight: Layout.preferredWidth source: taskCard.model.decoration } ColumnLayout { Layout.fillWidth: true spacing: 0 PC3.Label { Layout.fillWidth: true text: taskCard.model.display || "" font.weight: taskCard.activeTask ? Font.Medium : Font.Normal elide: Text.ElideRight maximumLineCount: 1 } Row { spacing: Kirigami.Units.smallSpacing PC3.Label { text: taskCard.activeTask ? i18n("Active") : taskCard.minimizedTask ? i18n("Minimized") : i18n("Open") opacity: 0.65 font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75 } PC3.Label { visible: taskCard.maximizedTask text: i18n("Maximized") opacity: 0.65 font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75 } PC3.Label { visible: taskCard.winIds.length > 1 text: i18np("%1 window", "%1 windows", taskCard.winIds.length) opacity: 0.65 font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75 } } } } RowLayout { Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing Item { Layout.fillWidth: true } RunningAppsPanelButton { iconName: taskCard.pinned ? "emblem-favorite" : "window-pin" toolTipText: taskCard.pinned ? i18n("Pinned") : i18n("Pin to Dock") checked: taskCard.pinned enabled: taskCard.storageId !== "" && !taskCard.pinned && !root.folio.FolioSettings.lockLayout onTriggered: root.folio.FavouritesModel.addApplication(taskCard.storageId) } RunningAppsPanelButton { iconName: taskCard.minimizedTask ? "window-restore" : "window-minimize" toolTipText: taskCard.minimizedTask ? i18n("Restore") : i18n("Minimize") onTriggered: tasksModel.requestToggleMinimized(taskCard.modelIndex) } RunningAppsPanelButton { iconName: taskCard.maximizedTask ? "window-restore" : "window-maximize" toolTipText: taskCard.maximizedTask ? i18n("Restore") : i18n("Maximize") enabled: !taskCard.groupTask onTriggered: { if (taskCard.canRequestDynamicTiling) { ShellSettings.Settings.requestDynamicTilingWindowAction(taskCard.windowId, "maximize-toggle") return } tasksModel.requestToggleMaximized(taskCard.modelIndex) } } RunningAppsPanelButton { iconName: "window-close" toolTipText: taskCard.winIds.length > 1 ? i18n("Close All") : i18n("Close") onTriggered: tasksModel.requestClose(taskCard.modelIndex) } } } Rectangle { anchors.fill: parent radius: Kirigami.Units.cornerRadius visible: taskCard.pendingMove color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.18) border.width: 1 border.pixelAligned: false border.color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.55) PC3.Label { anchors.centerIn: parent width: parent.width - Kirigami.Units.gridUnit text: i18n("Moving to %1", root.pendingMoveTargetName) horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight font.weight: Font.Medium color: Kirigami.Theme.highlightColor } } } PC3.ScrollBar.vertical: PC3.ScrollBar { interactive: true enabled: taskGrid.contentHeight > taskGrid.height implicitWidth: Kirigami.Units.smallSpacing } PC3.Label { anchors.centerIn: parent width: parent.width - Kirigami.Units.gridUnit * 2 visible: taskGrid.count === 0 text: i18n("No windows on this desktop") horizontalAlignment: Text.AlignHCenter opacity: 0.65 wrapMode: Text.WordWrap } } } }