diff --git a/shell/README.md b/shell/README.md index f121370a..ad9fb3d9 100644 --- a/shell/README.md +++ b/shell/README.md @@ -23,12 +23,9 @@ shell/ services/ PluginRegistry.qml discovers, validates plugins, looks up enabled state in shell.json BarWidgetRegistry.qml unified registry for bar widgets (1p + 3p) - ui/ - settings/ - DynamicSettingsForm.qml renders plugin-declared schemas plugins/ bar/ first-party plugins (see plugins/README.md) - settings/ + settings/ settings panel + plugin-declared widget forms launcher/ image-picker/ menu/ diff --git a/shell/plugins/settings/SettingsPanel.qml b/shell/plugins/settings/SettingsPanel.qml index 39121f3b..55d33a75 100644 --- a/shell/plugins/settings/SettingsPanel.qml +++ b/shell/plugins/settings/SettingsPanel.qml @@ -6,7 +6,6 @@ import Quickshell.Io import qs.Commons import qs.Ui -import "../../ui/settings" as SettingsUi import "./components" as Cmp Item { @@ -1147,10 +1146,10 @@ Item { Component { id: dynamicSettingsComponent - SettingsUi.DynamicSettingsForm { + Cmp.DynamicSettingsForm { schema: root.widgetSchema(entry.id || "") - foregroundColor: root.foreground - fontFamilyName: root.fontFamily + foreground: root.foreground + fontFamily: root.fontFamily } } diff --git a/shell/plugins/settings/components/DynamicSettingsForm.qml b/shell/plugins/settings/components/DynamicSettingsForm.qml new file mode 100644 index 00000000..a9e13b85 --- /dev/null +++ b/shell/plugins/settings/components/DynamicSettingsForm.qml @@ -0,0 +1,209 @@ +import QtQuick +import QtQuick.Controls as QQC +import qs.Commons +import qs.Ui as Ui + +Column { + id: root + + signal fieldChanged(string key, var value) + + property var schema: [] + property var entry: ({}) + property color foreground: Color.popups.text + property color accent: Color.accent + property string fontFamily: Style.font.family + + spacing: Style.spacing.xl + width: parent ? parent.width : 0 + + function currentValue(field) { + if (entry && entry[field.key] !== undefined) return entry[field.key] + if (field.defaultValue !== undefined) return field.defaultValue + + switch (field.type) { + case "boolean": return false + case "integer": + case "number": return field.min !== undefined ? field.min : 0 + case "enum": return field.options && field.options.length > 0 ? field.options[0] : "" + default: return "" + } + } + + Repeater { + model: root.schema + + Column { + required property var modelData + + width: root.width + spacing: Style.spacing.labelGap + + Text { + visible: text !== "" + text: modelData && modelData.label ? modelData.label : (modelData && modelData.key ? modelData.key : "") + color: Qt.darker(root.foreground, 1.3) + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + font.bold: true + } + + Text { + visible: !!(modelData && modelData.description) + text: modelData ? (modelData.description || "") : "" + color: Qt.darker(root.foreground, 1.6) + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + width: parent.width + } + + Loader { + width: parent.width + height: item ? item.implicitHeight : 0 + sourceComponent: { + if (!modelData || !modelData.type) return stringField + switch (String(modelData.type)) { + case "boolean": return booleanField + case "enum": return enumField + case "integer": return integerField + case "number": return numberField + default: return stringField + } + } + onLoaded: { + if (!item) return + item.fieldKey = modelData.key + item.field = modelData + } + } + } + } + + Text { + visible: !root.schema || root.schema.length === 0 + text: "No settings." + color: Qt.darker(root.foreground, 1.5) + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + } + + Component { + id: stringField + + Ui.TextField { + property string fieldKey: "" + property var field: ({}) + + width: parent.width + foreground: root.foreground + accent: root.accent + font.family: root.fontFamily + font.pixelSize: Style.font.body + text: root.currentValue(field) === undefined ? "" : String(root.currentValue(field)) + + onEditingFinished: if (fieldKey) root.fieldChanged(fieldKey, text) + } + } + + Component { + id: booleanField + + Ui.Toggle { + property string fieldKey: "" + property var field: ({}) + + width: parent.width + label: "Enabled" + foreground: root.foreground + accent: root.accent + fontFamily: root.fontFamily + checked: !!root.currentValue(field) + + onClicked: if (fieldKey) root.fieldChanged(fieldKey, !checked) + } + } + + Component { + id: enumField + + Ui.Dropdown { + property string fieldKey: "" + property var field: ({}) + + width: parent.width + showLabel: false + foreground: root.foreground + accent: root.accent + fontFamily: root.fontFamily + options: field && field.options ? field.options : [] + value: String(root.currentValue(field)) + + onChanged: function(value) { + if (fieldKey) root.fieldChanged(fieldKey, value) + } + } + } + + Component { + id: integerField + + Ui.NumberField { + property string fieldKey: "" + property var field: ({}) + + foreground: root.foreground + accent: root.accent + fontFamily: root.fontFamily + from: field && field.min !== undefined ? field.min : 0 + to: field && field.max !== undefined ? field.max : 9999 + stepSize: field && field.step !== undefined ? field.step : 1 + value: { + var v = root.currentValue(field) + var n = typeof v === "number" ? v : parseInt(String(v || 0), 10) + return isFinite(n) ? n : 0 + } + + onModified: function(value) { if (fieldKey) root.fieldChanged(fieldKey, value) } + } + } + + Component { + id: numberField + + Row { + property string fieldKey: "" + property var field: ({}) + property real currentNumber: { + var v = root.currentValue(field) + var n = typeof v === "number" ? v : parseFloat(String(v || 0)) + return isFinite(n) ? n : 0 + } + + width: parent.width + spacing: Style.spacing.rowGap + + QQC.Slider { + id: slider + + width: parent.width - readout.width - Style.spacing.rowGap + from: field && field.min !== undefined ? field.min : 0 + to: field && field.max !== undefined ? field.max : 1 + stepSize: field && field.step !== undefined ? field.step : 0.01 + value: parent.currentNumber + + onMoved: if (parent.fieldKey) root.fieldChanged(parent.fieldKey, value) + } + + Text { + id: readout + + text: slider.value.toFixed(2) + color: root.foreground + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + anchors.verticalCenter: parent.verticalCenter + } + } + } +} diff --git a/shell/ui/settings/DynamicSettingsForm.qml b/shell/ui/settings/DynamicSettingsForm.qml deleted file mode 100644 index aee49d90..00000000 --- a/shell/ui/settings/DynamicSettingsForm.qml +++ /dev/null @@ -1,196 +0,0 @@ -import QtQuick -import QtQuick.Controls as QQC -import QtQuick.Layouts -import qs.Commons -import qs.Ui as Ui - -// Generic schema-driven settings form. The host panel passes: -// - schema: array of { key, label, type, defaultValue?, options?, min?, max?, step?, description? } -// - entry: current widget entry object (set by the loader); fields override defaults -// - signal fieldChanged(string key, var value) — emitted on user edits -// -// Supported types: boolean, enum, integer, number, string, path, command, color. -Column { - id: root - - signal fieldChanged(string key, var value) - - property var schema: [] - property var entry: ({}) - property color foregroundColor: Color.foreground - property string fontFamilyName: Style.font.family - spacing: Style.spacing.xl - width: parent ? parent.width : 0 - - function currentValue(field) { - if (entry && entry[field.key] !== undefined) return entry[field.key] - if (field.defaultValue !== undefined) return field.defaultValue - switch (field.type) { - case "boolean": return false - case "integer": - case "number": return field.min !== undefined ? field.min : 0 - case "enum": return field.options && field.options.length > 0 ? field.options[0] : "" - default: return "" - } - } - - Repeater { - model: root.schema - - Column { - required property var modelData - width: root.width - spacing: Style.spacing.labelGap - - Text { - text: modelData && modelData.label ? modelData.label : (modelData && modelData.key ? modelData.key : "") - color: Qt.darker(root.foregroundColor, 1.3) - font.family: root.fontFamilyName - font.pixelSize: Style.font.bodySmall - font.bold: true - visible: text !== "" - } - - Text { - visible: !!(modelData && modelData.description) - text: modelData ? (modelData.description || "") : "" - color: Qt.darker(root.foregroundColor, 1.6) - font.family: root.fontFamilyName - font.pixelSize: Style.font.caption - wrapMode: Text.WordWrap - width: parent.width - } - - Loader { - width: parent.width - height: item ? item.implicitHeight : 0 - sourceComponent: { - if (!modelData || !modelData.type) return stringField - switch (String(modelData.type)) { - case "boolean": return booleanField - case "enum": return enumField - case "integer": return integerField - case "number": return numberField - case "color": - case "string": - case "path": - case "command": - default: return stringField - } - } - onLoaded: if (item && "fieldKey" in item) { - item.fieldKey = modelData.key - item.field = modelData - } - } - - Component { - id: stringField - Ui.TextField { - property string fieldKey: "" - property var field: ({}) - width: parent.width - foreground: root.foregroundColor - accent: Color.accent - font.family: root.fontFamilyName - font.pixelSize: Style.font.body - text: root.currentValue(field) === undefined ? "" : String(root.currentValue(field)) - onEditingFinished: if (fieldKey) root.fieldChanged(fieldKey, text) - } - } - - Component { - id: booleanField - Ui.Toggle { - property string fieldKey: "" - property var field: ({}) - width: parent.width - label: "Enabled" - foreground: root.foregroundColor - accent: Color.accent - fontFamily: root.fontFamilyName - checked: !!root.currentValue(field) - onClicked: if (fieldKey) root.fieldChanged(fieldKey, !checked) - } - } - - Component { - id: enumField - Ui.Dropdown { - property string fieldKey: "" - property var field: ({}) - width: parent.width - showLabel: false - foreground: root.foregroundColor - accent: Color.accent - fontFamily: root.fontFamilyName - options: field && field.options ? field.options : [] - value: String(root.currentValue(field)) - onChanged: function(value) { - if (fieldKey) root.fieldChanged(fieldKey, value) - } - } - } - - Component { - id: integerField - Ui.NumberField { - property string fieldKey: "" - property var field: ({}) - foreground: root.foregroundColor - accent: Color.accent - fontFamily: root.fontFamilyName - from: field && field.min !== undefined ? field.min : 0 - to: field && field.max !== undefined ? field.max : 9999 - stepSize: field && field.step !== undefined ? field.step : 1 - value: { - var v = root.currentValue(field) - var n = typeof v === "number" ? v : parseInt(String(v || 0), 10) - return isFinite(n) ? n : 0 - } - onModified: function(value) { if (fieldKey) root.fieldChanged(fieldKey, value) } - } - } - - Component { - id: numberField - Row { - property string fieldKey: "" - property var field: ({}) - width: parent.width - spacing: Style.spacing.rowGap - property real currentNumber: { - var v = root.currentValue(field) - var n = typeof v === "number" ? v : parseFloat(String(v || 0)) - return isFinite(n) ? n : 0 - } - QQC.Slider { - id: slider - width: parent.width - readout.width - Style.spacing.rowGap - from: field && field.min !== undefined ? field.min : 0 - to: field && field.max !== undefined ? field.max : 1 - stepSize: field && field.step !== undefined ? field.step : 0.01 - value: parent.currentNumber - onMoved: if (parent.fieldKey) root.fieldChanged(parent.fieldKey, value) - } - Text { - id: readout - text: slider.value.toFixed(2) - color: root.foregroundColor - font.family: root.fontFamilyName - font.pixelSize: Style.font.bodySmall - anchors.verticalCenter: parent.verticalCenter - } - } - } - } - } - - Text { - visible: !root.schema || root.schema.length === 0 - text: "No settings." - color: Qt.darker(root.foregroundColor, 1.5) - font.family: root.fontFamilyName - font.pixelSize: Style.font.bodySmall - } -}