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