From 4077292801051de2eb3d83ffb13ae7e00ed03f85 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Wed, 29 Apr 2026 12:10:31 +0200 Subject: [PATCH] Add task panel to Folio drawer Show running windows in the app drawer with previews, window actions, and virtual desktop controls. Hide dock task icons while the panel is open so the same windows are not duplicated. Use TaskManager to move windows between desktops, and expose desktop activation through the containment. --- CMakeLists.txt | 1 + containments/homescreens/folio/CMakeLists.txt | 2 + containments/homescreens/folio/homescreen.cpp | 12 + containments/homescreens/folio/homescreen.h | 2 + .../homescreens/folio/qml/FavouritesBar.qml | 8 +- .../folio/qml/RunningAppsPanel.qml | 762 ++++++++++++++++++ containments/homescreens/folio/qml/main.qml | 21 +- 7 files changed, 803 insertions(+), 5 deletions(-) create mode 100644 containments/homescreens/folio/qml/RunningAppsPanel.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index a6c80067..2a7ffcb1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,6 +83,7 @@ find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS find_package(Plasma CONFIG REQUIRED) find_package(PlasmaQuick CONFIG REQUIRED) find_package(PlasmaActivities CONFIG REQUIRED) +find_package(LibTaskManager CONFIG REQUIRED) find_package(KF6Screen CONFIG REQUIRED) find_package(KWayland CONFIG REQUIRED) find_package(KPipeWire ${PROJECT_DEP_VERSION} REQUIRED) diff --git a/containments/homescreens/folio/CMakeLists.txt b/containments/homescreens/folio/CMakeLists.txt index c27e5a96..3841a9b2 100644 --- a/containments/homescreens/folio/CMakeLists.txt +++ b/containments/homescreens/folio/CMakeLists.txt @@ -20,6 +20,7 @@ plasma_add_applet(org.kde.plasma.mobile.homescreen.folio qml/main.qml qml/PipeWireThumbnail.qml qml/PlaceholderDelegate.qml + qml/RunningAppsPanel.qml qml/WidgetDragItem.qml qml/config.qml CPP_SOURCES @@ -95,6 +96,7 @@ target_link_libraries(org.kde.plasma.mobile.homescreen.folio PRIVATE KF6::Service KF6::KIOGui KF6::Notifications + PW::LibTaskManager Plasma::KWaylandClient KF6::WindowSystem KF6::JobWidgets diff --git a/containments/homescreens/folio/homescreen.cpp b/containments/homescreens/folio/homescreen.cpp index a7e02e3c..d770ff9e 100644 --- a/containments/homescreens/folio/homescreen.cpp +++ b/containments/homescreens/folio/homescreen.cpp @@ -4,6 +4,8 @@ #include "homescreen.h" +#include + #include #include @@ -96,4 +98,14 @@ void HomeScreen::triggerOverview() const QDBusConnection::sessionBus().send(message); } +void HomeScreen::activateVirtualDesktop(const QVariant &desktop) const +{ + if (!desktop.isValid() || desktop.toString().isEmpty()) { + return; + } + + TaskManager::VirtualDesktopInfo virtualDesktopInfo; + virtualDesktopInfo.requestActivate(desktop); +} + #include "homescreen.moc" diff --git a/containments/homescreens/folio/homescreen.h b/containments/homescreens/folio/homescreen.h index e0ac8f87..010607a1 100644 --- a/containments/homescreens/folio/homescreen.h +++ b/containments/homescreens/folio/homescreen.h @@ -6,6 +6,7 @@ #include #include +#include #include "applicationlistmodel.h" #include "delegatetoucharea.h" @@ -49,6 +50,7 @@ public: void configChanged() override; Q_INVOKABLE void triggerOverview() const; + Q_INVOKABLE void activateVirtualDesktop(const QVariant &desktop) const; FolioSettings *folioSettings(); HomeScreenState *homeScreenState(); diff --git a/containments/homescreens/folio/qml/FavouritesBar.qml b/containments/homescreens/folio/qml/FavouritesBar.qml index 9cdd3692..7fb04007 100644 --- a/containments/homescreens/folio/qml/FavouritesBar.qml +++ b/containments/homescreens/folio/qml/FavouritesBar.qml @@ -24,12 +24,14 @@ MouseArea { 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 int totalItemCount: repeater.count + (convergenceMode ? taskRepeater.count : 0) + readonly property bool showRunningTasks: convergenceMode && !suppressRunningTasks + readonly property int totalItemCount: repeater.count + (showRunningTasks ? 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 @@ -59,7 +61,7 @@ MouseArea { : root.width / 2 // Visible spacer between pinned favourites and running tasks - readonly property bool showSpacer: convergenceMode && repeater.count > 0 && taskRepeater.count > 0 + readonly property bool showSpacer: showRunningTasks && 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 } @@ -936,7 +938,7 @@ MouseArea { Repeater { id: taskRepeater - model: root.convergenceMode ? tasksModel : null + model: root.showRunningTasks ? tasksModel : null delegate: Item { id: taskDelegate diff --git a/containments/homescreens/folio/qml/RunningAppsPanel.qml b/containments/homescreens/folio/qml/RunningAppsPanel.qml new file mode 100644 index 00000000..b4c79685 --- /dev/null +++ b/containments/homescreens/folio/qml/RunningAppsPanel.qml @@ -0,0 +1,762 @@ +// 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.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 + 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 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 = "" + } + } + + component PanelIconButton: MouseArea { + id: button + + property string iconName + property string toolTipText + property bool checked: false + + signal triggered() + + width: Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.smallSpacing * 2 + height: width + hoverEnabled: enabled + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + opacity: enabled ? 1 : 0.35 + + onClicked: button.triggered() + + Rectangle { + anchors.fill: parent + radius: Kirigami.Units.cornerRadius + color: button.containsPress + ? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.16) + : button.checked + ? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, button.containsMouse ? 0.22 : 0.16) + : button.containsMouse + ? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.08) + : "transparent" + + Behavior on color { + ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + } + + Kirigami.Icon { + anchors.centerIn: parent + width: Kirigami.Units.iconSizes.small + height: width + source: button.iconName + active: button.containsMouse || button.checked + } + + PC3.ToolTip { + text: button.toolTipText + visible: button.containsMouse && button.toolTipText.length > 0 + } + } + + 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 { + ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + Behavior on scale { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + } + + 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 maximizedTask: model.IsMaximized === 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 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) + 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 { + ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + } + + 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 } + + PanelIconButton { + 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) + } + + PanelIconButton { + iconName: taskCard.minimizedTask ? "window-restore" : "window-minimize" + toolTipText: taskCard.minimizedTask ? i18n("Restore") : i18n("Minimize") + onTriggered: tasksModel.requestToggleMinimized(taskCard.modelIndex) + } + + PanelIconButton { + iconName: taskCard.maximizedTask ? "window-restore" : "window-maximize" + toolTipText: taskCard.maximizedTask ? i18n("Restore") : i18n("Maximize") + enabled: !taskCard.groupTask + onTriggered: tasksModel.requestToggleMaximized(taskCard.modelIndex) + } + + PanelIconButton { + 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 + } + } + } +} diff --git a/containments/homescreens/folio/qml/main.qml b/containments/homescreens/folio/qml/main.qml index 8bc812ae..cc3d2ed5 100644 --- a/containments/homescreens/folio/qml/main.qml +++ b/containments/homescreens/folio/qml/main.qml @@ -32,7 +32,7 @@ import "./private" ContainmentItem { id: root - property Folio.HomeScreen folio: root.plasmoid + property var folio: root.plasmoid // Tracks whether the Game Center grid is visible within gaming mode. // If gaming mode is already enabled at startup, open it immediately so @@ -365,6 +365,7 @@ ContainmentItem { folio: root.folio maskManager: root.maskManager homeScreen: folioHomeScreen + suppressRunningTasks: runningAppsPanel.visible transform: Translate { y: dockOverlay.dockOffset } // Dock is an opaque panel — use Window colorset so all content // (labels, hover highlights, icon tints) follows the system theme @@ -519,7 +520,9 @@ ContainmentItem { width: tileSize height: overlayDrawer.popupHeight - x: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing + x: runningAppsPanel.visible + ? runningAppsPanel.x + runningAppsPanel.width + Kirigami.Units.smallSpacing + : categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing y: overlayDrawer.y opacity: overlayDrawer.opacity radius: Kirigami.Units.cornerRadius @@ -739,6 +742,20 @@ ContainmentItem { } } } + + RunningAppsPanel { + id: runningAppsPanel + folio: root.folio + + x: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing + y: overlayDrawer.y + width: Math.max(0, parent.width - x - powerPanel.width - Kirigami.Units.smallSpacing * 2) + height: overlayDrawer.popupHeight + opacity: overlayDrawer.opacity + visible: hasTasks && opacity > 0 + + onTaskActivated: folio.HomeScreenState.closeAppDrawer() + } } // Game Center overlay — full-screen grid of games shown when gaming mode