From 5c19c8433eb593b945a6790c69b5e1ca7b44242b Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Mon, 3 Nov 2025 06:11:02 +0800 Subject: [PATCH 01/13] feat: Implement Control Center custom button basic framework --- Assets/Translations/en.json | 30 ++++- Modules/ControlCenter/Cards/ShortcutsCard.qml | 6 +- .../ControlCenter/Widgets/CustomButton.qml | 34 +++++ .../ControlCenterWidgetSettingsDialog.qml | 120 ++++++++++++++++++ .../WidgetSettings/CustomButtonSettings.qml | 70 ++++++++++ Modules/Settings/Tabs/ControlCenterTab.qml | 4 +- Services/ControlCenterWidgetRegistry.qml | 15 ++- 7 files changed, 268 insertions(+), 11 deletions(-) create mode 100644 Modules/ControlCenter/Widgets/CustomButton.qml create mode 100644 Modules/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml create mode 100644 Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 76b87f64..8a08982e 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -833,7 +833,26 @@ "description": "Configure and manage the shortcuts widgets." }, "sectionLeft": "Left", - "sectionRight": "Right" + "sectionRight": "Right", + "custom-button": { + "icon": { + "label": "Icon", + "description": "Select an icon from the library." + }, + "browse": "Browse", + "command": { + "label": "Command", + "description": "The command to execute when the button is clicked." + }, + "tooltip": { + "label": "Tooltip", + "description": "The tooltip to show when hovering over the button." + } + }, + "dialog": { + "cancel": "Cancel", + "apply": "Apply" + } } }, "user-interface": { @@ -1450,15 +1469,16 @@ "search": "Search...", "select": "Select", "cancel": "Cancel", - "test": "Test" + "test": "Test", + "enter-tooltip": "Enter tooltip" }, "options": { "colors": { "primary": "Primary", "secondary": "Secondary", - "tertiary": "Tertiary", - "error": "Error", - "onSurface": "On Surface" + "tertiary": "Tertiary", + "error": "Error", + "onSurface": "On Surface" }, "bar": { "position": { diff --git a/Modules/ControlCenter/Cards/ShortcutsCard.qml b/Modules/ControlCenter/Cards/ShortcutsCard.qml index 2100a76b..3d478861 100644 --- a/Modules/ControlCenter/Cards/ShortcutsCard.qml +++ b/Modules/ControlCenter/Cards/ShortcutsCard.qml @@ -35,7 +35,8 @@ RowLayout { "widgetId": modelData.id, "section": "quickSettings", "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.controlCenter.shortcuts.left.length + "sectionWidgetsCount": Settings.data.controlCenter.shortcuts.left.length, + "widgetSettings": modelData } Layout.alignment: Qt.AlignVCenter } @@ -70,7 +71,8 @@ RowLayout { "widgetId": modelData.id, "section": "quickSettings", "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.controlCenter.shortcuts.right.length + "sectionWidgetsCount": Settings.data.controlCenter.shortcuts.right.length, + "widgetSettings": modelData } Layout.alignment: Qt.AlignVCenter } diff --git a/Modules/ControlCenter/Widgets/CustomButton.qml b/Modules/ControlCenter/Widgets/CustomButton.qml new file mode 100644 index 00000000..12b35343 --- /dev/null +++ b/Modules/ControlCenter/Widgets/CustomButton.qml @@ -0,0 +1,34 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +// Dummy comment to force re-evaluation +Item { + id: root + + // Widget properties + property string widgetId: "CustomButton" + property var widgetSettings: {} // This will be populated from settings + + // Use settings or provide defaults + readonly property string customIcon: widgetSettings.icon || "heart" + readonly property string exec: widgetSettings.exec || "" + readonly property string tooltipText: widgetSettings.tooltipText || "Custom Button" + + implicitWidth: button.implicitWidth + implicitHeight: button.implicitHeight + + NIconButton { + id: button + icon: customIcon + tooltipText: tooltipText + onClicked: { + if (exec) { + Quickshell.execDetached(["sh", "-c", exec]) + Logger.i("CC:CustomButton", `Executing command: ${exec}`) + } + } + } +} diff --git a/Modules/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml b/Modules/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml new file mode 100644 index 00000000..a0aaaa01 --- /dev/null +++ b/Modules/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml @@ -0,0 +1,120 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +// Widget Settings Dialog Component +Popup { + id: root + + property int widgetIndex: -1 + property var widgetData: null + property string widgetId: "" + property string sectionId: "" + + signal updateWidgetSettings(string section, int index, var settings) + + width: Math.max(content.implicitWidth + padding * 2, 500) + height: content.implicitHeight + padding * 2 + padding: Style.marginXL + modal: true + anchors.centerIn: parent + + onOpened: { + PanelService.willOpenPopup(root) + if (widgetData && widgetId) { + loadWidgetSettings() + } + } + + onClosed: { + PanelService.willClosePopup(root) + } + + background: Rectangle { + color: Color.mSurface + radius: Style.radiusL + border.color: Color.mPrimary + border.width: Style.borderM + } + + contentItem: ColumnLayout { + id: content + width: parent.width + spacing: Style.marginM + + // Title + RowLayout { + Layout.fillWidth: true + + NText { + text: I18n.tr("system.widget-settings-title", { "widget": root.widgetId }) + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("tooltips.close") + onClicked: root.close() + } + } + + // Separator + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + } + + Loader { + id: settingsLoader + Layout.fillWidth: true + } + + // Action buttons + RowLayout { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + spacing: Style.marginM + + Item { Layout.fillWidth: true } + + NButton { + text: I18n.tr("settings.control-center.shortcuts.dialog.cancel", "Cancel") + outlined: true + onClicked: root.close() + } + + NButton { + text: I18n.tr("settings.control-center.shortcuts.dialog.apply", "Apply") + icon: "check" + onClicked: { + if (settingsLoader.item && settingsLoader.item.saveSettings) { + var newSettings = settingsLoader.item.saveSettings() + root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings) + root.close() + } + } + } + } + } + + function loadWidgetSettings() { + const widgetSettingsMap = { + "CustomButton": "WidgetSettings/CustomButtonSettings.qml" + } + + const source = widgetSettingsMap[widgetId] + if (source) { + settingsLoader.setSource(source, { + "widgetData": widgetData, + "widgetMetadata": ControlCenterWidgetRegistry.widgetMetadata[widgetId] + }) + } + } +} diff --git a/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml b/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml new file mode 100644 index 00000000..516db036 --- /dev/null +++ b/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + property var widgetData: null + property var widgetMetadata: null + + property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon + property string valueTooltip: widgetData.tooltipText !== undefined ? widgetData.tooltipText : widgetMetadata.tooltipText + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.icon = valueIcon + settings.exec = execInput.text + settings.tooltipText = tooltipInput.text + return settings + } + + RowLayout { + spacing: Style.marginM + + NLabel { + label: I18n.tr("settings.control-center.shortcuts.custom-button.icon.label", "Icon") + description: I18n.tr("settings.control-center.shortcuts.custom-button.icon.description", "The icon for the button.") + } + + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: valueIcon + pointSize: Style.fontSizeXL + visible: valueIcon !== "" + } + + NButton { + text: I18n.tr("settings.control-center.shortcuts.custom-button.browse", "Browse") + onClicked: iconPicker.open() + } + } + + NIconPicker { + id: iconPicker + initialIcon: valueIcon + onIconSelected: function (iconName) { + valueIcon = iconName + } + } + + NTextInput { + id: execInput + Layout.fillWidth: true + label: I18n.tr("settings.control-center.shortcuts.custom-button.command.label", "Command") + description: I18n.tr("settings.control-center.shortcuts.custom-button.command.description", "The command to execute when the button is clicked.") + placeholderText: I18n.tr("placeholders.enter-command") + text: widgetData?.exec || widgetMetadata.exec + } + + NTextInput { + id: tooltipInput + Layout.fillWidth: true + label: I18n.tr("settings.control-center.shortcuts.custom-button.tooltip.label", "Tooltip") + description: I18n.tr("settings.control-center.shortcuts.custom-button.tooltip.description", "The tooltip to show when hovering over the button.") + placeholderText: I18n.tr("placeholders.enter-tooltip") + text: widgetData?.tooltipText || widgetMetadata.tooltipText + } +} diff --git a/Modules/Settings/Tabs/ControlCenterTab.qml b/Modules/Settings/Tabs/ControlCenterTab.qml index 372add57..69110393 100644 --- a/Modules/Settings/Tabs/ControlCenterTab.qml +++ b/Modules/Settings/Tabs/ControlCenterTab.qml @@ -226,7 +226,7 @@ ColumnLayout { NSectionEditor { sectionName: I18n.tr("settings.control-center.shortcuts.sectionLeft") sectionId: "left" - settingsDialogComponent: "" + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml") maxWidgets: 5 widgetRegistry: ControlCenterWidgetRegistry widgetModel: Settings.data.controlCenter.shortcuts["left"] @@ -245,7 +245,7 @@ ColumnLayout { NSectionEditor { sectionName: I18n.tr("settings.control-center.shortcuts.sectionRight") sectionId: "right" - settingsDialogComponent: "" + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml") maxWidgets: 5 widgetRegistry: ControlCenterWidgetRegistry widgetModel: Settings.data.controlCenter.shortcuts["right"] diff --git a/Services/ControlCenterWidgetRegistry.qml b/Services/ControlCenterWidgetRegistry.qml index f00263ed..cd7cb6c1 100644 --- a/Services/ControlCenterWidgetRegistry.qml +++ b/Services/ControlCenterWidgetRegistry.qml @@ -17,10 +17,18 @@ Singleton { "PowerProfile": powerProfileComponent, "ScreenRecorder": screenRecorderComponent, "WiFi": wiFiComponent, - "WallpaperSelector": wallpaperSelectorComponent + "WallpaperSelector": wallpaperSelectorComponent, + "CustomButton": customButtonComponent }) - property var widgetMetadata: ({}) + property var widgetMetadata: ({ + "CustomButton": { + "allowUserSettings": true, + "icon": "heart", + "exec": "", + "tooltipText": "Custom Button" + } + }) // Component definitions - these are loaded once at startup property Component bluetoothComponent: Component { @@ -47,6 +55,9 @@ Singleton { property Component wallpaperSelectorComponent: Component { WallpaperSelector {} } + property Component customButtonComponent: Component { + CustomButton {} + } function init() { Logger.i("ControlCenterWidgetRegistry", "Service started") From e182fe25520fca6f931e45fcc33c5d5e663144f1 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Mon, 3 Nov 2025 09:58:37 +0800 Subject: [PATCH 02/13] feat: add on state logic --- Assets/Translations/en.json | 28 ++++ .../ControlCenter/Widgets/CustomButton.qml | 100 ++++++++++-- .../WidgetSettings/CustomButtonSettings.qml | 147 +++++++++++++++--- Services/ControlCenterWidgetRegistry.qml | 9 +- 4 files changed, 247 insertions(+), 37 deletions(-) diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 8a08982e..6b972cc6 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -847,6 +847,34 @@ "tooltip": { "label": "Tooltip", "description": "The tooltip to show when hovering over the button." + }, + "on-state-icon": { + "label": "On State Icon", + "description": "The icon for the button when it's in the 'on' state." + }, + "on-clicked": { + "label": "Left Click Command", + "description": "Command to execute when the button is left-clicked." + }, + "on-right-clicked": { + "label": "Right Click Command", + "description": "Command to execute when the button is right-clicked." + }, + "on-middle-clicked": { + "label": "Middle Click Command", + "description": "Command to execute when the button is middle-clicked." + }, + "on-state-command": { + "label": "On State Check Command", + "description": "Command to execute to check if the button should be in the 'on' state. Returns 0 for on, non-zero for off." + }, + "general-tooltip-text": { + "label": "General Tooltip Text", + "description": "General description for the button's tooltip." + }, + "enable-on-state-logic": { + "label": "Enable On-State Logic", + "description": "Enable a second icon and 'hot' state based on a check command." } }, "dialog": { diff --git a/Modules/ControlCenter/Widgets/CustomButton.qml b/Modules/ControlCenter/Widgets/CustomButton.qml index 12b35343..cab9df16 100644 --- a/Modules/ControlCenter/Widgets/CustomButton.qml +++ b/Modules/ControlCenter/Widgets/CustomButton.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import Quickshell.Io import qs.Commons import qs.Services import qs.Widgets @@ -10,24 +11,103 @@ Item { // Widget properties property string widgetId: "CustomButton" - property var widgetSettings: {} // This will be populated from settings + property var widgetSettings // Use settings or provide defaults - readonly property string customIcon: widgetSettings.icon || "heart" - readonly property string exec: widgetSettings.exec || "" - readonly property string tooltipText: widgetSettings.tooltipText || "Custom Button" + property string onClickedCommand: "" + property string onRightClickedCommand: "" + property string onMiddleClickedCommand: "" + property string initialIcon: "heart" + property string onStateIcon: "heart" + property string onStateCommand: "" + property string generalTooltipText: "Custom Button" + property bool enableOnStateLogic: false + + // Internal state + property string _currentIcon: initialIcon + property bool _isHot: false + + Connections { + target: root + function _updatePropertiesFromSettings() { + onClickedCommand = widgetSettings.onClicked || "" + onRightClickedCommand = widgetSettings.onRightClicked || "" + onMiddleClickedCommand = widgetSettings.onMiddleClicked || "" + initialIcon = (widgetSettings.icon && widgetSettings.icon !== "") ? widgetSettings.icon : "heart" + onStateIcon = (widgetSettings.onStateIcon && widgetSettings.onStateIcon !== "") ? widgetSettings.onStateIcon : "heart" + onStateCommand = widgetSettings.onStateCommand || "" + generalTooltipText = widgetSettings.generalTooltipText || "Custom Button" + enableOnStateLogic = widgetSettings.enableOnStateLogic || false + + updateState() + } + function onWidgetSettingsChanged() { _updatePropertiesFromSettings() } + } + + Process { + id: onStateCheckProcess + running: false + command: ["sh", "-c", onStateCommand] + onExited: function(exitCode, stdout, stderr) { + if (exitCode === 0) { + _isHot = true + _currentIcon = onStateIcon || initialIcon + } else { + _isHot = false + _currentIcon = initialIcon + } + + } + } + + function updateState() { + if (enableOnStateLogic && onStateCommand) { + onStateCheckProcess.running = true // Start the process + } else { + _isHot = false + _currentIcon = initialIcon + } + } + + function _buildTooltipText() { + let tooltip = generalTooltipText + if (onClickedCommand) { + tooltip += `\nLeft click: ${onClickedCommand}` + } + if (onRightClickedCommand) { + tooltip += `\nRight click: ${onRightClickedCommand}` + } + if (onMiddleClickedCommand) { + tooltip += `\nMiddle click: ${onMiddleClickedCommand}` + } + + return tooltip + } implicitWidth: button.implicitWidth implicitHeight: button.implicitHeight - NIconButton { + NIconButtonHot { id: button - icon: customIcon - tooltipText: tooltipText + icon: _currentIcon + hot: _isHot + tooltipText: _buildTooltipText() onClicked: { - if (exec) { - Quickshell.execDetached(["sh", "-c", exec]) - Logger.i("CC:CustomButton", `Executing command: ${exec}`) + if (onClickedCommand) { + Quickshell.execDetached(["sh", "-c", onClickedCommand]) + updateState() + } + } + onRightClicked: { + if (onRightClickedCommand) { + Quickshell.execDetached(["sh", "-c", onRightClickedCommand]) + updateState() + } + } + onMiddleClicked: { + if (onMiddleClickedCommand) { + Quickshell.execDetached(["sh", "-c", onMiddleClickedCommand]) + updateState() } } } diff --git a/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml b/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml index 516db036..1631106c 100644 --- a/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml +++ b/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml @@ -5,66 +5,163 @@ import qs.Widgets ColumnLayout { id: root - spacing: Style.marginM property var widgetData: null property var widgetMetadata: null - property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon - property string valueTooltip: widgetData.tooltipText !== undefined ? widgetData.tooltipText : widgetMetadata.tooltipText + QtObject { + id: _settings + + property string icon: (widgetData && widgetData.icon !== undefined) ? widgetData.icon : widgetMetadata.icon + property string onStateIcon: (widgetData && widgetData.onStateIcon !== undefined) ? widgetData.onStateIcon : widgetMetadata.onStateIcon + property string onClicked: (widgetData && widgetData.onClicked !== undefined) ? widgetData.onClicked : widgetMetadata.onClicked + property string onRightClicked: (widgetData && widgetData.onRightClicked !== undefined) ? widgetData.onRightClicked : widgetMetadata.onRightClicked + property string onMiddleClicked: (widgetData && widgetData.onMiddleClicked !== undefined) ? widgetData.onMiddleClicked : widgetMetadata.onMiddleClicked + property string onStateCommand: (widgetData && widgetData.onStateCommand !== undefined) ? widgetData.onStateCommand : widgetMetadata.onStateCommand + property string generalTooltipText: (widgetData && widgetData.generalTooltipText !== undefined) ? widgetData.generalTooltipText : widgetMetadata.generalTooltipText + property bool enableOnStateLogic: (widgetData && widgetData.enableOnStateLogic !== undefined) ? widgetData.enableOnStateLogic : widgetMetadata.enableOnStateLogic + + + } function saveSettings() { - var settings = Object.assign({}, widgetData || {}) - settings.icon = valueIcon - settings.exec = execInput.text - settings.tooltipText = tooltipInput.text - return settings + var saved = { + id: widgetData.id, + icon: _settings.icon, + onStateIcon: _settings.onStateIcon, + onClicked: _settings.onClicked, + onRightClicked: _settings.onRightClicked, + onMiddleClicked: _settings.onMiddleClicked, + onStateCommand: _settings.onStateCommand, + generalTooltipText: _settings.generalTooltipText, + enableOnStateLogic: _settings.enableOnStateLogic + } + + return saved } RowLayout { spacing: Style.marginM NLabel { - label: I18n.tr("settings.control-center.shortcuts.custom-button.icon.label", "Icon") - description: I18n.tr("settings.control-center.shortcuts.custom-button.icon.description", "The icon for the button.") + label: I18n.tr("settings.control-center.shortcuts.custom-button.icon.label") + description: I18n.tr("settings.control-center.shortcuts.custom-button.icon.description") } NIcon { Layout.alignment: Qt.AlignVCenter - icon: valueIcon + icon: _settings.icon || widgetMetadata.icon pointSize: Style.fontSizeXL - visible: valueIcon !== "" + visible: (_settings.icon || widgetMetadata.icon) !== "" } NButton { - text: I18n.tr("settings.control-center.shortcuts.custom-button.browse", "Browse") + text: I18n.tr("settings.control-center.shortcuts.custom-button.browse") onClicked: iconPicker.open() } } NIconPicker { id: iconPicker - initialIcon: valueIcon + initialIcon: _settings.icon onIconSelected: function (iconName) { - valueIcon = iconName + _settings.icon = iconName } } NTextInput { - id: execInput + id: generalTooltipTextInput Layout.fillWidth: true - label: I18n.tr("settings.control-center.shortcuts.custom-button.command.label", "Command") - description: I18n.tr("settings.control-center.shortcuts.custom-button.command.description", "The command to execute when the button is clicked.") - placeholderText: I18n.tr("placeholders.enter-command") - text: widgetData?.exec || widgetMetadata.exec + label: I18n.tr("settings.control-center.shortcuts.custom-button.general-tooltip-text.label") + description: I18n.tr("settings.control-center.shortcuts.custom-button.general-tooltip-text.description") + placeholderText: I18n.tr("placeholders.enter-text") + text: _settings.generalTooltipText + onTextChanged: _settings.generalTooltipText = text } NTextInput { - id: tooltipInput + id: onClickedCommandInput Layout.fillWidth: true - label: I18n.tr("settings.control-center.shortcuts.custom-button.tooltip.label", "Tooltip") - description: I18n.tr("settings.control-center.shortcuts.custom-button.tooltip.description", "The tooltip to show when hovering over the button.") - placeholderText: I18n.tr("placeholders.enter-tooltip") - text: widgetData?.tooltipText || widgetMetadata.tooltipText + label: I18n.tr("settings.control-center.shortcuts.custom-button.on-clicked.label") + description: I18n.tr("settings.control-center.shortcuts.custom-button.on-clicked.description") + placeholderText: I18n.tr("placeholders.enter-command") + text: _settings.onClicked + onTextChanged: _settings.onClicked = text + } + + NTextInput { + id: onRightClickedCommandInput + Layout.fillWidth: true + label: I18n.tr("settings.control-center.shortcuts.custom-button.on-right-clicked.label") + description: I18n.tr("settings.control-center.shortcuts.custom-button.on-right-clicked.description") + placeholderText: I18n.tr("placeholders.enter-command") + text: _settings.onRightClicked + onTextChanged: _settings.onRightClicked = text + } + + NTextInput { + id: onMiddleClickedCommandInput + Layout.fillWidth: true + label: I18n.tr("settings.control-center.shortcuts.custom-button.on-middle-clicked.label") + description: I18n.tr("settings.control-center.shortcuts.custom-button.on-middle-clicked.description") + placeholderText: I18n.tr("placeholders.enter-command") + text: _settings.onMiddleClicked + onTextChanged: _settings.onMiddleClicked = text + } + + NDivider {} + + NToggle { + id: enableOnStateLogicToggle + Layout.fillWidth: true + label: I18n.tr("settings.control-center.shortcuts.custom-button.enable-on-state-logic.label") + description: I18n.tr("settings.control-center.shortcuts.custom-button.enable-on-state-logic.description") + checked: _settings.enableOnStateLogic + onToggled: checked => _settings.enableOnStateLogic = checked + } + + // On-State Icon + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + visible: _settings.enableOnStateLogic + + NLabel { + label: I18n.tr("settings.control-center.shortcuts.custom-button.on-state-icon.label") + description: I18n.tr("settings.control-center.shortcuts.custom-button.on-state-icon.description") + } + + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: _settings.onStateIcon || widgetMetadata.onStateIcon + pointSize: Style.fontSizeXL + visible: (_settings.onStateIcon || widgetMetadata.onStateIcon) !== "" + } + + NButton { + Layout.fillWidth: true + text: I18n.tr("settings.control-center.shortcuts.custom-button.browse") + onClicked: onStateIconPicker.open() + } + } + + NIconPicker { + id: onStateIconPicker + initialIcon: _settings.onStateIcon + onIconSelected: function (iconName) { + _settings.onStateIcon = iconName + } + } + + NTextInput { + id: onStateCommandInput + Layout.fillWidth: true + label: I18n.tr("settings.control-center.shortcuts.custom-button.on-state-command.label") + description: I18n.tr("settings.control-center.shortcuts.custom-button.on-state-command.description") + placeholderText: I18n.tr("placeholders.enter-command") + text: _settings.onStateCommand + onTextChanged: _settings.onStateCommand = text + enabled: _settings.enableOnStateLogic + visible: _settings.enableOnStateLogic } } diff --git a/Services/ControlCenterWidgetRegistry.qml b/Services/ControlCenterWidgetRegistry.qml index cd7cb6c1..d460fdfa 100644 --- a/Services/ControlCenterWidgetRegistry.qml +++ b/Services/ControlCenterWidgetRegistry.qml @@ -25,8 +25,13 @@ Singleton { "CustomButton": { "allowUserSettings": true, "icon": "heart", - "exec": "", - "tooltipText": "Custom Button" + "onStateIcon": "heart", + "onClicked": "", + "onRightClicked": "", + "onMiddleClicked": "", + "onStateCommand": "", + "generalTooltipText": "Custom Button", + "enableOnStateLogic": false } }) From 697fb55ab162a792bf1eb9331f97d53779062390 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Mon, 3 Nov 2025 13:39:56 +0800 Subject: [PATCH 03/13] fix: enter tooltip --- .../ControlCenter/WidgetSettings/CustomButtonSettings.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml b/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml index 1631106c..13c6aaac 100644 --- a/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml +++ b/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml @@ -74,7 +74,7 @@ ColumnLayout { Layout.fillWidth: true label: I18n.tr("settings.control-center.shortcuts.custom-button.general-tooltip-text.label") description: I18n.tr("settings.control-center.shortcuts.custom-button.general-tooltip-text.description") - placeholderText: I18n.tr("placeholders.enter-text") + placeholderText: I18n.tr("placeholders.enter-tooltip") text: _settings.generalTooltipText onTextChanged: _settings.generalTooltipText = text } From ba7282daa6105034d3f7bb402991519bb6291300 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Mon, 3 Nov 2025 14:04:39 +0800 Subject: [PATCH 04/13] feat(controlcenter): Optimize command execution logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The command is “fire-and-forget”: it starts a process and returns immediately, so QML has no way of knowing when that process actually finishes. To avoid this race condition, the simplest approach that least intrudes on existing QML code is to introduce a short timer. --- .../ControlCenter/Widgets/CustomButton.qml | 59 ++++++++++++++----- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/Modules/ControlCenter/Widgets/CustomButton.qml b/Modules/ControlCenter/Widgets/CustomButton.qml index cab9df16..2a8adafc 100644 --- a/Modules/ControlCenter/Widgets/CustomButton.qml +++ b/Modules/ControlCenter/Widgets/CustomButton.qml @@ -5,15 +5,15 @@ import qs.Commons import qs.Services import qs.Widgets -// Dummy comment to force re-evaluation + Item { id: root - // Widget properties - property string widgetId: "CustomButton" - property var widgetSettings - // Use settings or provide defaults + property string widgetId: "CustomButton" + property var widgetSettings: null + + property string onClickedCommand: "" property string onRightClickedCommand: "" property string onMiddleClickedCommand: "" @@ -23,13 +23,18 @@ Item { property string generalTooltipText: "Custom Button" property bool enableOnStateLogic: false - // Internal state + property string _currentIcon: initialIcon property bool _isHot: false Connections { target: root function _updatePropertiesFromSettings() { + + if (!widgetSettings) { + return + } + onClickedCommand = widgetSettings.onClicked || "" onRightClickedCommand = widgetSettings.onRightClicked || "" onMiddleClickedCommand = widgetSettings.onMiddleClicked || "" @@ -41,34 +46,58 @@ Item { updateState() } - function onWidgetSettingsChanged() { _updatePropertiesFromSettings() } + + + function onWidgetSettingsChanged() { + if (widgetSettings) { + _updatePropertiesFromSettings() + } + } } Process { id: onStateCheckProcess + running: false command: ["sh", "-c", onStateCommand] onExited: function(exitCode, stdout, stderr) { - if (exitCode === 0) { - _isHot = true - _currentIcon = onStateIcon || initialIcon + if (enableOnStateLogic && onStateCommand) { + if (exitCode === 0) { + _isHot = true + _currentIcon = onStateIcon + } else { + _isHot = false + _currentIcon = initialIcon + } } else { _isHot = false _currentIcon = initialIcon } + } + } + Timer { + id: stateUpdateTimer + interval: 200 + running: false + repeat: false + onTriggered: { + if (enableOnStateLogic && onStateCommand && !onStateCheckProcess.running) { + onStateCheckProcess.running = true + } } } function updateState() { - if (enableOnStateLogic && onStateCommand) { - onStateCheckProcess.running = true // Start the process - } else { - _isHot = false - _currentIcon = initialIcon + if (!enableOnStateLogic || !onStateCommand) { + _isHot = false; + _currentIcon = initialIcon; + return; } + stateUpdateTimer.restart(); } + function _buildTooltipText() { let tooltip = generalTooltipText if (onClickedCommand) { From 5a9470d64e23268c2989866d2c3947fbe42d5153 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Mon, 3 Nov 2025 16:17:55 +0800 Subject: [PATCH 05/13] feat(controlcenter): implement multi-state support for custom button --- .../ControlCenter/Widgets/CustomButton.qml | 152 ++++++++++-------- .../WidgetSettings/CustomButtonSettings.qml | 135 ++++++++++------ Modules/Settings/Tabs/ControlCenterTab.qml | 8 +- Services/ControlCenterWidgetRegistry.qml | 3 +- 4 files changed, 181 insertions(+), 117 deletions(-) diff --git a/Modules/ControlCenter/Widgets/CustomButton.qml b/Modules/ControlCenter/Widgets/CustomButton.qml index 2a8adafc..b36c6ae6 100644 --- a/Modules/ControlCenter/Widgets/CustomButton.qml +++ b/Modules/ControlCenter/Widgets/CustomButton.qml @@ -13,24 +13,22 @@ Item { property string widgetId: "CustomButton" property var widgetSettings: null - property string onClickedCommand: "" property string onRightClickedCommand: "" property string onMiddleClickedCommand: "" - property string initialIcon: "heart" - property string onStateIcon: "heart" - property string onStateCommand: "" + property string stateChecksJson: "[]" // Store state checks as JSON string + property string generalTooltipText: "Custom Button" property bool enableOnStateLogic: false - - property string _currentIcon: initialIcon + property string _currentIcon: "heart" // Default icon property bool _isHot: false + property var _parsedStateChecks: [] // Local array for parsed state checks Connections { target: root - function _updatePropertiesFromSettings() { + function _updatePropertiesFromSettings() { if (!widgetSettings) { return } @@ -38,16 +36,19 @@ Item { onClickedCommand = widgetSettings.onClicked || "" onRightClickedCommand = widgetSettings.onRightClicked || "" onMiddleClickedCommand = widgetSettings.onMiddleClicked || "" - initialIcon = (widgetSettings.icon && widgetSettings.icon !== "") ? widgetSettings.icon : "heart" - onStateIcon = (widgetSettings.onStateIcon && widgetSettings.onStateIcon !== "") ? widgetSettings.onStateIcon : "heart" - onStateCommand = widgetSettings.onStateCommand || "" + stateChecksJson = widgetSettings.stateChecksJson || "[]" // Populate from widgetSettings + try { + _parsedStateChecks = JSON.parse(stateChecksJson) + } catch (e) { + console.error("CustomButton: Failed to parse stateChecksJson:", e.message) + _parsedStateChecks = [] + } generalTooltipText = widgetSettings.generalTooltipText || "Custom Button" enableOnStateLogic = widgetSettings.enableOnStateLogic || false updateState() } - function onWidgetSettingsChanged() { if (widgetSettings) { _updatePropertiesFromSettings() @@ -55,23 +56,24 @@ Item { } } - Process { - id: onStateCheckProcess + property int _currentStateCheckIndex: -1 + property string _activeStateIcon: "" + Process { + id: stateCheckProcessExecutor running: false - command: ["sh", "-c", onStateCommand] + command: _currentStateCheckIndex !== -1 && _parsedStateChecks.length > _currentStateCheckIndex ? ["sh", "-c", _parsedStateChecks[_currentStateCheckIndex].command] : [] onExited: function(exitCode, stdout, stderr) { - if (enableOnStateLogic && onStateCommand) { - if (exitCode === 0) { - _isHot = true - _currentIcon = onStateIcon - } else { - _isHot = false - _currentIcon = initialIcon - } + var currentCheckItem = _parsedStateChecks[_currentStateCheckIndex] + var currentCommand = currentCheckItem.command + if (exitCode === 0) { + // Command succeeded, this is the active state + _isHot = true + _activeStateIcon = currentCheckItem.icon || widgetSettings.icon || "heart" } else { - _isHot = false - _currentIcon = initialIcon + // Command failed, try next one + _currentStateCheckIndex++ + _checkNextState() } } } @@ -82,62 +84,80 @@ Item { running: false repeat: false onTriggered: { - if (enableOnStateLogic && onStateCommand && !onStateCheckProcess.running) { - onStateCheckProcess.running = true + if (enableOnStateLogic && _parsedStateChecks.length > 0) { + _currentStateCheckIndex = 0 + _checkNextState() + } else { + _isHot = false + _activeStateIcon = widgetSettings.icon || "heart" } } } + function _checkNextState() { + if (_currentStateCheckIndex < _parsedStateChecks.length) { + var currentCheckItem = _parsedStateChecks[_currentStateCheckIndex] + if (currentCheckItem && currentCheckItem.command) { + stateCheckProcessExecutor.running = true + } else { + _currentStateCheckIndex++ + _checkNextState() + } + } else { + // All checks failed + _isHot = false + _activeStateIcon = widgetSettings.icon || "heart" + } + } + function updateState() { - if (!enableOnStateLogic || !onStateCommand) { + if (!enableOnStateLogic || _parsedStateChecks.length === 0) { _isHot = false; - _currentIcon = initialIcon; + _activeStateIcon = widgetSettings.icon || "heart"; return; } stateUpdateTimer.restart(); - } - - - function _buildTooltipText() { - let tooltip = generalTooltipText - if (onClickedCommand) { - tooltip += `\nLeft click: ${onClickedCommand}` - } - if (onRightClickedCommand) { - tooltip += `\nRight click: ${onRightClickedCommand}` - } - if (onMiddleClickedCommand) { - tooltip += `\nMiddle click: ${onMiddleClickedCommand}` - } - - return tooltip - } - - implicitWidth: button.implicitWidth - implicitHeight: button.implicitHeight - - NIconButtonHot { - id: button - icon: _currentIcon - hot: _isHot - tooltipText: _buildTooltipText() - onClicked: { + } + function _buildTooltipText() { + let tooltip = generalTooltipText if (onClickedCommand) { - Quickshell.execDetached(["sh", "-c", onClickedCommand]) - updateState() + tooltip += `\nLeft click: ${onClickedCommand}` } - } - onRightClicked: { if (onRightClickedCommand) { - Quickshell.execDetached(["sh", "-c", onRightClickedCommand]) - updateState() + tooltip += `\nRight click: ${onRightClickedCommand}` } - } - onMiddleClicked: { if (onMiddleClickedCommand) { - Quickshell.execDetached(["sh", "-c", onMiddleClickedCommand]) - updateState() + tooltip += `\nMiddle click: ${onMiddleClickedCommand}` + } + + return tooltip + } + + implicitWidth: button.implicitWidth + implicitHeight: button.implicitHeight + + NIconButtonHot { + id: button + icon: _activeStateIcon + hot: _isHot + tooltipText: _buildTooltipText() + onClicked: { + if (onClickedCommand) { + Quickshell.execDetached(["sh", "-c", onClickedCommand]) + updateState() + } + } + onRightClicked: { + if (onRightClickedCommand) { + Quickshell.execDetached(["sh", "-c", onRightClickedCommand]) + updateState() + } + } + onMiddleClicked: { + if (onMiddleClickedCommand) { + Quickshell.execDetached(["sh", "-c", onMiddleClickedCommand]) + updateState() + } } } - } } diff --git a/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml b/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml index 13c6aaac..6d71a674 100644 --- a/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml +++ b/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Layouts +import QtQml.Models // Import ListModel import qs.Commons import qs.Widgets @@ -13,35 +14,58 @@ ColumnLayout { id: _settings property string icon: (widgetData && widgetData.icon !== undefined) ? widgetData.icon : widgetMetadata.icon - property string onStateIcon: (widgetData && widgetData.onStateIcon !== undefined) ? widgetData.onStateIcon : widgetMetadata.onStateIcon property string onClicked: (widgetData && widgetData.onClicked !== undefined) ? widgetData.onClicked : widgetMetadata.onClicked property string onRightClicked: (widgetData && widgetData.onRightClicked !== undefined) ? widgetData.onRightClicked : widgetMetadata.onRightClicked property string onMiddleClicked: (widgetData && widgetData.onMiddleClicked !== undefined) ? widgetData.onMiddleClicked : widgetMetadata.onMiddleClicked - property string onStateCommand: (widgetData && widgetData.onStateCommand !== undefined) ? widgetData.onStateCommand : widgetMetadata.onStateCommand + property ListModel _stateChecksListModel: ListModel {} + property string stateChecksJson: "[]" property string generalTooltipText: (widgetData && widgetData.generalTooltipText !== undefined) ? widgetData.generalTooltipText : widgetMetadata.generalTooltipText property bool enableOnStateLogic: (widgetData && widgetData.enableOnStateLogic !== undefined) ? widgetData.enableOnStateLogic : widgetMetadata.enableOnStateLogic - + Component.onCompleted: { + stateChecksJson = (widgetData && widgetData.stateChecksJson !== undefined) ? widgetData.stateChecksJson : widgetMetadata.stateChecksJson || "[]" + try { + var initialChecks = JSON.parse(stateChecksJson) + if (initialChecks && Array.isArray(initialChecks)) { + for (var i = 0; i < initialChecks.length; i++) { + var item = initialChecks[i] + if (item && typeof item === "object") { + _settings._stateChecksListModel.append({ + command: item.command || "", + icon: item.icon || "" + }) + } else { + console.warn("⚠️ Invalid stateChecks entry at index " + i + ":", item) + } + } + } + } catch (e) { + console.error("CustomButtonSettings: Failed to parse stateChecksJson:", e.message) + } + } } function saveSettings() { - var saved = { + var savedStateChecksArray = [] + for (var i = 0; i < _settings._stateChecksListModel.count; i++) { + savedStateChecksArray.push(_settings._stateChecksListModel.get(i)) + } + _settings.stateChecksJson = JSON.stringify(savedStateChecksArray) + + return { id: widgetData.id, icon: _settings.icon, - onStateIcon: _settings.onStateIcon, onClicked: _settings.onClicked, onRightClicked: _settings.onRightClicked, onMiddleClicked: _settings.onMiddleClicked, - onStateCommand: _settings.onStateCommand, + stateChecksJson: _settings.stateChecksJson, generalTooltipText: _settings.generalTooltipText, enableOnStateLogic: _settings.enableOnStateLogic } - - return saved } RowLayout { - spacing: Style.marginM + spacing: Style?.marginM ?? 8 NLabel { label: I18n.tr("settings.control-center.shortcuts.custom-button.icon.label") @@ -51,7 +75,7 @@ ColumnLayout { NIcon { Layout.alignment: Qt.AlignVCenter icon: _settings.icon || widgetMetadata.icon - pointSize: Style.fontSizeXL + pointSize: Style?.fontSizeXL ?? 24 visible: (_settings.icon || widgetMetadata.icon) !== "" } @@ -70,7 +94,6 @@ ColumnLayout { } NTextInput { - id: generalTooltipTextInput Layout.fillWidth: true label: I18n.tr("settings.control-center.shortcuts.custom-button.general-tooltip-text.label") description: I18n.tr("settings.control-center.shortcuts.custom-button.general-tooltip-text.description") @@ -80,7 +103,6 @@ ColumnLayout { } NTextInput { - id: onClickedCommandInput Layout.fillWidth: true label: I18n.tr("settings.control-center.shortcuts.custom-button.on-clicked.label") description: I18n.tr("settings.control-center.shortcuts.custom-button.on-clicked.description") @@ -90,7 +112,6 @@ ColumnLayout { } NTextInput { - id: onRightClickedCommandInput Layout.fillWidth: true label: I18n.tr("settings.control-center.shortcuts.custom-button.on-right-clicked.label") description: I18n.tr("settings.control-center.shortcuts.custom-button.on-right-clicked.description") @@ -100,7 +121,6 @@ ColumnLayout { } NTextInput { - id: onMiddleClickedCommandInput Layout.fillWidth: true label: I18n.tr("settings.control-center.shortcuts.custom-button.on-middle-clicked.label") description: I18n.tr("settings.control-center.shortcuts.custom-button.on-middle-clicked.description") @@ -120,48 +140,69 @@ ColumnLayout { onToggled: checked => _settings.enableOnStateLogic = checked } - // On-State Icon - RowLayout { + ColumnLayout { Layout.fillWidth: true - spacing: Style.marginS visible: _settings.enableOnStateLogic + spacing: (Style?.marginM ?? 8) * 2 NLabel { - label: I18n.tr("settings.control-center.shortcuts.custom-button.on-state-icon.label") - description: I18n.tr("settings.control-center.shortcuts.custom-button.on-state-icon.description") + label: "State Checks" } - NIcon { - Layout.alignment: Qt.AlignVCenter - icon: _settings.onStateIcon || widgetMetadata.onStateIcon - pointSize: Style.fontSizeXL - visible: (_settings.onStateIcon || widgetMetadata.onStateIcon) !== "" + Repeater { + model: _settings._stateChecksListModel + delegate: ColumnLayout { + Layout.fillWidth: true + spacing: Style?.marginM ?? 8 + property int currentIndex: index + + RowLayout { + Layout.fillWidth: true + spacing: Style?.marginS ?? 4 + + NTextInput { + Layout.fillWidth: true + label: "Command" + text: model.command + onEditingFinished: _settings._stateChecksListModel.set(currentIndex, { "command": text, "icon": model.icon }) + } + + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: model.icon + visible: model.icon !== undefined && model.icon !== "" + } + + NButton { + text: "Browse Icon" + Layout.preferredWidth: Style?.buttonWidthM ?? 100 + onClicked: iconPickerDelegate.open() + } + + NIconPicker { + id: iconPickerDelegate + initialIcon: model.icon + onIconSelected: function (iconName) { + _settings._stateChecksListModel.set(currentIndex, { "command": model.command, "icon": iconName }) + } + } + + NButton { + text: "Remove" + Layout.preferredWidth: Style?.buttonWidthM ?? 100 + onClicked: _settings._stateChecksListModel.remove(currentIndex) + } + } + + NDivider {} + } } NButton { - Layout.fillWidth: true - text: I18n.tr("settings.control-center.shortcuts.custom-button.browse") - onClicked: onStateIconPicker.open() + text: "Add State Check" + onClicked: _settings._stateChecksListModel.append({ command: "", icon: "" }) } } - NIconPicker { - id: onStateIconPicker - initialIcon: _settings.onStateIcon - onIconSelected: function (iconName) { - _settings.onStateIcon = iconName - } - } - - NTextInput { - id: onStateCommandInput - Layout.fillWidth: true - label: I18n.tr("settings.control-center.shortcuts.custom-button.on-state-command.label") - description: I18n.tr("settings.control-center.shortcuts.custom-button.on-state-command.description") - placeholderText: I18n.tr("placeholders.enter-command") - text: _settings.onStateCommand - onTextChanged: _settings.onStateCommand = text - enabled: _settings.enableOnStateLogic - visible: _settings.enableOnStateLogic - } -} + NDivider {} +} \ No newline at end of file diff --git a/Modules/Settings/Tabs/ControlCenterTab.qml b/Modules/Settings/Tabs/ControlCenterTab.qml index 69110393..298a2946 100644 --- a/Modules/Settings/Tabs/ControlCenterTab.qml +++ b/Modules/Settings/Tabs/ControlCenterTab.qml @@ -329,8 +329,12 @@ ColumnLayout { } function _updateWidgetSettingsInSection(section, index, settings) { - // Update the widget settings in the Settings data - Settings.data.controlCenter.shortcuts[section][index] = settings + // Create a new array to trigger QML's change detection for persistence. + // This is crucial for Settings.data to detect the change and persist it. + var newSectionArray = Settings.data.controlCenter.shortcuts[section].slice() + newSectionArray[index] = settings + Settings.data.controlCenter.shortcuts[section] = newSectionArray + Settings.saveImmediate() } // Base list model for all combo boxes diff --git a/Services/ControlCenterWidgetRegistry.qml b/Services/ControlCenterWidgetRegistry.qml index d460fdfa..5a1431f1 100644 --- a/Services/ControlCenterWidgetRegistry.qml +++ b/Services/ControlCenterWidgetRegistry.qml @@ -25,11 +25,10 @@ Singleton { "CustomButton": { "allowUserSettings": true, "icon": "heart", - "onStateIcon": "heart", "onClicked": "", "onRightClicked": "", "onMiddleClicked": "", - "onStateCommand": "", + "stateChecks": [], "generalTooltipText": "Custom Button", "enableOnStateLogic": false } From e5e9b5961b69daafe1996aadcdd62e08ee00594f Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Mon, 3 Nov 2025 17:12:55 +0800 Subject: [PATCH 06/13] feat(controlcenter): beautify UI interface for custom button state checks --- Assets/Translations/en.json | 7 ++ .../WidgetSettings/CustomButtonSettings.qml | 91 ++++++++++++------- 2 files changed, 66 insertions(+), 32 deletions(-) diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 6b972cc6..c8b35651 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -875,6 +875,13 @@ "enable-on-state-logic": { "label": "Enable On-State Logic", "description": "Enable a second icon and 'hot' state based on a check command." + }, + "state-checks": { + "label": "State Checks", + "command": "Command to execute for this state check", + "browse-icon": "Browse", + "remove": "Remove", + "add": "Add State Check" } }, "dialog": { diff --git a/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml b/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml index 6d71a674..e7a613c0 100644 --- a/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml +++ b/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml @@ -146,63 +146,90 @@ ColumnLayout { spacing: (Style?.marginM ?? 8) * 2 NLabel { - label: "State Checks" + label: I18n.tr("settings.control-center.shortcuts.custom-button.state-checks.label") } Repeater { model: _settings._stateChecksListModel - delegate: ColumnLayout { - Layout.fillWidth: true - spacing: Style?.marginM ?? 8 + delegate: Item { property int currentIndex: index + implicitHeight: contentRow.implicitHeight + ((divider.visible) ? divider.height : 0) + Layout.fillWidth: true + RowLayout { - Layout.fillWidth: true - spacing: Style?.marginS ?? 4 + id: contentRow + anchors.fill: parent + spacing: Style?.marginM ?? 8 NTextInput { Layout.fillWidth: true - label: "Command" + placeholderText: I18n.tr("settings.control-center.shortcuts.custom-button.state-checks.command") text: model.command onEditingFinished: _settings._stateChecksListModel.set(currentIndex, { "command": text, "icon": model.icon }) } - NIcon { + RowLayout { Layout.alignment: Qt.AlignVCenter - icon: model.icon - visible: model.icon !== undefined && model.icon !== "" - } + spacing: Style?.marginS ?? 4 - NButton { - text: "Browse Icon" - Layout.preferredWidth: Style?.buttonWidthM ?? 100 - onClicked: iconPickerDelegate.open() - } - - NIconPicker { - id: iconPickerDelegate - initialIcon: model.icon - onIconSelected: function (iconName) { - _settings._stateChecksListModel.set(currentIndex, { "command": model.command, "icon": iconName }) + NIcon { + icon: model.icon + pointSize: Style?.fontSizeL ?? 20 + visible: model.icon !== undefined && model.icon !== "" } - } - NButton { - text: "Remove" - Layout.preferredWidth: Style?.buttonWidthM ?? 100 - onClicked: _settings._stateChecksListModel.remove(currentIndex) + NIconButton { + icon: "folder" + tooltipText: I18n.tr("settings.control-center.shortcuts.custom-button.state-checks.browse-icon") + baseSize: Style?.buttonSizeS ?? 24 + onClicked: iconPickerDelegate.open() + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("settings.control-center.shortcuts.custom-button.state-checks.remove") + baseSize: Style?.buttonSizeS ?? 24 + colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight) + colorBg: Color.mError + colorFg: Color.mOnError + colorBgHover: Qt.alpha(Color.mError, Style.opacityMedium) + colorFgHover: Color.mOnError + onClicked: _settings._stateChecksListModel.remove(currentIndex) + } } } - NDivider {} + NIconPicker { + id: iconPickerDelegate + initialIcon: model.icon + onIconSelected: function (iconName) { + _settings._stateChecksListModel.set(currentIndex, { "command": model.command, "icon": iconName }) + } + } + + NDivider { + id: divider + anchors.bottom: parent.bottom + visible: index < _settings._stateChecksListModel.count - 1 // Only show divider if not the last item + } } } - NButton { - text: "Add State Check" - onClicked: _settings._stateChecksListModel.append({ command: "", icon: "" }) + RowLayout { + Layout.fillWidth: true + spacing: Style?.marginM ?? 8 + + NButton { + text: I18n.tr("settings.control-center.shortcuts.custom-button.state-checks.add") + onClicked: _settings._stateChecksListModel.append({ command: "", icon: "" }) + } + + Item { + Layout.fillWidth: true + } } } NDivider {} -} \ No newline at end of file +} From 34cdbfea3205e91a3aea818f17358265bf2f7891 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Mon, 3 Nov 2025 17:21:48 +0800 Subject: [PATCH 07/13] fix(controlcenter): fix widget sorting in dropdown list --- Modules/Settings/Tabs/ControlCenterTab.qml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Modules/Settings/Tabs/ControlCenterTab.qml b/Modules/Settings/Tabs/ControlCenterTab.qml index 298a2946..7c24ba54 100644 --- a/Modules/Settings/Tabs/ControlCenterTab.qml +++ b/Modules/Settings/Tabs/ControlCenterTab.qml @@ -68,12 +68,13 @@ ColumnLayout { Component.onCompleted: { // Fill out availableWidgets ListModel availableWidgets.clear() - ControlCenterWidgetRegistry.getAvailableWidgets().forEach(entry => { - availableWidgets.append({ - "key": entry, - "name": entry - }) - }) + var sortedEntries = ControlCenterWidgetRegistry.getAvailableWidgets().slice().sort() + sortedEntries.forEach(entry => { + availableWidgets.append({ + "key": entry, + "name": entry + }) + }) // Starts empty cardsModel = [] From 3970aee7964890ce94ad03f7079cb59ebfaaefb4 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Tue, 4 Nov 2025 02:42:26 +0800 Subject: [PATCH 08/13] feat(i18n): Add German translations for custom button --- Assets/Translations/de.json | 56 ++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index f9224eef..0c41a25c 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -833,7 +833,61 @@ "label": "Widgets für Kurzbefehle" }, "sectionLeft": "Links", - "sectionRight": "Rechts" + "sectionRight": "Rechts", + "custom-button": { + "icon": { + "label": "Symbol", + "description": "Wählen Sie ein Symbol aus der Bibliothek." + }, + "browse": "Durchsuchen", + "command": { + "label": "Befehl", + "description": "Der Befehl, der ausgeführt werden soll, wenn die Schaltfläche geklickt wird." + }, + "tooltip": { + "label": "Tooltip", + "description": "Der Tooltip, der angezeigt wird, wenn Sie mit der Maus über die Schaltfläche fahren." + }, + "on-state-icon": { + "label": "Symbol im Aktiv-Zustand", + "description": "Das Symbol für die Schaltfläche, wenn sie sich im 'aktiv' Zustand befindet." + }, + "on-clicked": { + "label": "Linksklick-Befehl", + "description": "Befehl, der ausgeführt wird, wenn die Schaltfläche links geklickt wird." + }, + "on-right-clicked": { + "label": "Rechtsklick-Befehl", + "description": "Befehl, der ausgeführt wird, wenn die Schaltfläche rechts geklickt wird." + }, + "on-middle-clicked": { + "label": "Mittelklick-Befehl", + "description": "Befehl, der ausgeführt wird, wenn die Schaltfläche mit der mittleren Maustaste geklickt wird." + }, + "on-state-command": { + "label": "Befehl für Aktiv-Zustand-Prüfung", + "description": "Befehl, der ausgeführt wird, um zu prüfen, ob sich die Schaltfläche im 'aktiv' Zustand befinden soll. Gibt 0 für aktiv, ungleich 0 für inaktiv zurück." + }, + "general-tooltip-text": { + "label": "Allgemeiner Tooltip-Text", + "description": "Allgemeine Beschreibung für den Tooltip der Schaltfläche." + }, + "enable-on-state-logic": { + "label": "Aktiv-Zustand-Logik aktivieren", + "description": "Aktiviert ein zweites Symbol und 'aktiv' Zustand basierend auf einem Prüfbefehl." + }, + "state-checks": { + "label": "Zustandsprüfungen", + "command": "Befehl, der für diese Zustandsprüfung ausgeführt werden soll", + "browse-icon": "Durchsuchen", + "remove": "Entfernen", + "add": "Zustandsprüfung hinzufügen" + } + }, + "dialog": { + "cancel": "Abbrechen", + "apply": "Anwenden" + } } }, "user-interface": { From 59ab5f77cbdea14e76c2fbc311a447f2c614709e Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Tue, 4 Nov 2025 02:43:16 +0800 Subject: [PATCH 09/13] feat(i18n): Add Spanish translations for custom button --- Assets/Translations/es.json | 56 ++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 7252dbf9..4fc1b872 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -833,7 +833,61 @@ "label": "Widgets de accesos directos" }, "sectionLeft": "Izquierda", - "sectionRight": "Derecha" + "sectionRight": "Derecha", + "custom-button": { + "icon": { + "label": "Icono", + "description": "Selecciona un icono de la biblioteca." + }, + "browse": "Explorar", + "command": { + "label": "Comando", + "description": "El comando a ejecutar cuando se hace clic en el botón." + }, + "tooltip": { + "label": "Tooltip", + "description": "El tooltip a mostrar al pasar el cursor sobre el botón." + }, + "on-state-icon": { + "label": "Icono de estado activado", + "description": "El icono para el botón cuando está en el estado 'encendido'." + }, + "on-clicked": { + "label": "Comando de clic izquierdo", + "description": "Comando a ejecutar cuando se hace clic izquierdo en el botón." + }, + "on-right-clicked": { + "label": "Comando de clic derecho", + "description": "Comando a ejecutar cuando se hace clic derecho en el botón." + }, + "on-middle-clicked": { + "label": "Comando de clic central", + "description": "Comando a ejecutar cuando se hace clic central en el botón." + }, + "on-state-command": { + "label": "Comando de comprobación de estado activado", + "description": "Comando a ejecutar para comprobar si el botón debe estar en el estado 'encendido'. Devuelve 0 para encendido, no cero para apagado." + }, + "general-tooltip-text": { + "label": "Texto de tooltip general", + "description": "Descripción general para el tooltip del botón." + }, + "enable-on-state-logic": { + "label": "Habilitar lógica de estado activado", + "description": "Habilita un segundo icono y un estado 'caliente' basado en un comando de comprobación." + }, + "state-checks": { + "label": "Comprobaciones de estado", + "command": "Comando a ejecutar para esta comprobación de estado", + "browse-icon": "Explorar", + "remove": "Eliminar", + "add": "Añadir comprobación de estado" + } + }, + "dialog": { + "cancel": "Cancelar", + "apply": "Aplicar" + } } }, "user-interface": { From 8e9069b2fe296dfc7742ba800a32b7ae50600190 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Tue, 4 Nov 2025 02:43:54 +0800 Subject: [PATCH 10/13] feat(i18n): Add French translations for custom button --- Assets/Translations/fr.json | 56 ++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 1e0b879d..1d46feb0 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -833,7 +833,61 @@ "label": "Widgets de raccourcis" }, "sectionLeft": "Gauche", - "sectionRight": "Droite" + "sectionRight": "Droite", + "custom-button": { + "icon": { + "label": "Icône", + "description": "Sélectionnez une icône dans la bibliothèque." + }, + "browse": "Parcourir", + "command": { + "label": "Commande", + "description": "La commande à exécuter lorsque le bouton est cliqué." + }, + "tooltip": { + "label": "Info-bulle", + "description": "L'info-bulle à afficher lors du survol du bouton." + }, + "on-state-icon": { + "label": "Icône d'état activé", + "description": "L'icône du bouton lorsqu'il est dans l'état 'activé'." + }, + "on-clicked": { + "label": "Commande de clic gauche", + "description": "Commande à exécuter lorsque le bouton est cliqué avec le bouton gauche." + }, + "on-right-clicked": { + "label": "Commande de clic droit", + "description": "Commande à exécuter lorsque le bouton est cliqué avec le bouton droit." + }, + "on-middle-clicked": { + "label": "Commande de clic central", + "description": "Commande à exécuter lorsque le bouton est cliqué avec le bouton du milieu." + }, + "on-state-command": { + "label": "Commande de vérification de l'état activé", + "description": "Commande à exécuter pour vérifier si le bouton doit être dans l'état 'activé'. Retourne 0 pour activé, non-zéro pour désactivé." + }, + "general-tooltip-text": { + "label": "Texte de l'info-bulle générale", + "description": "Description générale de l'info-bulle du bouton." + }, + "enable-on-state-logic": { + "label": "Activer la logique d'état activé", + "description": "Active une deuxième icône et un état 'chaud' basés sur une commande de vérification." + }, + "state-checks": { + "label": "Vérifications d'état", + "command": "Commande à exécuter pour cette vérification d'état", + "browse-icon": "Parcourir", + "remove": "Supprimer", + "add": "Ajouter une vérification d'état" + } + }, + "dialog": { + "cancel": "Annuler", + "apply": "Appliquer" + } } }, "user-interface": { From eb37dc9951af5a546f4e79dafc851a1531f272fc Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Tue, 4 Nov 2025 02:44:26 +0800 Subject: [PATCH 11/13] feat(i18n): Add Portuguese translations for custom button --- Assets/Translations/pt.json | 56 ++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 7b3722cd..d0ba8dac 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -833,7 +833,61 @@ "label": "Widgets de atalhos" }, "sectionLeft": "Esquerda", - "sectionRight": "Direito/Certo/À direita" + "sectionRight": "Direito/Certo/À direita", + "custom-button": { + "icon": { + "label": "Ícone", + "description": "Selecione um ícone da biblioteca." + }, + "browse": "Procurar", + "command": { + "label": "Comando", + "description": "O comando a ser executado quando o botão é clicado." + }, + "tooltip": { + "label": "Dica de ferramenta", + "description": "A dica de ferramenta a ser exibida ao passar o mouse sobre o botão." + }, + "on-state-icon": { + "label": "Ícone de estado ativado", + "description": "O ícone para o botão quando ele está no estado 'ligado'." + }, + "on-clicked": { + "label": "Comando de clique esquerdo", + "description": "Comando a ser executado quando o botão é clicado com o botão esquerdo." + }, + "on-right-clicked": { + "label": "Comando de clique direito", + "description": "Comando a ser executado quando o botão é clicado com o botão direito." + }, + "on-middle-clicked": { + "label": "Comando de clique do meio", + "description": "Comando a ser executado quando o botão é clicado com o botão do meio." + }, + "on-state-command": { + "label": "Comando de verificação de estado ativado", + "description": "Comando a ser executado para verificar se o botão deve estar no estado 'ligado'. Retorna 0 para ligado, diferente de zero para desligado." + }, + "general-tooltip-text": { + "label": "Texto geral da dica de ferramenta", + "description": "Descrição geral para a dica de ferramenta do botão." + }, + "enable-on-state-logic": { + "label": "Ativar lógica de estado ativado", + "description": "Ativa um segundo ícone e um estado 'quente' com base em um comando de verificação." + }, + "state-checks": { + "label": "Verificações de estado", + "command": "Comando a ser executado para esta verificação de estado", + "browse-icon": "Procurar", + "remove": "Remover", + "add": "Adicionar verificação de estado" + } + }, + "dialog": { + "cancel": "Cancelar", + "apply": "Aplicar" + } } }, "user-interface": { From a7bbfe03f0a82d0eec8b73771bdd0bc206c9543c Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Tue, 4 Nov 2025 02:45:06 +0800 Subject: [PATCH 12/13] feat(i18n): Add Ukrainian translations for custom button --- Assets/Translations/uk-UA.json | 56 +++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 9e238ecb..eb36a39a 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -833,7 +833,61 @@ "description": "Налаштуйте та керуйте віджетами швидкого доступу." }, "sectionLeft": "Лівий", - "sectionRight": "Правий" + "sectionRight": "Правий", + "custom-button": { + "icon": { + "label": "Іконка", + "description": "Виберіть іконку з бібліотеки." + }, + "browse": "Огляд", + "command": { + "label": "Команда", + "description": "Команда, яка буде виконана при натисканні кнопки." + }, + "tooltip": { + "label": "Підказка", + "description": "Підказка, яка буде відображатися при наведенні на кнопку." + }, + "on-state-icon": { + "label": "Іконка увімкненого стану", + "description": "Іконка для кнопки, коли вона у стані 'увімкнено'." + }, + "on-clicked": { + "label": "Команда лівого кліку", + "description": "Команда, яка буде виконана при натисканні лівої кнопки миші." + }, + "on-right-clicked": { + "label": "Команда правого кліку", + "description": "Команда, яка буде виконана при натисканні правої кнопки миші." + }, + "on-middle-clicked": { + "label": "Команда середнього кліку", + "description": "Команда, яка буде виконана при натисканні середньої кнопки миші." + }, + "on-state-command": { + "label": "Команда перевірки увімкненого стану", + "description": "Команда для перевірки, чи повинна кнопка бути у стані 'увімкнено'. Повертає 0 для увімкненого стану, ненульове значення для вимкненого." + }, + "general-tooltip-text": { + "label": "Загальний текст підказки", + "description": "Загальний опис для підказки кнопки." + }, + "enable-on-state-logic": { + "label": "Увімкнути логіку увімкненого стану", + "description": "Увімкнути другу іконку та 'гарячий' стан на основі команди перевірки." + }, + "state-checks": { + "label": "Перевірки стану", + "command": "Команда для виконання для цієї перевірки стану", + "browse-icon": "Огляд", + "remove": "Видалити", + "add": "Додати перевірку стану" + } + }, + "dialog": { + "cancel": "Скасувати", + "apply": "Застосувати" + } } }, "user-interface": { From be78a72c51d15a653ffc1ae2a71eefdd34479788 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Tue, 4 Nov 2025 02:46:05 +0800 Subject: [PATCH 13/13] feat(i18n): Add Chinese translations for custom button --- Assets/Translations/zh-CN.json | 56 +++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index d107d181..f0b9be6c 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -833,7 +833,61 @@ "label": "快捷方式小部件" }, "sectionLeft": "左", - "sectionRight": "右" + "sectionRight": "右", + "custom-button": { + "icon": { + "label": "图标", + "description": "从库中选择一个图标。" + }, + "browse": "浏览", + "command": { + "label": "命令", + "description": "单击按钮时执行的命令。" + }, + "tooltip": { + "label": "工具提示", + "description": "悬停在按钮上时显示的工具提示。" + }, + "on-state-icon": { + "label": "开启状态图标", + "description": "按钮处于“开启”状态时的图标。" + }, + "on-clicked": { + "label": "左键单击命令", + "description": "左键单击按钮时执行的命令。" + }, + "on-right-clicked": { + "label": "右键单击命令", + "description": "右键单击按钮时执行的命令。" + }, + "on-middle-clicked": { + "label": "中键单击命令", + "description": "中键单击按钮时执行的命令。" + }, + "on-state-command": { + "label": "开启状态检查命令", + "description": "执行以检查按钮是否应处于“开启”状态的命令。返回 0 表示开启,非零表示关闭。" + }, + "general-tooltip-text": { + "label": "常规工具提示文本", + "description": "按钮工具提示的常规说明。" + }, + "enable-on-state-logic": { + "label": "启用开启状态逻辑", + "description": "根据检查命令启用第二个图标和“高亮”状态。" + }, + "state-checks": { + "label": "状态检查", + "command": "要为此状态检查执行的命令", + "browse-icon": "浏览", + "remove": "移除", + "add": "添加状态检查" + } + }, + "dialog": { + "cancel": "取消", + "apply": "应用" + } } }, "user-interface": {