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.
This commit is contained in:
Marco Allegretti 2026-04-11 09:42:21 +02:00
parent eb33b533c6
commit 8c56409f1c
7 changed files with 101 additions and 14 deletions

View file

@ -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

View file

@ -4,6 +4,7 @@
#include "favouritesmodel.h"
#include "homescreenstate.h"
#include <KService>
#include <QByteArray>
#include <QDebug>
#include <QJsonArray>
@ -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<FolioApplication>(service);
auto delegate = std::make_shared<FolioDelegate>(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) {

View file

@ -42,6 +42,8 @@ public:
QHash<int, QByteArray> 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<FolioDelegate> delegate);
bool addEntry(int row, std::shared_ptr<FolioDelegate> delegate);

View file

@ -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))
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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<Kirigami.Action> 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