plasmoides KDE Store/GitHub (gnome-pager, kMenu, kvitals, windowtitle, claude, launchpad)
This commit is contained in:
+300
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* Plasma Gnome Pager — main.qml
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2026 Kenan Salar
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Root PlasmoidItem: owns the virtual-desktop data source (read) and the KWin DBus helpers (write),
|
||||
* and renders the dot strip inline in the panel. This is the e2e boundary (not headless-testable).
|
||||
*/
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
|
||||
import org.kde.plasma.plasmoid
|
||||
|
||||
// Public, stable imports only — never org.kde.plasma.private.* (robustness.md).
|
||||
import org.kde.plasma.core as PlasmaCore // PlasmaCore.Action + PlasmaCore.Types
|
||||
import org.kde.taskmanager as TaskManager // VirtualDesktopInfo + TasksModel/ActivityInfo (read)
|
||||
import org.kde.plasma.workspace.dbus as DBus // KWin DBus (switch/add/remove/rename)
|
||||
import org.kde.kcmutils as KCM // KCMLauncher — open the system Virtual Desktops settings module
|
||||
|
||||
import "logic.js" as Logic
|
||||
|
||||
PlasmoidItem {
|
||||
id: root
|
||||
|
||||
Plasmoid.icon: "virtual-desktops" // match metadata.json's Icon (theme-safe Breeze name)
|
||||
|
||||
// Passive always-on widget, no background of its own (dots float on the panel — the GNOME look).
|
||||
Plasmoid.status: PlasmaCore.Types.PassiveStatus
|
||||
Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground
|
||||
|
||||
// Panel orientation, passed DOWN as a plain bool so the sub-components stay Plasmoid-free. Planar/Floating → horizontal.
|
||||
readonly property bool isVertical: Plasmoid.formFactor === PlasmaCore.Types.Vertical
|
||||
|
||||
// Behaviour settings, read live from main.xml. Each `?? Logic.DEFAULTS` guards the transient-undefined frame (undefined → false).
|
||||
readonly property bool enableScroll: Plasmoid.configuration.enableScroll ?? Logic.DEFAULTS.enableScroll
|
||||
readonly property bool scrollWrap: Plasmoid.configuration.scrollWrap ?? Logic.DEFAULTS.scrollWrap
|
||||
readonly property bool invertScroll: Plasmoid.configuration.invertScroll ?? Logic.DEFAULTS.invertScroll
|
||||
// Action when the current desktop's pill is clicked (default None); see Logic.PILL_CLICK_ACTION.
|
||||
readonly property int pillClickAction: Plasmoid.configuration.pillClickAction ?? Logic.DEFAULTS.pillClickAction
|
||||
readonly property bool showTooltips: Plasmoid.configuration.showTooltips ?? Logic.DEFAULTS.showTooltips
|
||||
readonly property bool showWindowList: Plasmoid.configuration.showWindowList ?? Logic.DEFAULTS.showWindowList
|
||||
readonly property bool enableAddRemove: Plasmoid.configuration.enableAddRemove ?? Logic.DEFAULTS.enableAddRemove
|
||||
readonly property bool enableRename: Plasmoid.configuration.enableRename ?? Logic.DEFAULTS.enableRename
|
||||
// Dynamic workspaces (default OFF): auto-keep one empty trailing desktop; dynamicNamePrefix is the created-desktop base name ("" = "Desktop").
|
||||
readonly property bool dynamicWorkspaces: Plasmoid.configuration.dynamicWorkspaces ?? Logic.DEFAULTS.dynamicWorkspaces
|
||||
readonly property string dynamicNamePrefix: Plasmoid.configuration.dynamicNamePrefix ?? Logic.DEFAULTS.dynamicNamePrefix
|
||||
|
||||
// Manual Add/Remove only when enabled AND dynamic workspaces off — the two conflict. Reused by the contextualActions below.
|
||||
readonly property bool canAddRemove: enableAddRemove && !dynamicWorkspaces
|
||||
|
||||
// Appearance/animation settings, read the same way. dotSize/pillSize/animationDuration use a 0 = auto sentinel resolved in the indicator/dot.
|
||||
readonly property int animationDuration: Plasmoid.configuration.animationDuration ?? Logic.DEFAULTS.animationDuration
|
||||
// Overall pager look (Sliding pill / Filled & ring — see Logic.DOT_STYLE); the indicator neutralizes the pill in ring mode.
|
||||
readonly property int dotStyle: Plasmoid.configuration.dotStyle ?? Logic.DEFAULTS.dotStyle
|
||||
// Ignore KWin's grid rows: lay every desktop out in one strip along the panel (a vertical strip on a vertical panel).
|
||||
readonly property bool singleLine: Plasmoid.configuration.singleLine ?? Logic.DEFAULTS.singleLine
|
||||
// Vertical panels: lay the grid out in KWin orientation (rows top-to-bottom) instead of transposing it down the panel.
|
||||
readonly property bool matchDesktopGrid: Plasmoid.configuration.matchDesktopGrid ?? Logic.DEFAULTS.matchDesktopGrid
|
||||
readonly property int dotSize: Plasmoid.configuration.dotSize ?? Logic.DEFAULTS.dotSize
|
||||
readonly property int pillSize: Plasmoid.configuration.pillSize ?? Logic.DEFAULTS.pillSize
|
||||
readonly property real spacingFactor: Plasmoid.configuration.spacingFactor ?? Logic.DEFAULTS.spacingFactor
|
||||
readonly property real pillWidthFactor: Plasmoid.configuration.pillWidthFactor ?? Logic.DEFAULTS.pillWidthFactor
|
||||
readonly property real inactiveOpacity: Plasmoid.configuration.inactiveOpacity ?? Logic.DEFAULTS.inactiveOpacity
|
||||
readonly property real hoverOpacity: Plasmoid.configuration.hoverOpacity ?? Logic.DEFAULTS.hoverOpacity
|
||||
// Occupied-dot indicator (default OFF): mark desktops that hold windows (reuses the occupancy snapshot below).
|
||||
// occupancyStyle picks HOW (Filled/InnerDot/Ring — see Logic.OCCUPANCY); occupiedOpacity applies to every style.
|
||||
readonly property bool showOccupancy: Plasmoid.configuration.showOccupancy ?? Logic.DEFAULTS.showOccupancy
|
||||
readonly property real occupiedOpacity: Plasmoid.configuration.occupiedOpacity ?? Logic.DEFAULTS.occupiedOpacity
|
||||
readonly property int occupancyStyle: Plasmoid.configuration.occupancyStyle ?? Logic.DEFAULTS.occupancyStyle
|
||||
readonly property bool followThemeColors: Plasmoid.configuration.followThemeColors ?? Logic.DEFAULTS.followThemeColors
|
||||
readonly property color activeColor: Plasmoid.configuration.activeColor ?? Logic.DEFAULTS.activeColor
|
||||
readonly property color inactiveColor: Plasmoid.configuration.inactiveColor ?? Logic.DEFAULTS.inactiveColor
|
||||
readonly property color occupiedColor: Plasmoid.configuration.occupiedColor ?? Logic.DEFAULTS.occupiedColor
|
||||
|
||||
// A pager renders inline. Plasma instantiates NO representation unless a fullRepresentation exists, so
|
||||
// the dot strip IS the full representation, forced inline via preferredRepresentation.
|
||||
preferredRepresentation: fullRepresentation
|
||||
fullRepresentation: WorkspaceIndicator {
|
||||
vertical: root.isVertical
|
||||
virtualDesktopInfo: vdi
|
||||
enableScroll: root.enableScroll
|
||||
scrollWrap: root.scrollWrap
|
||||
invertScroll: root.invertScroll
|
||||
showTooltips: root.showTooltips
|
||||
desktopTooltips: root.desktopTooltips
|
||||
|
||||
dotStyle: root.dotStyle
|
||||
singleLine: root.singleLine
|
||||
matchDesktopGrid: root.matchDesktopGrid
|
||||
// dotSize/pillSize passed as raw 0=auto requests; resolved in the indicator (pillSize 0 = match dots).
|
||||
dotSizeRequest: root.dotSize
|
||||
pillSizeRequest: root.pillSize
|
||||
spacingFactor: root.spacingFactor
|
||||
pillWidthFactor: root.pillWidthFactor
|
||||
inactiveOpacity: root.inactiveOpacity
|
||||
hoverOpacity: root.hoverOpacity
|
||||
showOccupancy: root.showOccupancy
|
||||
desktopOccupancy: root.screenOccupancy // PER-SCREEN: only mark dots for windows on THIS pager's monitor
|
||||
occupiedOpacity: root.occupiedOpacity
|
||||
occupancyStyle: root.occupancyStyle
|
||||
followThemeColors: root.followThemeColors
|
||||
activeColor: root.activeColor
|
||||
inactiveColor: root.inactiveColor
|
||||
occupiedColor: root.occupiedColor
|
||||
animationDuration: root.animationDuration
|
||||
|
||||
onSwitchRequested: uuid => root.switchTo(uuid)
|
||||
// Clicking the current desktop's pill: dispatch the configured action (pillClickSpec is null for None → no-op).
|
||||
onActiveClicked: root.dispatch(Logic.pillClickSpec(root.pillClickAction))
|
||||
}
|
||||
|
||||
// Reactive read-only desktop state — bind, never cache; updates on ANY change. Writes go through KWin DBus below.
|
||||
TaskManager.VirtualDesktopInfo {
|
||||
id: vdi
|
||||
}
|
||||
|
||||
// Per-desktop tooltip subText (rich-text window list), gated by showTooltips && showWindowList INDEPENDENTLY of the Loader.
|
||||
readonly property var desktopTooltips: (root.showTooltips && root.showWindowList && tooltipLoader.item)
|
||||
? (tooltipLoader.item as WindowAggregator).desktopTooltips : []
|
||||
|
||||
// GLOBAL per-desktop occupancy boolean[] from the snapshot, consumed by the dynamic-workspaces controller (the desktop SET is global). Empty [] when the Loader is inactive.
|
||||
readonly property var desktopOccupancy: tooltipLoader.item ? (tooltipLoader.item as WindowAggregator).desktopOccupancy : []
|
||||
// PER-SCREEN per-desktop occupancy (this monitor only) from the SAME snapshot, consumed by the occupied-dot indicator. Empty [] when the Loader is inactive.
|
||||
readonly property var screenOccupancy: tooltipLoader.item ? (tooltipLoader.item as WindowAggregator).screenOccupancy : []
|
||||
|
||||
// This pager's output rect, read from the placed representation (Screen.* is only valid on the on-screen item).
|
||||
// Empty until placed → per-screen occupancy degrades to global. Cast mirrors `tooltipLoader.item as WindowAggregator`.
|
||||
readonly property rect screenRect: root.fullRepresentationItem
|
||||
? (root.fullRepresentationItem as WorkspaceIndicator).screenRect : Qt.rect(0, 0, 0, 0)
|
||||
|
||||
// The window-list machinery lives behind a Loader (zero cost when unused). Needed by the tooltip list, dynamic workspaces, OR the occupied-dot indicator — gate is the OR.
|
||||
Loader {
|
||||
id: tooltipLoader
|
||||
active: (root.showTooltips && root.showWindowList) || root.dynamicWorkspaces || root.showOccupancy
|
||||
sourceComponent: aggregatorComponent
|
||||
}
|
||||
Component {
|
||||
id: aggregatorComponent
|
||||
WindowAggregator {
|
||||
virtualDesktopInfo: vdi // inject the read source (the aggregator is data-source-agnostic)
|
||||
screenRect: root.screenRect // inject this pager's output rect (drives per-screen occupancy)
|
||||
// Each feature gate is injected as a plain bool so the aggregator computes ONLY the reductions an
|
||||
// active consumer reads (an off feature's array stays []). windowListActive also trims tooltip-only roles.
|
||||
windowListActive: root.showTooltips && root.showWindowList
|
||||
occupancyActive: root.showOccupancy // per-screen occupancy → occupied-dot indicator
|
||||
dynamicActive: root.dynamicWorkspaces // global occupancy → dynamic-workspaces controller
|
||||
}
|
||||
}
|
||||
|
||||
// Every desktop write goes through KWin's VirtualDesktopManager. The CALL SHAPES live in logic.js's
|
||||
// *Spec builders; here we only DISPATCH a built spec (async fire-and-forget). A null spec is a no-op.
|
||||
function dispatch(spec) {
|
||||
if (!spec) {
|
||||
return;
|
||||
}
|
||||
// Async fire-and-forget — VirtualDesktopInfo reflects the resulting state, not a return value.
|
||||
// We capture the reply ONLY to warn on a rejected call (the silent-DBus-drop class this widget avoids).
|
||||
const reply = DBus.SessionBus.asyncCall({
|
||||
"service": spec.service,
|
||||
"path": spec.path,
|
||||
"iface": spec.iface,
|
||||
"member": spec.member,
|
||||
"arguments": spec.args.map(a => root.toDBusArg(a))
|
||||
});
|
||||
if (reply) {
|
||||
reply.finished.connect(() => {
|
||||
if (reply.isError) {
|
||||
console.warn("plasma-gnome-pager: DBus call failed:", spec.member, "-", reply.error.name, reply.error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Map ONE spec arg { t, v } to its DBus.* constructor. The "v" case wraps a PLAIN value — a wrapped DBus.string is silently rejected by KWin.
|
||||
function toDBusArg(a) {
|
||||
switch (a.t) {
|
||||
case "s":
|
||||
return new DBus.string(a.v);
|
||||
case "u":
|
||||
return new DBus.uint32(a.v);
|
||||
case "i":
|
||||
return new DBus.int32(a.v);
|
||||
case "v":
|
||||
return new DBus.variant(a.v);
|
||||
default:
|
||||
// Unknown type letter: warn LOUDLY rather than fail silently (the silent-DBus-drop class this widget avoids).
|
||||
console.warn("plasma-gnome-pager: toDBusArg got unknown DBus type letter", a.t);
|
||||
return a.v;
|
||||
}
|
||||
}
|
||||
|
||||
function switchTo(uuid) {
|
||||
root.dispatch(Logic.switchSpec(uuid));
|
||||
}
|
||||
|
||||
// Append a new desktop at the end. `?? 0` keeps a transient-undefined count out of the uint32 (i18n label stays here).
|
||||
function addDesktop() {
|
||||
root.dispatch(Logic.addSpec(vdi.numberOfDesktops ?? 0, i18n("New Desktop")));
|
||||
}
|
||||
|
||||
// Remove a desktop by UUID. removeSpec enforces never-remove-last (returns null).
|
||||
function removeDesktop(uuid) {
|
||||
root.dispatch(Logic.removeSpec(uuid, vdi.numberOfDesktops));
|
||||
}
|
||||
|
||||
// "Remove" targets the last desktop (the one addDesktop appended).
|
||||
function removeLastDesktop() {
|
||||
root.removeDesktop(Logic.lastDesktopId(vdi.desktopIds));
|
||||
}
|
||||
|
||||
// Dynamic workspaces (GNOME-style): one GLOBAL behaviour keeping an empty trailing desktop. The non-visual controller emits the two signals below.
|
||||
DynamicWorkspacesController {
|
||||
id: dynamicController
|
||||
|
||||
dynamicEnabled: root.dynamicWorkspaces
|
||||
namePrefix: root.dynamicNamePrefix
|
||||
// i18n default base name, passed IN so the controller stays i18n-free (never empty — KWin drops createDesktop on an empty name).
|
||||
defaultPrefix: i18nc("@info default base name for auto-created virtual desktops", "Desktop")
|
||||
virtualDesktopInfo: vdi
|
||||
desktopOccupancy: root.desktopOccupancy
|
||||
|
||||
onDispatchRequested: spec => root.dispatch(spec)
|
||||
onSyncConfigRequested: (nextEnabled, nextPrefix) => {
|
||||
// Mirror the global setting into this instance's persisted config (value-guarded so sync→onChanged→publish can't loop).
|
||||
if (Plasmoid.configuration.dynamicWorkspaces !== nextEnabled)
|
||||
Plasmoid.configuration.dynamicWorkspaces = nextEnabled;
|
||||
if (Plasmoid.configuration.dynamicNamePrefix !== nextPrefix)
|
||||
Plasmoid.configuration.dynamicNamePrefix = nextPrefix;
|
||||
}
|
||||
}
|
||||
|
||||
// Rename a desktop via KWin setDesktopName. renameSpec sanitizes and rejects empty (null → no-op); `vdi` reports the new name.
|
||||
function renameDesktop(uuid, name) {
|
||||
root.dispatch(Logic.renameSpec(uuid, name));
|
||||
}
|
||||
|
||||
// Open the rename prompt prefilled with the desktop's current name (resolved from the live vdi arrays, guarded for the transient frame).
|
||||
function openRenameDialog(uuid) {
|
||||
if (!uuid) {
|
||||
return;
|
||||
}
|
||||
const ids = vdi.desktopIds ?? [];
|
||||
const names = vdi.desktopNames ?? [];
|
||||
renameDialog.openFor(uuid, names[ids.indexOf(uuid)] ?? "");
|
||||
}
|
||||
|
||||
// Open the system "Virtual Desktops" settings module (like the stock pager). NOT a DBus write — an imperative
|
||||
// GUI launch via the public KCMLauncher, so it lives here, not in logic.js. Module name is platform-branched.
|
||||
function openVirtualDesktopsKcm() {
|
||||
KCM.KCMLauncher.openSystemSettings(Qt.platform.pluginName.includes("wayland")
|
||||
? "kcm_kwin_virtualdesktops"
|
||||
: "kcm_kwin_virtualdesktops_x11");
|
||||
}
|
||||
|
||||
// Right-click menu. Add/Remove gated by canAddRemove (they conflict with dynamic workspaces); Remove
|
||||
// also disables at the last desktop. "Configure Virtual Desktops…" is always shown and opens the SYSTEM
|
||||
// KCM (distinct from "Configure Workspaces…", which Plasma auto-adds for THIS widget's own settings).
|
||||
Plasmoid.contextualActions: [
|
||||
PlasmaCore.Action {
|
||||
text: i18n("Add Desktop")
|
||||
icon.name: "list-add"
|
||||
priority: Plasmoid.LowPriorityAction
|
||||
visible: root.canAddRemove
|
||||
enabled: root.canAddRemove
|
||||
onTriggered: root.addDesktop()
|
||||
},
|
||||
PlasmaCore.Action {
|
||||
text: i18n("Remove Last Desktop")
|
||||
icon.name: "list-remove"
|
||||
priority: Plasmoid.LowPriorityAction
|
||||
visible: root.canAddRemove
|
||||
enabled: root.canAddRemove && Logic.canRemoveDesktop(vdi.numberOfDesktops)
|
||||
onTriggered: root.removeLastDesktop()
|
||||
},
|
||||
PlasmaCore.Action {
|
||||
text: i18n("Rename Current Desktop…")
|
||||
icon.name: "edit-rename"
|
||||
priority: Plasmoid.LowPriorityAction
|
||||
visible: root.enableRename
|
||||
enabled: root.enableRename
|
||||
onTriggered: root.openRenameDialog(vdi.currentDesktop)
|
||||
},
|
||||
PlasmaCore.Action {
|
||||
text: i18n("Configure Virtual Desktops…")
|
||||
icon.name: "preferences-desktop-virtual" // the icon the Virtual Desktops KCM itself uses
|
||||
priority: Plasmoid.LowPriorityAction
|
||||
onTriggered: root.openVirtualDesktopsKcm()
|
||||
}
|
||||
]
|
||||
|
||||
// Rename prompt — the view lives in RenameDialog.qml; here we only place it and turn accepted() into the KWin write.
|
||||
RenameDialog {
|
||||
id: renameDialog
|
||||
visualParent: root.fullRepresentationItem
|
||||
location: Plasmoid.location
|
||||
onAccepted: (uuid, name) => root.renameDesktop(uuid, name)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user