mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-07-30 00:04:45 +00:00
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:
parent
eb33b533c6
commit
8c56409f1c
7 changed files with 101 additions and 14 deletions
|
|
@ -30,6 +30,8 @@ Key changes so far:
|
||||||
- **Desktop niceties** — right-click wallpaper settings, minimize-all on
|
- **Desktop niceties** — right-click wallpaper settings, minimize-all on
|
||||||
home press, clickable page indicators, action drawer toggle on click.
|
home press, clickable page indicators, action drawer toggle on click.
|
||||||
- **Thumbnail previews** on dock icon hover via PipeWire screencasting.
|
- **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,
|
- **Bug fixes** — minimized windows stay in dock instead of vanishing,
|
||||||
app drawer dismiss overlay no longer covers dock icons, launching an
|
app drawer dismiss overlay no longer covers dock icons, launching an
|
||||||
app in convergence mode always starts a new process instead of
|
app in convergence mode always starts a new process instead of
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "favouritesmodel.h"
|
#include "favouritesmodel.h"
|
||||||
#include "homescreenstate.h"
|
#include "homescreenstate.h"
|
||||||
|
|
||||||
|
#include <KService>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
|
|
@ -76,6 +77,35 @@ void FavouritesModel::removeEntry(int row)
|
||||||
save();
|
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)
|
void FavouritesModel::moveEntry(int fromRow, int toRow)
|
||||||
{
|
{
|
||||||
if (fromRow < 0 || toRow < 0 || fromRow >= m_delegates.size() || toRow >= m_delegates.size() || fromRow == toRow) {
|
if (fromRow < 0 || toRow < 0 || fromRow >= m_delegates.size() || toRow >= m_delegates.size() || fromRow == toRow) {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ public:
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
Q_INVOKABLE void removeEntry(int row);
|
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);
|
void moveEntry(int fromRow, int toRow);
|
||||||
bool canAddEntry(int row, std::shared_ptr<FolioDelegate> delegate);
|
bool canAddEntry(int row, std::shared_ptr<FolioDelegate> delegate);
|
||||||
bool addEntry(int row, std::shared_ptr<FolioDelegate> delegate);
|
bool addEntry(int row, std::shared_ptr<FolioDelegate> delegate);
|
||||||
|
|
|
||||||
|
|
@ -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.plasma.private.mobileshell as MobileShell
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
import QtQuick.Controls as Controls
|
import QtQuick.Controls as Controls
|
||||||
|
import QtQuick.Templates as T
|
||||||
|
|
||||||
import "./private"
|
import "./private"
|
||||||
import "./delegate"
|
import "./delegate"
|
||||||
|
|
@ -345,6 +346,7 @@ MouseArea {
|
||||||
|
|
||||||
ContextMenuLoader {
|
ContextMenuLoader {
|
||||||
id: contextMenu
|
id: contextMenu
|
||||||
|
menuPopupType: root.convergenceMode ? T.Popup.Window : T.Popup.Item
|
||||||
|
|
||||||
// close menu when drag starts
|
// close menu when drag starts
|
||||||
Connections {
|
Connections {
|
||||||
|
|
@ -425,6 +427,7 @@ MouseArea {
|
||||||
|
|
||||||
ContextMenuLoader {
|
ContextMenuLoader {
|
||||||
id: contextMenu
|
id: contextMenu
|
||||||
|
menuPopupType: root.convergenceMode ? T.Popup.Window : T.Popup.Item
|
||||||
|
|
||||||
// close menu when drag starts
|
// close menu when drag starts
|
||||||
Connections {
|
Connections {
|
||||||
|
|
@ -661,7 +664,7 @@ MouseArea {
|
||||||
if (mouse.button === Qt.RightButton) {
|
if (mouse.button === Qt.RightButton) {
|
||||||
thumbnailPopup.close()
|
thumbnailPopup.close()
|
||||||
thumbnailShowTimer.stop()
|
thumbnailShowTimer.stop()
|
||||||
taskContextMenu.popup();
|
taskContextMenu.open();
|
||||||
} else {
|
} else {
|
||||||
thumbnailPopup.close()
|
thumbnailPopup.close()
|
||||||
tasksModel.requestActivate(tasksModel.makeModelIndex(taskDelegate.index));
|
tasksModel.requestActivate(tasksModel.makeModelIndex(taskDelegate.index));
|
||||||
|
|
@ -688,23 +691,38 @@ MouseArea {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Controls.Menu {
|
PC3.Menu {
|
||||||
id: taskContextMenu
|
id: taskContextMenu
|
||||||
Controls.MenuItem {
|
popupType: T.Popup.Window
|
||||||
text: taskDelegate.model.IsMinimized ? i18n("Restore") : i18n("Minimize")
|
|
||||||
|
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"
|
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 {
|
PC3.MenuItem {
|
||||||
text: taskDelegate.model.IsMaximized ? i18n("Restore") : i18n("Maximize")
|
|
||||||
icon.name: taskDelegate.model.IsMaximized ? "window-restore" : "window-maximize"
|
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 {}
|
PC3.MenuItem {
|
||||||
Controls.MenuItem {
|
|
||||||
text: i18n("Close")
|
|
||||||
icon.name: "window-close"
|
icon.name: "window-close"
|
||||||
onTriggered: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index))
|
text: i18n("Close")
|
||||||
|
onClicked: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -348,7 +348,8 @@ Item {
|
||||||
|
|
||||||
// don't show in settings mode
|
// don't show in settings mode
|
||||||
opacity: 1 - folio.HomeScreenState.settingsOpenProgress
|
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
|
// one is ignored as anchors are set
|
||||||
height: ShellSettings.Settings.convergenceModeEnabled ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 6
|
height: ShellSettings.Settings.convergenceModeEnabled ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 6
|
||||||
|
|
|
||||||
|
|
@ -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.windowplugin as WindowPlugin
|
||||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
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 plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
||||||
|
|
||||||
import "./private"
|
import "./private"
|
||||||
|
|
@ -156,6 +158,33 @@ ContainmentItem {
|
||||||
opacity: folio.HomeScreenState.settingsOpenProgress
|
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 {
|
MobileShell.HomeScreen {
|
||||||
id: homeScreen
|
id: homeScreen
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Window
|
import QtQuick.Window
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Templates as T
|
||||||
|
|
||||||
import org.kde.plasma.components 3.0 as PC3
|
import org.kde.plasma.components 3.0 as PC3
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
@ -13,6 +14,7 @@ Loader {
|
||||||
active: false
|
active: false
|
||||||
|
|
||||||
property list<Kirigami.Action> actions
|
property list<Kirigami.Action> actions
|
||||||
|
property int menuPopupType: T.Popup.Item
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
root.active = true;
|
root.active = true;
|
||||||
|
|
@ -28,7 +30,10 @@ Loader {
|
||||||
sourceComponent: PC3.Menu {
|
sourceComponent: PC3.Menu {
|
||||||
id: menu
|
id: menu
|
||||||
title: "Context 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 {
|
Repeater {
|
||||||
model: root.actions
|
model: root.actions
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue