diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 9290497a5..5d508a8c7 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -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", diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index 88d093075..7557aaec7 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -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}`); + } + } + } } diff --git a/Modules/Panels/Settings/Bar/WidgetSettings/CustomButtonSettings.qml b/Modules/Panels/Settings/Bar/WidgetSettings/CustomButtonSettings.qml index b35467b7d..b1b31f846 100644 --- a/Modules/Panels/Settings/Bar/WidgetSettings/CustomButtonSettings.qml +++ b/Modules/Panels/Settings/Bar/WidgetSettings/CustomButtonSettings.qml @@ -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 diff --git a/Services/Control/CustomButtonIPCService.qml b/Services/Control/CustomButtonIPCService.qml new file mode 100644 index 000000000..07b2f2cbc --- /dev/null +++ b/Services/Control/CustomButtonIPCService.qml @@ -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`); + } + } + } +} diff --git a/Services/UI/BarWidgetRegistry.qml b/Services/UI/BarWidgetRegistry.qml index 5d1bb0ba5..b49bc85a2 100644 --- a/Services/UI/BarWidgetRegistry.qml +++ b/Services/UI/BarWidgetRegistry.qml @@ -142,7 +142,8 @@ Singleton { "vertical": 10 }, "enableColorization": false, - "colorizeSystemIcon": "none" + "colorizeSystemIcon": "none", + "ipcIdentifier": "" }, "KeyboardLayout": { "displayMode": "onhover", diff --git a/shell.qml b/shell.qml index fdd65a3c5..d3d1a58b6 100644 --- a/shell.qml +++ b/shell.qml @@ -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