Skip to content

Commit 574c493

Browse files
authored
Merge pull request #660 from yanekyuk/feat/clipboard
feat(clipboard): add clipboard history panel
2 parents bb24485 + aca753e commit 574c493

25 files changed

Lines changed: 4403 additions & 0 deletions

clipboard/BarWidget.qml

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import QtQuick
2+
import Quickshell
3+
import qs.Commons
4+
import qs.Services.UI
5+
import qs.Widgets
6+
7+
// Status bar button for the clipboard plugin.
8+
// One instance is created per screen that has a bar.
9+
//
10+
// Clicking toggles the plugin's Panel. Right-clicking shows a small context
11+
// menu (toggle / open settings) mirroring the clipper reference plugin
12+
// (noctalia-dev/noctalia-plugins#clipper).
13+
//
14+
// Contracts followed:
15+
// - docs/specs/plugin-entry-point-contracts.md (NIconButton root, injected
16+
// props, togglePanel/closePanel usage).
17+
// - docs/specs/plugin-qml-idioms.md (null-guard pluginApi, translations via
18+
// pluginApi?.tr, capsule style tokens from qs.Commons).
19+
NIconButton {
20+
id: root
21+
22+
// --- Shell-injected properties ------------------------------------------
23+
// Always null-guard every pluginApi access with optional chaining (?.).
24+
property var pluginApi: null
25+
// ShellScreen is a Quickshell type. The entry-point contract
26+
// (docs/specs/plugin-entry-point-contracts.md, BarWidget section) names
27+
// `property ShellScreen screen` as the canonical declaration; the
28+
// noctalia-plugins AGENTS.md audit treats a missing typed declaration as
29+
// a merge blocker. The `import Quickshell` above makes the type visible
30+
// to the shell's QML engine (qmllint can't resolve shell-internal
31+
// modules in isolation — that's a tooling limitation, not a contract
32+
// concern).
33+
property ShellScreen screen
34+
// Identity / layout metadata — injected by the shell after instantiation.
35+
// They participate in BarService layout calculations; we surface them as
36+
// explicit properties so the shell can write to them.
37+
property string widgetId: ""
38+
property string section: ""
39+
property int sectionWidgetIndex: -1
40+
property int sectionWidgetsCount: 0
41+
42+
// --- NIconButton styling ------------------------------------------------
43+
44+
icon: "clipboard-data"
45+
// No `?? ""` fallback on tr() — the translation system returns the key
46+
// itself on a miss, which is the correct fallback per the upstream
47+
// AGENTS.md (noctalia-dev/noctalia-plugins). Adding `?? ""` silently
48+
// swallows misses and hides broken keys from translators.
49+
tooltipText: pluginApi?.tr("bar.tooltip")
50+
tooltipDirection: BarService.getTooltipDirection(screen?.name)
51+
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
52+
// The bar renders at its own scale; we opt out of the extra UI scale so
53+
// the capsule heights line up with neighboring bar widgets.
54+
applyUiScale: false
55+
customRadius: Style.radiusL
56+
57+
colorBg: Style.capsuleColor
58+
colorFg: Color.mOnSurface
59+
colorBgHover: Color.mHover
60+
colorFgHover: Color.mOnHover
61+
colorBorder: "transparent"
62+
colorBorderHover: "transparent"
63+
border.color: Style.capsuleBorderColor
64+
border.width: Style.capsuleBorderWidth
65+
66+
// --- Interaction --------------------------------------------------------
67+
68+
// Left-click: open the plugin panel anchored to this bar widget.
69+
//
70+
// The second argument — the `caller` — is what the shell's SmartPanel
71+
// wrapper uses to anchor the panel geometrically. Without it the panel
72+
// falls back to centered positioning (issue #16). The canonical pattern
73+
// (per docs/specs/plugin-entry-point-contracts.md and the clipper
74+
// reference plugin) is `pluginApi.openPanel(screen, this)`; using `root`
75+
// here is equivalent to `this` since `root` is the root `NIconButton`
76+
// item id.
77+
//
78+
// To recover the "click again to close" behavior that togglePanel gave
79+
// us, we check `pluginApi.isPanelOpen(screen)` first and route to
80+
// `closePanel` when the panel is already open on this screen. When the
81+
// API lacks `isPanelOpen` (older shells), we fall back to calling
82+
// `openPanel` unconditionally — the shell's own deduplication handles
83+
// the repeated-open case.
84+
onClicked: {
85+
if (!pluginApi) return;
86+
const alreadyOpen = pluginApi.isPanelOpen?.(screen) === true;
87+
if (alreadyOpen && pluginApi.closePanel) {
88+
pluginApi.closePanel(screen);
89+
} else if (pluginApi.openPanel) {
90+
pluginApi.openPanel(screen, root);
91+
}
92+
}
93+
94+
// Right-click: show a lightweight context menu. The menu itself is a
95+
// child NPopupContextMenu; PanelService handles the visual attachment
96+
// (the same service also closes the menu when another panel opens).
97+
onRightClicked: {
98+
PanelService.showContextMenu(contextMenu, root, screen);
99+
}
100+
101+
// --- Context menu -------------------------------------------------------
102+
103+
NPopupContextMenu {
104+
id: contextMenu
105+
106+
model: [
107+
{
108+
"label": pluginApi?.tr("context.toggle"),
109+
"action": "toggle",
110+
"icon": "clipboard-data"
111+
},
112+
{
113+
"label": pluginApi?.tr("context.settings"),
114+
"action": "open-settings",
115+
"icon": "settings"
116+
}
117+
]
118+
119+
onTriggered: action => {
120+
contextMenu.close();
121+
PanelService.closeContextMenu(screen);
122+
123+
if (action === "toggle") {
124+
// Same anchoring concern as the left-click handler above:
125+
// always pass `root` as the caller so the panel attaches to
126+
// this bar widget rather than drifting to screen center.
127+
if (!pluginApi) return;
128+
const alreadyOpen = pluginApi.isPanelOpen?.(screen) === true;
129+
if (alreadyOpen && pluginApi.closePanel) {
130+
pluginApi.closePanel(screen);
131+
} else if (pluginApi.openPanel) {
132+
pluginApi.openPanel(screen, root);
133+
}
134+
} else if (action === "open-settings") {
135+
if (pluginApi?.manifest) {
136+
BarService.openPluginSettings(screen, pluginApi.manifest);
137+
}
138+
}
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)