From 8c56409f1c593b05b792f8f883f87d0853355896 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Sat, 11 Apr 2026 09:42:21 +0200 Subject: [PATCH] Add pin-to-dock for running apps in convergence mode Right-click a running task icon to pin the app to the dock favourites bar. Pinned apps show a "Remove from Dock" action. The dock overlay renders as a LayerTop window so context menus and the dock itself stay above application windows. Menus use popupType Window to avoid clipping inside the narrow dock surface. --- CHANGELOG.md | 2 + .../homescreens/folio/favouritesmodel.cpp | 30 +++++++++++++ .../homescreens/folio/favouritesmodel.h | 2 + .../homescreens/folio/qml/FavouritesBar.qml | 42 +++++++++++++------ .../homescreens/folio/qml/FolioHomeScreen.qml | 3 +- containments/homescreens/folio/qml/main.qml | 29 +++++++++++++ .../folio/qml/private/ContextMenuLoader.qml | 7 +++- 7 files changed, 101 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08936df2..f6808f98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ Key changes so far: - **Desktop niceties** — right-click wallpaper settings, minimize-all on home press, clickable page indicators, action drawer toggle on click. - **Thumbnail previews** on dock icon hover via PipeWire screencasting. +- **Pin to dock** — right-click a running app to pin it to favourites; + pinned apps get a "Remove from Dock" action. - **Bug fixes** — minimized windows stay in dock instead of vanishing, app drawer dismiss overlay no longer covers dock icons, launching an app in convergence mode always starts a new process instead of diff --git a/containments/homescreens/folio/favouritesmodel.cpp b/containments/homescreens/folio/favouritesmodel.cpp index 955fd140..7c3f8bc2 100644 --- a/containments/homescreens/folio/favouritesmodel.cpp +++ b/containments/homescreens/folio/favouritesmodel.cpp @@ -4,6 +4,7 @@ #include "favouritesmodel.h" #include "homescreenstate.h" +#include #include #include #include @@ -76,6 +77,35 @@ void FavouritesModel::removeEntry(int row) save(); } +bool FavouritesModel::addApplication(const QString &storageId) +{ + if (containsApplication(storageId)) { + return false; + } + + KService::Ptr service = KService::serviceByStorageId(storageId); + if (!service) { + return false; + } + + auto app = std::make_shared(service); + auto delegate = std::make_shared(app); + return addEntry(m_delegates.size(), delegate); +} + +bool FavouritesModel::containsApplication(const QString &storageId) const +{ + for (const auto &entry : m_delegates) { + if (entry.delegate && entry.delegate->type() == FolioDelegate::Application) { + auto app = entry.delegate->application(); + if (app && app->storageId() == storageId) { + return true; + } + } + } + return false; +} + void FavouritesModel::moveEntry(int fromRow, int toRow) { if (fromRow < 0 || toRow < 0 || fromRow >= m_delegates.size() || toRow >= m_delegates.size() || fromRow == toRow) { diff --git a/containments/homescreens/folio/favouritesmodel.h b/containments/homescreens/folio/favouritesmodel.h index d1dbeb56..af94bda4 100644 --- a/containments/homescreens/folio/favouritesmodel.h +++ b/containments/homescreens/folio/favouritesmodel.h @@ -42,6 +42,8 @@ public: QHash roleNames() const override; Q_INVOKABLE void removeEntry(int row); + Q_INVOKABLE bool addApplication(const QString &storageId); + Q_INVOKABLE bool containsApplication(const QString &storageId) const; void moveEntry(int fromRow, int toRow); bool canAddEntry(int row, std::shared_ptr delegate); bool addEntry(int row, std::shared_ptr delegate); diff --git a/containments/homescreens/folio/qml/FavouritesBar.qml b/containments/homescreens/folio/qml/FavouritesBar.qml index 1b3a8a7a..056a344c 100644 --- a/containments/homescreens/folio/qml/FavouritesBar.qml +++ b/containments/homescreens/folio/qml/FavouritesBar.qml @@ -13,6 +13,7 @@ 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" @@ -345,6 +346,7 @@ MouseArea { ContextMenuLoader { id: contextMenu + menuPopupType: root.convergenceMode ? T.Popup.Window : T.Popup.Item // close menu when drag starts Connections { @@ -425,6 +427,7 @@ MouseArea { ContextMenuLoader { id: contextMenu + menuPopupType: root.convergenceMode ? T.Popup.Window : T.Popup.Item // close menu when drag starts Connections { @@ -661,7 +664,7 @@ MouseArea { if (mouse.button === Qt.RightButton) { thumbnailPopup.close() thumbnailShowTimer.stop() - taskContextMenu.popup(); + taskContextMenu.open(); } else { thumbnailPopup.close() tasksModel.requestActivate(tasksModel.makeModelIndex(taskDelegate.index)); @@ -688,23 +691,38 @@ MouseArea { } } - Controls.Menu { + PC3.Menu { id: taskContextMenu - Controls.MenuItem { - text: taskDelegate.model.IsMinimized ? i18n("Restore") : i18n("Minimize") + 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") + visible: taskContextMenu.taskStorageId !== "" && !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" - onTriggered: tasksModel.requestToggleMinimized(tasksModel.makeModelIndex(taskDelegate.index)) + text: taskDelegate.model.IsMinimized ? i18n("Restore") : i18n("Minimize") + onClicked: tasksModel.requestToggleMinimized(tasksModel.makeModelIndex(taskDelegate.index)) } - Controls.MenuItem { - text: taskDelegate.model.IsMaximized ? i18n("Restore") : i18n("Maximize") + PC3.MenuItem { icon.name: taskDelegate.model.IsMaximized ? "window-restore" : "window-maximize" - onTriggered: tasksModel.requestToggleMaximized(tasksModel.makeModelIndex(taskDelegate.index)) + text: taskDelegate.model.IsMaximized ? i18n("Restore") : i18n("Maximize") + onClicked: tasksModel.requestToggleMaximized(tasksModel.makeModelIndex(taskDelegate.index)) } - Controls.MenuSeparator {} - Controls.MenuItem { - text: i18n("Close") + PC3.MenuItem { icon.name: "window-close" - onTriggered: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index)) + text: i18n("Close") + onClicked: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index)) } } } diff --git a/containments/homescreens/folio/qml/FolioHomeScreen.qml b/containments/homescreens/folio/qml/FolioHomeScreen.qml index 10063e14..01ae9804 100644 --- a/containments/homescreens/folio/qml/FolioHomeScreen.qml +++ b/containments/homescreens/folio/qml/FolioHomeScreen.qml @@ -348,7 +348,8 @@ Item { // don't show in settings mode opacity: 1 - folio.HomeScreenState.settingsOpenProgress - visible: opacity > 0 + // Hidden in convergence mode — the dock overlay window renders it instead + visible: opacity > 0 && !ShellSettings.Settings.convergenceModeEnabled // one is ignored as anchors are set height: ShellSettings.Settings.convergenceModeEnabled ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 6 diff --git a/containments/homescreens/folio/qml/main.qml b/containments/homescreens/folio/qml/main.qml index 60c20436..f20153b8 100644 --- a/containments/homescreens/folio/qml/main.qml +++ b/containments/homescreens/folio/qml/main.qml @@ -17,6 +17,8 @@ import org.kde.plasma.private.mobileshell.state as MobileShellState import org.kde.plasma.private.mobileshell.windowplugin as WindowPlugin import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings +import org.kde.layershell 1.0 as LayerShell + import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio import "./private" @@ -156,6 +158,33 @@ ContainmentItem { opacity: folio.HomeScreenState.settingsOpenProgress } + // Dock overlay window — renders the favourites bar above application + // windows in convergence mode. LayerTop sits above normal windows but + // below LayerOverlay (notifications, volume OSD). The exclusive zone + // that reserves screen space is handled by the dockSpaceReserver in the + // task panel containment; this window only provides the visible dock. + Window { + id: dockOverlay + visible: ShellSettings.Settings.convergenceModeEnabled + color: "transparent" + width: Screen.width + height: Kirigami.Units.gridUnit * 3 + + LayerShell.Window.scope: "dock-overlay" + LayerShell.Window.layer: LayerShell.Window.LayerTop + LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight + LayerShell.Window.exclusionZone: -1 + LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityOnDemand + + FavouritesBar { + id: dockOverlayBar + anchors.fill: parent + folio: root.folio + maskManager: root.maskManager + homeScreen: folioHomeScreen + } + } + MobileShell.HomeScreen { id: homeScreen anchors.fill: parent diff --git a/containments/homescreens/folio/qml/private/ContextMenuLoader.qml b/containments/homescreens/folio/qml/private/ContextMenuLoader.qml index 434f3a19..39835ddf 100644 --- a/containments/homescreens/folio/qml/private/ContextMenuLoader.qml +++ b/containments/homescreens/folio/qml/private/ContextMenuLoader.qml @@ -4,6 +4,7 @@ import QtQuick import QtQuick.Window import QtQuick.Layouts +import QtQuick.Templates as T import org.kde.plasma.components 3.0 as PC3 import org.kde.kirigami as Kirigami @@ -13,6 +14,7 @@ Loader { active: false property list actions + property int menuPopupType: T.Popup.Item function open() { root.active = true; @@ -28,7 +30,10 @@ Loader { sourceComponent: PC3.Menu { id: menu title: "Context Menu" - closePolicy: PC3.Menu.CloseOnReleaseOutside | PC3.Menu.CloseOnEscape + popupType: root.menuPopupType + closePolicy: root.menuPopupType === T.Popup.Window + ? (PC3.Menu.CloseOnPressOutside | PC3.Menu.CloseOnEscape) + : (PC3.Menu.CloseOnReleaseOutside | PC3.Menu.CloseOnEscape) Repeater { model: root.actions