Skip to content

Commit 89f3a59

Browse files
feat(plugin-manager): add-to-bar menu, auto-select, richer markdown, 16 locales
- Add cursor-positioned "Add to bar" context menu (Left/Center/Right) via fullscreen overlay using wl_pointer hover events for cross-compositor support (Hyprland, Niri, Sway). - Auto-select the first plugin on panel open and tab switch so the README pane is never empty. - Fuzzy-search bar in the Installed tab. - Richer README rendering: inline italic/strikethrough, soft line breaks, expanded image domain whitelist. - Ship 16 additional locale files (de, es, fr, it, pt, nl, ru, ja, zh-CN, zh-TW, ko-KR, tr, uk-UA, pl, sv, hu).
1 parent f288944 commit 89f3a59

24 files changed

Lines changed: 1309 additions & 15 deletions

plugin-manager/AddToBarMenu.qml

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import QtQuick
2+
import Quickshell
3+
import Quickshell.Wayland
4+
import qs.Commons
5+
import qs.Services.UI
6+
import qs.Widgets
7+
8+
// Fullscreen overlay that positions an NPopupContextMenu at the cursor.
9+
// Cross-compositor — relies on standard wl_pointer hover events delivered
10+
// by any Wayland compositor (Hyprland, Niri, Sway, ...). No hyprctl.
11+
PanelWindow {
12+
id: root
13+
14+
required property ShellScreen screen
15+
property var pluginApi: null
16+
property var menuItems: []
17+
18+
signal actionSelected(string action)
19+
signal cancelled
20+
21+
property bool _menuShown: false
22+
23+
anchors.top: true
24+
anchors.left: true
25+
anchors.right: true
26+
anchors.bottom: true
27+
visible: false
28+
color: "transparent"
29+
30+
WlrLayershell.layer: WlrLayer.Overlay
31+
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
32+
WlrLayershell.namespace: "noctalia-plugin-manager-add-to-bar-" + (screen?.name || "unknown")
33+
WlrLayershell.exclusionMode: ExclusionMode.Ignore
34+
35+
function show(items) {
36+
menuItems = items || []
37+
_menuShown = false
38+
visible = true
39+
// Fallback: show menu after 300 ms even if no pointer event arrives
40+
// (e.g. cursor outside screen). Normally the first wl_pointer.enter or
41+
// wl_pointer.motion reaching cursorArea fires _showMenuAtCursor sooner.
42+
fallbackTimer.restart()
43+
}
44+
45+
function close() {
46+
fallbackTimer.stop()
47+
_menuShown = false
48+
visible = false
49+
contextMenu.visible = false
50+
}
51+
52+
function _showMenuAtCursor() {
53+
if (_menuShown || !visible) return
54+
_menuShown = true
55+
fallbackTimer.stop()
56+
anchorPoint.x = cursorArea.mouseX
57+
anchorPoint.y = cursorArea.mouseY
58+
contextMenu.model = root.menuItems
59+
contextMenu.anchorItem = anchorPoint
60+
contextMenu.visible = true
61+
}
62+
63+
Timer {
64+
id: fallbackTimer
65+
interval: 300
66+
repeat: false
67+
onTriggered: root._showMenuAtCursor()
68+
}
69+
70+
NPopupContextMenu {
71+
id: contextMenu
72+
visible: false
73+
screen: root.screen
74+
minWidth: 200
75+
76+
onTriggered: (action, item) => {
77+
root.actionSelected(action)
78+
root.close()
79+
}
80+
}
81+
82+
Item {
83+
id: anchorPoint
84+
width: 1
85+
height: 1
86+
x: 0
87+
y: 0
88+
}
89+
90+
MouseArea {
91+
id: cursorArea
92+
anchors.fill: parent
93+
hoverEnabled: true
94+
acceptedButtons: Qt.LeftButton | Qt.RightButton
95+
96+
onEntered: root._showMenuAtCursor()
97+
onPositionChanged: mouse => root._showMenuAtCursor()
98+
99+
onClicked: mouse => {
100+
root.cancelled()
101+
root.close()
102+
}
103+
}
104+
105+
Component.onDestruction: {
106+
close()
107+
}
108+
}

