feat: Implement automatic IPC registration for CustomButton widgets

- Add automatic registration/unregistration of CustomButton instances to
CustomButtonIPCService
- Enable CustomButton widgets to register themselves with unique
identifiers
- Support external control via IPC commands using 'qs -c noctalia-loner
ipc call cb <action> "[identifier]"' format

Supported actions: left, right, middle, up (separate wheel mode), down
(separate wheel mode),wheel (unified wheel mode), refresh (textCommand)
This commit is contained in:
loner
2026-01-14 07:13:07 +08:00
parent 7fa94b4253
commit db0a2ccb7f
6 changed files with 259 additions and 1 deletions
+3
View File
@@ -103,6 +103,8 @@
"hide-mode-label": "Hide mode",
"hide-mode-max-transparent": "Max expanded but transparent",
"icon-description": "Select an icon from the library.",
"ipc-identifier-description": "Unique identifier for IPC commands. Use this identifier with 'qs -c noctalia-loner ipc call cb [action] [identifier]' to control this button via IPC.",
"ipc-identifier-label": "IPC Identifier",
"left-click-description": "Command to execute when the button is left-clicked.",
"left-click-label": "Left click",
"left-click-update-text": "Update displayed text on left-click",
@@ -1358,6 +1360,7 @@
"placeholders": {
"command-example": "echo \"Hello World\"",
"enter-command": "Enter command to execute (app or custom script)",
"enter-ipc-identifier": "Enter unique identifier for IPC commands",
"enter-text-to-collapse": "e.g., 'nothing is playing'. Use /regex/ for patterns.",
"enter-tooltip": "Enter tooltip",
"enter-width-pixels": "Enter width in pixels",
+59
View File
@@ -7,6 +7,7 @@ import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings
import qs.Services.UI
import qs.Widgets
import qs.Services.Control
Item {
id: root
@@ -39,6 +40,7 @@ Item {
readonly property bool rightClickUpdateText: widgetSettings.rightClickUpdateText ?? widgetMetadata.rightClickUpdateText
readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec
readonly property bool middleClickUpdateText: widgetSettings.middleClickUpdateText ?? widgetMetadata.middleClickUpdateText
readonly property string ipcIdentifier: widgetSettings.ipcIdentifier !== undefined ? widgetSettings.ipcIdentifier : (widgetMetadata.ipcIdentifier || "")
readonly property string wheelExec: widgetSettings.wheelExec || widgetMetadata.wheelExec
readonly property string wheelUpExec: widgetSettings.wheelUpExec || widgetMetadata.wheelUpExec
readonly property string wheelDownExec: widgetSettings.wheelDownExec || widgetMetadata.wheelDownExec
@@ -607,4 +609,61 @@ Item {
}
}
}
// Timer to handle registration attempts
Timer {
id: registrationTimer
interval: 1500
repeat: false
onTriggered: {
// Only register if ipcIdentifier is set
if (ipcIdentifier && ipcIdentifier.trim() !== "") {
// Try to access the service through the global application object
try {
if (typeof Qt !== 'undefined' && Qt.application && Qt.application.customButtonIPCService) {
var service = Qt.application.customButtonIPCService;
var success = service.registerButton(root);
if (success) {
Logger.i("CustomButton", `Successfully registered button with identifier: '${ipcIdentifier}'`);
} else {
Logger.w("CustomButton", `Failed to register button with identifier: '${ipcIdentifier}'`);
}
} else {
Logger.w("CustomButton", `Service not available for button with identifier '${ipcIdentifier}'`);
}
} catch (e) {
Logger.w("CustomButton", `Error during registration of button with identifier '${ipcIdentifier}': ${e.message}`);
}
} else {
Logger.d("CustomButton", `No IPC identifier set for button, skipping registration`);
}
}
}
// Register this button with the IPC service when component is completed
Component.onCompleted: {
registrationTimer.start();
}
// Unregister this button when component is destroyed
Component.onDestruction: {
if (ipcIdentifier && ipcIdentifier.trim() !== "") {
// Try to access the service through the global application object for unregistration
try {
if (typeof Qt !== 'undefined' && Qt.application && Qt.application.customButtonIPCService) {
var service = Qt.application.customButtonIPCService;
var success = service.unregisterButton(root);
if (success) {
Logger.i("CustomButton", `Successfully unregistered button with identifier: '${ipcIdentifier}'`);
} else {
Logger.w("CustomButton", `Failed to unregister button with identifier: '${ipcIdentifier}'`);
}
} else {
Logger.w("CustomButton", `Service not available for unregistration of button with identifier '${ipcIdentifier}'`);
}
} catch (e) {
Logger.w("CustomButton", `Error during unregistration of button with identifier '${ipcIdentifier}': ${e.message}`);
}
}
}
}
@@ -22,6 +22,7 @@ ColumnLayout {
property bool valueShowIcon: (widgetData.showIcon !== undefined) ? widgetData.showIcon : widgetMetadata.showIcon
property bool valueEnableColorization: widgetData.enableColorization || false
property string valueColorizeSystemIcon: widgetData.colorizeSystemIcon !== undefined ? widgetData.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon || "none"
property string valueIpcIdentifier: widgetData.ipcIdentifier !== undefined ? widgetData.ipcIdentifier : widgetMetadata.ipcIdentifier || ""
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
@@ -52,6 +53,7 @@ ColumnLayout {
settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10);
settings.enableColorization = valueEnableColorization;
settings.colorizeSystemIcon = valueColorizeSystemIcon;
settings.ipcIdentifier = valueIpcIdentifier;
return settings;
}
@@ -142,6 +144,15 @@ ColumnLayout {
onSelected: key => valueColorizeSystemIcon = key
}
NTextInput {
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.ipc-identifier-label")
description: I18n.tr("bar.custom-button.ipc-identifier-description")
placeholderText: I18n.tr("placeholders.enter-ipc-identifier")
text: valueIpcIdentifier
onTextChanged: valueIpcIdentifier = text
}
RowLayout {
spacing: Style.marginM
+179
View File
@@ -0,0 +1,179 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.UI
import qs.Services.Control
Item {
id: root
// Registry to store references to active custom buttons by their user-defined identifier
property var customButtonRegistry: ({})
Component.onCompleted: {
Logger.i("CustomButtonIPCService", "Service started");
// Make this service globally accessible
if (typeof Qt !== 'undefined' && Qt && Qt.application) {
Qt.application.customButtonIPCService = root;
}
}
// Register a custom button instance
function registerButton(button) {
if (!button || !button.ipcIdentifier) {
Logger.w("CustomButtonIPCService", "Cannot register button without ipcIdentifier");
return false;
}
customButtonRegistry[button.ipcIdentifier] = button;
Logger.d("CustomButtonIPCService", `Registered button with identifier: ${button.ipcIdentifier}`);
return true;
}
// Unregister a custom button instance
function unregisterButton(button) {
if (!button || !button.ipcIdentifier) {
return false;
}
if (customButtonRegistry[button.ipcIdentifier] === button) {
delete customButtonRegistry[button.ipcIdentifier];
Logger.d("CustomButtonIPCService", `Unregistered button with identifier: ${button.ipcIdentifier}`);
return true;
}
return false;
}
// Find a button by identifier
function findButton(identifier) {
return customButtonRegistry[identifier] || null;
}
// IpcHandler for custom button commands using short alias 'cb'
IpcHandler {
target: "cb"
// Handle left click: cb left "identifier"
function left(identifier: string) {
const button = findButton(identifier);
if (!button) {
Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`);
return;
}
// Trigger left click if configured
if (button.leftClickExec || button.textCommand) {
button.onClicked();
Logger.i("CustomButtonIPCService", `Triggered left click on button '${identifier}'`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no left click action configured`);
}
}
// Handle right click: cb right "identifier"
function right(identifier: string) {
const button = findButton(identifier);
if (!button) {
Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`);
return;
}
// Trigger right click if configured
if (button.rightClickExec) {
button.onRightClicked();
Logger.i("CustomButtonIPCService", `Triggered right click on button '${identifier}'`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no right click action configured`);
}
}
// Handle middle click: cb middle "identifier"
function middle(identifier: string) {
const button = findButton(identifier);
if (!button) {
Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`);
return;
}
// Trigger middle click if configured
if (button.middleClickExec) {
button.onMiddleClicked();
Logger.i("CustomButtonIPCService", `Triggered middle click on button '${identifier}'`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no middle click action configured`);
}
}
// Handle wheel up: cb up "identifier"
function up(identifier: string) {
const button = findButton(identifier);
if (!button) {
Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`);
return;
}
// Trigger wheel up if in separate mode and configured
if (button.wheelMode === "separate" && button.wheelUpExec) {
button.onWheel(1);
Logger.i("CustomButtonIPCService", `Triggered wheel up on button '${identifier}'`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no separate wheel up action configured or is not in separate mode`);
}
}
// Handle wheel down: cb down "identifier"
function down(identifier: string) {
const button = findButton(identifier);
if (!button) {
Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`);
return;
}
// Trigger wheel down if in separate mode and configured
if (button.wheelMode === "separate" && button.wheelDownExec) {
button.onWheel(-1);
Logger.i("CustomButtonIPCService", `Triggered wheel down on button '${identifier}'`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no separate wheel down action configured or is not in separate mode`);
}
}
// Handle wheel action: cb wheel "identifier"
function wheel(identifier: string) {
const button = findButton(identifier);
if (!button) {
Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`);
return;
}
// Trigger unified wheel if in unified mode and configured
if (button.wheelMode === "unified" && button.wheelExec) {
button.onWheel(1);
Logger.i("CustomButtonIPCService", `Triggered wheel action on button '${identifier}'`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no unified wheel action configured or is not in unified mode`);
}
}
// Handle refresh: cb refresh "identifier"
function refresh(identifier: string) {
const button = findButton(identifier);
if (!button) {
Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`);
return;
}
// Trigger text command refresh if configured and not streaming
if (button.textCommand && button.textCommand.length > 0 && !button.textStream) {
button.runTextCommand();
Logger.i("CustomButtonIPCService", `Triggered refresh (text command) on button '${identifier}'`);
} else if (button.textStream) {
Logger.w("CustomButtonIPCService", `Button '${identifier}' uses streaming, manual refresh disabled`);
} else {
Logger.w("CustomButtonIPCService", `Button '${identifier}' has no text command to refresh`);
}
}
}
}
+2 -1
View File
@@ -142,7 +142,8 @@ Singleton {
"vertical": 10
},
"enableColorization": false,
"colorizeSystemIcon": "none"
"colorizeSystemIcon": "none",
"ipcIdentifier": ""
},
"KeyboardLayout": {
"displayMode": "onhover",
+5
View File
@@ -133,6 +133,11 @@ ShellRoot {
screenDetector: screenDetector
}
// CustomButtonIPCService handles IPC commands for custom buttons
CustomButtonIPCService {
id: customButtonIPCService
}
// Container for plugins Main.qml instances (must be in graphics scene)
Item {
id: pluginContainer