diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index d4148acd..ae63f949 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -857,7 +857,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": { diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 0c5cc96c..669e9010 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -857,7 +857,61 @@ "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." + }, + "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." + }, + "state-checks": { + "label": "State Checks", + "command": "Command to execute for this state check", + "browse-icon": "Browse", + "remove": "Remove", + "add": "Add State Check" + } + }, + "dialog": { + "cancel": "Cancel", + "apply": "Apply" + } } }, "user-interface": { @@ -1471,7 +1525,8 @@ "search": "Search...", "select": "Select", "cancel": "Cancel", - "test": "Test" + "test": "Test", + "enter-tooltip": "Enter tooltip" }, "options": { "colors": { diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index c1b662b0..8e24ca62 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -857,7 +857,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": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 6ae6057f..56386b72 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -857,7 +857,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": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 15a280dd..ad10c0a5 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -857,7 +857,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": { diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 4b1f55ef..0eb362ea 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -857,7 +857,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": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 8504cfa8..6a0827cf 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -857,7 +857,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": { diff --git a/Modules/ControlCenter/Cards/ShortcutsCard.qml b/Modules/ControlCenter/Cards/ShortcutsCard.qml index 18e593d8..3c9cefee 100644 --- a/Modules/ControlCenter/Cards/ShortcutsCard.qml +++ b/Modules/ControlCenter/Cards/ShortcutsCard.qml @@ -38,7 +38,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 } @@ -76,7 +77,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..b36c6ae6 --- /dev/null +++ b/Modules/ControlCenter/Widgets/CustomButton.qml @@ -0,0 +1,163 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services +import qs.Widgets + + +Item { + id: root + + + property string widgetId: "CustomButton" + property var widgetSettings: null + + property string onClickedCommand: "" + property string onRightClickedCommand: "" + property string onMiddleClickedCommand: "" + property string stateChecksJson: "[]" // Store state checks as JSON string + + property string generalTooltipText: "Custom Button" + property bool enableOnStateLogic: false + + property string _currentIcon: "heart" // Default icon + property bool _isHot: false + property var _parsedStateChecks: [] // Local array for parsed state checks + + Connections { + target: root + + function _updatePropertiesFromSettings() { + if (!widgetSettings) { + return + } + + onClickedCommand = widgetSettings.onClicked || "" + onRightClickedCommand = widgetSettings.onRightClicked || "" + onMiddleClickedCommand = widgetSettings.onMiddleClicked || "" + 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() + } + } + } + + property int _currentStateCheckIndex: -1 + property string _activeStateIcon: "" + + Process { + id: stateCheckProcessExecutor + running: false + command: _currentStateCheckIndex !== -1 && _parsedStateChecks.length > _currentStateCheckIndex ? ["sh", "-c", _parsedStateChecks[_currentStateCheckIndex].command] : [] + onExited: function(exitCode, stdout, stderr) { + 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 { + // Command failed, try next one + _currentStateCheckIndex++ + _checkNextState() + } + } + } + + Timer { + id: stateUpdateTimer + interval: 200 + running: false + repeat: false + onTriggered: { + 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 || _parsedStateChecks.length === 0) { + _isHot = false; + _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: _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/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..e7a613c0 --- /dev/null +++ b/Modules/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml @@ -0,0 +1,235 @@ +import QtQuick +import QtQuick.Layouts +import QtQml.Models // Import ListModel +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + + property var widgetData: null + property var widgetMetadata: null + + QtObject { + id: _settings + + property string icon: (widgetData && widgetData.icon !== undefined) ? widgetData.icon : widgetMetadata.icon + 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 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 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, + onClicked: _settings.onClicked, + onRightClicked: _settings.onRightClicked, + onMiddleClicked: _settings.onMiddleClicked, + stateChecksJson: _settings.stateChecksJson, + generalTooltipText: _settings.generalTooltipText, + enableOnStateLogic: _settings.enableOnStateLogic + } + } + + RowLayout { + spacing: Style?.marginM ?? 8 + + NLabel { + 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: _settings.icon || widgetMetadata.icon + pointSize: Style?.fontSizeXL ?? 24 + visible: (_settings.icon || widgetMetadata.icon) !== "" + } + + NButton { + text: I18n.tr("settings.control-center.shortcuts.custom-button.browse") + onClicked: iconPicker.open() + } + } + + NIconPicker { + id: iconPicker + initialIcon: _settings.icon + onIconSelected: function (iconName) { + _settings.icon = iconName + } + } + + NTextInput { + 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-tooltip") + text: _settings.generalTooltipText + onTextChanged: _settings.generalTooltipText = text + } + + NTextInput { + 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") + placeholderText: I18n.tr("placeholders.enter-command") + text: _settings.onClicked + onTextChanged: _settings.onClicked = text + } + + NTextInput { + 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 { + 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 + } + + ColumnLayout { + Layout.fillWidth: true + visible: _settings.enableOnStateLogic + spacing: (Style?.marginM ?? 8) * 2 + + NLabel { + label: I18n.tr("settings.control-center.shortcuts.custom-button.state-checks.label") + } + + Repeater { + model: _settings._stateChecksListModel + delegate: Item { + property int currentIndex: index + + implicitHeight: contentRow.implicitHeight + ((divider.visible) ? divider.height : 0) + Layout.fillWidth: true + + RowLayout { + id: contentRow + anchors.fill: parent + spacing: Style?.marginM ?? 8 + + NTextInput { + Layout.fillWidth: true + 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 }) + } + + RowLayout { + Layout.alignment: Qt.AlignVCenter + spacing: Style?.marginS ?? 4 + + NIcon { + icon: model.icon + pointSize: Style?.fontSizeL ?? 20 + visible: model.icon !== undefined && model.icon !== "" + } + + 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) + } + } + } + + 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 + } + } + } + + 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 {} +} diff --git a/Modules/Settings/Tabs/ControlCenterTab.qml b/Modules/Settings/Tabs/ControlCenterTab.qml index 7b1a91a7..69f08b77 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 = [] @@ -226,7 +227,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 +246,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"] @@ -329,8 +330,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 f00263ed..5a1431f1 100644 --- a/Services/ControlCenterWidgetRegistry.qml +++ b/Services/ControlCenterWidgetRegistry.qml @@ -17,10 +17,22 @@ Singleton { "PowerProfile": powerProfileComponent, "ScreenRecorder": screenRecorderComponent, "WiFi": wiFiComponent, - "WallpaperSelector": wallpaperSelectorComponent + "WallpaperSelector": wallpaperSelectorComponent, + "CustomButton": customButtonComponent }) - property var widgetMetadata: ({}) + property var widgetMetadata: ({ + "CustomButton": { + "allowUserSettings": true, + "icon": "heart", + "onClicked": "", + "onRightClicked": "", + "onMiddleClicked": "", + "stateChecks": [], + "generalTooltipText": "Custom Button", + "enableOnStateLogic": false + } + }) // Component definitions - these are loaded once at startup property Component bluetoothComponent: Component { @@ -47,6 +59,9 @@ Singleton { property Component wallpaperSelectorComponent: Component { WallpaperSelector {} } + property Component customButtonComponent: Component { + CustomButton {} + } function init() { Logger.i("ControlCenterWidgetRegistry", "Service started")