plugin-manager/InstalledTabContent.qml

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,63 @@ ColumnLayout {
1818
// Track which plugins are currently updating
1919
property var updatingPlugins: ({})
2020
property int installedPluginsRefreshCounter: 0
21+
property string pluginSearchText: ""
22+
23+
// Plugin id targeted by the add-to-bar context menu (set on click, read on action)
24+
property string _addToBarPluginId: ""
25+
26+
// Add a plugin's bar widget to the given section on the current panel screen.
27+
// Mirrors the core Settings.Bar → MonitorWidgetsConfig._addWidgetToSection flow.
28+
function _addPluginToBar(pluginId, section, pluginName) {
29+
if (!pluginApi) return
30+
var screen = pluginApi.panelOpenScreen
31+
if (!screen || !screen.name) return
32+
var screenName = screen.name
33+
var widgetId = "plugin:" + pluginId
34+
35+
var currentWidgets = Settings.getBarWidgetsForScreen(screenName)
36+
var widgets = {
37+
"left": [],
38+
"center": [],
39+
"right": []
40+
}
41+
try {
42+
widgets.left = JSON.parse(JSON.stringify(currentWidgets.left || []))
43+
widgets.center = JSON.parse(JSON.stringify(currentWidgets.center || []))
44+
widgets.right = JSON.parse(JSON.stringify(currentWidgets.right || []))
45+
} catch (e) {
46+
Logger.w("PluginManager", "Failed to clone bar widgets:", e)
47+
}
48+
49+
var sections = ["left", "center", "right"]
50+
for (var s = 0; s < sections.length; s++) {
51+
var arr = widgets[sections[s]]
52+
for (var i = 0; i < arr.length; i++) {
53+
if (arr[i] && arr[i].id === widgetId) {
54+
var alreadyMsg = pluginApi.tr("panel.already-on-bar").replace("{plugin}", pluginName || pluginId)
55+
ToastService.showNotice(pluginApi.tr("panel.title"), alreadyMsg)
56+
return
57+
}
58+
}
59+
}
60+
61+
var newWidget = { "id": widgetId }
62+
var meta = BarWidgetRegistry.pluginWidgetMetadata ? BarWidgetRegistry.pluginWidgetMetadata[widgetId] : null
63+
if (meta) {
64+
Object.keys(meta).forEach(function (key) {
65+
if (key !== "id") newWidget[key] = meta[key]
66+
})
67+
}
68+
69+
if (!widgets[section]) widgets[section] = []
70+
widgets[section].push(newWidget)
71+
72+
Settings.setScreenOverride(screenName, "widgets", widgets)
73+
BarService.widgetsRevision++
74+
75+
var successMsg = pluginApi.tr("panel.add-to-bar-success").replace("{plugin}", pluginName || pluginId)
76+
ToastService.showNotice(pluginApi.tr("panel.title"), successMsg)
77+
}
2178

2279
function stripAuthorEmail(author) {
2380
if (!author) return "";
@@ -102,6 +159,9 @@ ColumnLayout {
102159
showToastOnSave: true
103160
}
104161

162+
// Plugin name paired with _addToBarPluginId (kept for external call-backs)
163+
property var _addToBarPluginName: ""
164+
105165
function uninstallPlugin(pluginId) {
106166
var manifest = PluginRegistry.getPluginManifest(pluginId);
107167
var pluginName = manifest?.name || pluginId;
@@ -195,6 +255,15 @@ ColumnLayout {
195255
}
196256
}
197257

258+
// Search input
259+
NTextInput {
260+
placeholderText: I18n.tr("placeholders.search")
261+
inputIconName: "search"
262+
text: root.pluginSearchText
263+
onTextChanged: root.pluginSearchText = text
264+
Layout.fillWidth: true
265+
}
266+
198267
// Installed plugins list
199268
ColumnLayout {
200269
spacing: Style.marginM
@@ -238,6 +307,21 @@ ColumnLayout {
238307
plugins.push(pluginData);
239308
}
240309
}
310+
311+
// Apply fuzzy search
312+
var query = root.pluginSearchText.trim();
313+
if (query !== "") {
314+
var results = FuzzySort.go(query, plugins, {
315+
"keys": ["name", "description"],
316+
"limit": 50
317+
});
318+
var out = [];
319+
for (var k = 0; k < results.length; k++) {
320+
out.push(results[k].obj);
321+
}
322+
return out;
323+
}
324+
241325
return plugins;
242326
}
243327

@@ -325,6 +409,25 @@ ColumnLayout {
325409
visible: Settings.isDebug
326410
}
327411

412+
NIconButton {
413+
id: addToBarBtn
414+
icon: "plus"
415+
tooltipText: pluginApi?.tr("panel.add-to-bar")
416+
baseSize: Style.baseWidgetSize * 0.7
417+
visible: (modelData.entryPoints?.barWidget !== undefined)
418+
enabled: modelData.enabled
419+
onClicked: {
420+
var rootRef = root
421+
var pid = modelData.id
422+
var pname = modelData.name
423+
var main = pluginApi?.mainInstance
424+
if (!main) return
425+
main.showAddToBarMenu(pid, pname, function (chosenId, chosenAction, chosenName) {
426+
rootRef._addPluginToBar(chosenId, chosenAction, chosenName)
427+
})
428+
}
429+
}
430+
328431
NIconButton {
329432
icon: "settings"
330433
tooltipText: pluginApi?.tr("panel.open-settings")
@@ -383,16 +486,17 @@ ColumnLayout {
383486
updates2[pid] = false;
384487
rootRef.updatingPlugins = updates2;
385488

386-
if (!pluginApi) return;
489+
var api = rootRef.pluginApi;
490+
if (!api) return;
387491
if (success) {
388-
var title = pluginApi.tr("panel.title")
389-
var msg = pluginApi.tr("panel.install-success")
492+
var title = api.tr("panel.title")
493+
var msg = api.tr("panel.install-success")
390494
msg = msg.replace("{plugin}", pname)
391495
ToastService.showNotice(title, msg);
392496
} else {
393-
var title2 = pluginApi.tr("panel.title")
394-
var errMsg = pluginApi.tr("panel.install-error")
395-
errMsg = errMsg.replace("{error}", error || pluginApi.tr("panel.unknown-error"))
497+
var title2 = api.tr("panel.title")
498+
var errMsg = api.tr("panel.install-error")
499+
errMsg = errMsg.replace("{error}", error || api.tr("panel.unknown-error"))
396500
ToastService.showError(title2, errMsg);
397501
}
398502
});

plugin-manager/Main.qml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,34 @@
11
import QtQuick
2+
import Quickshell
23
import Quickshell.Io
34
import qs.Commons
45

56
Item {
67
id: root
78
property var pluginApi: null
89

10+
// First AddToBarMenu instance, captured from the per-screen Variants delegate.
11+
property var addToBarMenu: null
12+
13+
// State for the pending add-to-bar request
14+
property string _pendingPluginId: ""
15+
property string _pendingPluginName: ""
16+
property var _onAddToBarAction: null
17+
18+
// Called by InstalledTabContent when the user clicks the "+" button on a plugin.
19+
// onAction(pluginId, section, pluginName) runs when user picks left/center/right.
20+
function showAddToBarMenu(pluginId, pluginName, onAction) {
21+
if (!pluginApi || !addToBarMenu) return
22+
_pendingPluginId = pluginId
23+
_pendingPluginName = pluginName
24+
_onAddToBarAction = onAction
25+
addToBarMenu.show([
26+
{ "label": pluginApi.tr("panel.add-to-bar-left"), "action": "left", "icon": "align-left" },
27+
{ "label": pluginApi.tr("panel.add-to-bar-center"), "action": "center", "icon": "align-center" },
28+
{ "label": pluginApi.tr("panel.add-to-bar-right"), "action": "right", "icon": "align-right" }
29+
])
30+
}
31+
932
IpcHandler {
1033
target: "plugin:plugin-manager"
1134

@@ -17,4 +40,42 @@ Item {
1740
}
1841
}
1942
}
43+
44+
Variants {
45+
model: Quickshell.screens
46+
47+
delegate: AddToBarMenu {
48+
required property var modelData
49+
50+
screen: modelData
51+
pluginApi: root.pluginApi
52+
53+
Component.onCompleted: {
54+
if (!root.addToBarMenu) {
55+
root.addToBarMenu = this
56+
}
57+
}
58+
59+
onActionSelected: action => {
60+
if (root._onAddToBarAction) {
61+
root._onAddToBarAction(root._pendingPluginId, action, root._pendingPluginName)
62+
}
63+
root._pendingPluginId = ""
64+
root._pendingPluginName = ""
65+
root._onAddToBarAction = null
66+
}
67+
68+
onCancelled: {
69+
root._pendingPluginId = ""
70+
root._pendingPluginName = ""
71+
root._onAddToBarAction = null
72+
}
73+
}
74+
}
75+
76+
Component.onDestruction: {
77+
_pendingPluginId = ""
78+
_pendingPluginName = ""
79+
_onAddToBarAction = null
80+
}
2081
}

0 commit comments

Comments
 (0)