From 0ca12d275c72096ed1bc2fd6db5cc3151a9747db Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 1 Oct 2025 17:47:39 -0400 Subject: [PATCH] Abstract away plugin dev a little more --- DMSShell.qml | 1 + Modules/DankBar/CenterSection.qml | 23 +- Modules/DankBar/DankBar.qml | 15 + Modules/DankBar/LeftSection.qml | 11 + Modules/DankBar/RightSection.qml | 11 + Modules/DankBar/WidgetHost.qml | 53 ++- Modules/Plugins/BaseHorizontalPill.qml | 53 +++ Modules/Plugins/BaseVerticalPill.qml | 53 +++ Modules/Plugins/ListSetting.qml | 131 +++++++ Modules/Plugins/ListSettingWithInput.qml | 230 +++++++++++++ Modules/Plugins/PluginComponent.qml | 69 ++++ Modules/Plugins/PluginPopout.qml | 116 +++++++ Modules/Plugins/PluginSettings.qml | 33 ++ Modules/Plugins/SelectionSetting.qml | 113 ++++++ Modules/Plugins/StringSetting.qml | 65 ++++ Modules/Plugins/ToggleSetting.qml | 72 ++++ Modules/Settings/PluginsTab.qml | 184 ++++------ Services/PluginService.qml | 27 +- docs/PLUGINS.md | 417 +++++++++++++++++------ 19 files changed, 1447 insertions(+), 230 deletions(-) create mode 100644 Modules/Plugins/BaseHorizontalPill.qml create mode 100644 Modules/Plugins/BaseVerticalPill.qml create mode 100644 Modules/Plugins/ListSetting.qml create mode 100644 Modules/Plugins/ListSettingWithInput.qml create mode 100644 Modules/Plugins/PluginComponent.qml create mode 100644 Modules/Plugins/PluginPopout.qml create mode 100644 Modules/Plugins/PluginSettings.qml create mode 100644 Modules/Plugins/SelectionSetting.qml create mode 100644 Modules/Plugins/StringSetting.qml create mode 100644 Modules/Plugins/ToggleSetting.qml diff --git a/DMSShell.qml b/DMSShell.qml index a0dad130..5a2e57cd 100644 --- a/DMSShell.qml +++ b/DMSShell.qml @@ -22,6 +22,7 @@ import qs.Modules.ProcessList import qs.Modules.Settings import qs.Modules.DankBar import qs.Modules.DankBar.Popouts +import qs.Modules.Plugins import qs.Services diff --git a/Modules/DankBar/CenterSection.qml b/Modules/DankBar/CenterSection.qml index 074dcc7e..8b340ab8 100644 --- a/Modules/DankBar/CenterSection.qml +++ b/Modules/DankBar/CenterSection.qml @@ -9,6 +9,10 @@ Item { property var components: null property bool noBackground: false required property var axis + property string section: "center" + property var parentScreen: null + property real widgetThickness: 30 + property real barThickness: 48 readonly property bool isVertical: axis?.isVertical ?? false readonly property real spacing: noBackground ? 2 : Theme.spacingXS @@ -371,7 +375,24 @@ Item { item.axis = root.axis } if (root.axis && "isVertical" in item) { - item.isVertical = root.axis.isVertical + try { + item.isVertical = root.axis.isVertical + } catch (e) { + } + } + + // Inject properties for plugin widgets + if ("section" in item) { + item.section = root.section + } + if ("parentScreen" in item) { + item.parentScreen = root.parentScreen + } + if ("widgetThickness" in item) { + item.widgetThickness = root.widgetThickness + } + if ("barThickness" in item) { + item.barThickness = root.barThickness } // Inject PluginService for plugin widgets diff --git a/Modules/DankBar/DankBar.qml b/Modules/DankBar/DankBar.qml index 0184f338..08933928 100644 --- a/Modules/DankBar/DankBar.qml +++ b/Modules/DankBar/DankBar.qml @@ -537,6 +537,9 @@ Item { widgetsModel: SettingsData.dankBarLeftWidgetsModel components: topBarContent.allComponents noBackground: SettingsData.dankBarNoBackground + parentScreen: barWindow.screen + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness } RightSection { @@ -549,6 +552,9 @@ Item { widgetsModel: SettingsData.dankBarRightWidgetsModel components: topBarContent.allComponents noBackground: SettingsData.dankBarNoBackground + parentScreen: barWindow.screen + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness } CenterSection { @@ -561,6 +567,9 @@ Item { widgetsModel: SettingsData.dankBarCenterWidgetsModel components: topBarContent.allComponents noBackground: SettingsData.dankBarNoBackground + parentScreen: barWindow.screen + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness } } @@ -580,6 +589,9 @@ Item { widgetsModel: SettingsData.dankBarLeftWidgetsModel components: topBarContent.allComponents noBackground: SettingsData.dankBarNoBackground + parentScreen: barWindow.screen + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness } CenterSection { @@ -593,6 +605,9 @@ Item { widgetsModel: SettingsData.dankBarCenterWidgetsModel components: topBarContent.allComponents noBackground: SettingsData.dankBarNoBackground + parentScreen: barWindow.screen + widgetThickness: barWindow.widgetThickness + barThickness: barWindow.effectiveBarThickness } RightSection { diff --git a/Modules/DankBar/LeftSection.qml b/Modules/DankBar/LeftSection.qml index 24e94955..2454088e 100644 --- a/Modules/DankBar/LeftSection.qml +++ b/Modules/DankBar/LeftSection.qml @@ -8,6 +8,9 @@ Item { property var components: null property bool noBackground: false required property var axis + property var parentScreen: null + property real widgetThickness: 30 + property real barThickness: 48 readonly property bool isVertical: axis?.isVertical ?? false @@ -38,6 +41,10 @@ Item { components: root.components isInColumn: false axis: root.axis + section: "left" + parentScreen: root.parentScreen + widgetThickness: root.widgetThickness + barThickness: root.barThickness } } } @@ -63,6 +70,10 @@ Item { components: root.components isInColumn: true axis: root.axis + section: "left" + parentScreen: root.parentScreen + widgetThickness: root.widgetThickness + barThickness: root.barThickness } } } diff --git a/Modules/DankBar/RightSection.qml b/Modules/DankBar/RightSection.qml index 4b1a3fc1..a2f602dc 100644 --- a/Modules/DankBar/RightSection.qml +++ b/Modules/DankBar/RightSection.qml @@ -8,6 +8,9 @@ Item { property var components: null property bool noBackground: false required property var axis + property var parentScreen: null + property real widgetThickness: 30 + property real barThickness: 48 readonly property bool isVertical: axis?.isVertical ?? false @@ -40,6 +43,10 @@ Item { components: root.components isInColumn: false axis: root.axis + section: "right" + parentScreen: root.parentScreen + widgetThickness: root.widgetThickness + barThickness: root.barThickness } } } @@ -65,6 +72,10 @@ Item { components: root.components isInColumn: true axis: root.axis + section: "right" + parentScreen: root.parentScreen + widgetThickness: root.widgetThickness + barThickness: root.barThickness } } } diff --git a/Modules/DankBar/WidgetHost.qml b/Modules/DankBar/WidgetHost.qml index c58b85ab..055f3ef6 100644 --- a/Modules/DankBar/WidgetHost.qml +++ b/Modules/DankBar/WidgetHost.qml @@ -11,6 +11,10 @@ Loader { property var components: null property bool isInColumn: false property var axis: null + property string section: "center" + property var parentScreen: null + property real widgetThickness: 30 + property real barThickness: 48 asynchronous: false @@ -21,20 +25,59 @@ Loader { signal contentItemReady(var item) + Binding { + target: root.item + when: root.item && "parentScreen" in root.item + property: "parentScreen" + value: root.parentScreen + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "section" in root.item + property: "section" + value: root.section + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "widgetThickness" in root.item + property: "widgetThickness" + value: root.widgetThickness + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "barThickness" in root.item + property: "barThickness" + value: root.barThickness + restoreMode: Binding.RestoreNone + } + + Binding { + target: root.item + when: root.item && "axis" in root.item + property: "axis" + value: root.axis + restoreMode: Binding.RestoreNone + } + onLoaded: { if (item) { contentItemReady(item) if (widgetId === "spacer") { item.spacerSize = Qt.binding(() => spacerSize) } - if (axis && "axis" in item) { - item.axis = axis - } if (axis && "isVertical" in item) { - item.isVertical = axis.isVertical + try { + item.isVertical = axis.isVertical + } catch (e) { + } } - // Inject PluginService for plugin widgets if (item.pluginService !== undefined) { console.log("WidgetHost: Injecting PluginService into plugin widget:", widgetId) item.pluginService = PluginService diff --git a/Modules/Plugins/BaseHorizontalPill.qml b/Modules/Plugins/BaseHorizontalPill.qml new file mode 100644 index 00000000..85b6109c --- /dev/null +++ b/Modules/Plugins/BaseHorizontalPill.qml @@ -0,0 +1,53 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property var axis: null + property string section: "center" + property var popoutTarget: null + property var parentScreen: null + property real widgetThickness: 30 + property real barThickness: 48 + property alias content: contentLoader.sourceComponent + + readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30)) + + signal clicked() + + width: contentLoader.item ? (contentLoader.item.implicitWidth + horizontalPadding * 2) : 0 + height: widgetThickness + radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.dankBarNoBackground) { + return "transparent" + } + + const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency) + } + + Loader { + id: contentLoader + anchors.centerIn: parent + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (popoutTarget && popoutTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0) + const currentScreen = parentScreen || Screen + const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width) + popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen) + } + root.clicked() + } + } +} diff --git a/Modules/Plugins/BaseVerticalPill.qml b/Modules/Plugins/BaseVerticalPill.qml new file mode 100644 index 00000000..2fcf9d20 --- /dev/null +++ b/Modules/Plugins/BaseVerticalPill.qml @@ -0,0 +1,53 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property var axis: null + property string section: "center" + property var popoutTarget: null + property var parentScreen: null + property real widgetThickness: 30 + property real barThickness: 48 + property alias content: contentLoader.sourceComponent + + readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30)) + + signal clicked() + + width: widgetThickness + height: contentLoader.item ? (contentLoader.item.implicitHeight + horizontalPadding * 2) : 0 + radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.dankBarNoBackground) { + return "transparent" + } + + const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency) + } + + Loader { + id: contentLoader + anchors.centerIn: parent + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + if (popoutTarget && popoutTarget.setTriggerPosition) { + const globalPos = mapToGlobal(0, 0) + const currentScreen = parentScreen || Screen + const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, height) + popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen) + } + root.clicked() + } + } +} diff --git a/Modules/Plugins/ListSetting.qml b/Modules/Plugins/ListSetting.qml new file mode 100644 index 00000000..df3cd9b0 --- /dev/null +++ b/Modules/Plugins/ListSetting.qml @@ -0,0 +1,131 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Column { + id: root + + required property string settingKey + required property string label + property string description: "" + property var items: [] + property Component delegate: null + + width: parent.width + spacing: Theme.spacingM + + Component.onCompleted: { + const settings = findSettings() + if (settings) { + items = settings.loadValue(settingKey, []) + } + } + + onItemsChanged: { + const settings = findSettings() + if (settings) { + settings.saveValue(settingKey, items) + } + } + + function findSettings() { + let item = parent + while (item) { + if (item.saveValue !== undefined && item.loadValue !== undefined) { + return item + } + item = item.parent + } + return null + } + + function addItem(item) { + items = items.concat([item]) + } + + function removeItem(index) { + const newItems = items.slice() + newItems.splice(index, 1) + items = newItems + } + + StyledText { + text: root.label + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: root.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + visible: root.description !== "" + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: root.items + delegate: root.delegate ? root.delegate : defaultDelegate + } + + StyledText { + text: "No items added yet" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: root.items.length === 0 + } + } + + Component { + id: defaultDelegate + StyledRect { + width: parent.width + height: 40 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 0 + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + text: modelData + color: Theme.surfaceText + } + + Rectangle { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + width: 60 + height: 28 + color: removeArea.containsMouse ? Theme.errorHover : Theme.error + radius: Theme.cornerRadius + + StyledText { + anchors.centerIn: parent + text: "Remove" + color: Theme.errorText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + } + + MouseArea { + id: removeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.removeItem(index) + } + } + } + } + } +} diff --git a/Modules/Plugins/ListSettingWithInput.qml b/Modules/Plugins/ListSettingWithInput.qml new file mode 100644 index 00000000..e15c8f01 --- /dev/null +++ b/Modules/Plugins/ListSettingWithInput.qml @@ -0,0 +1,230 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Column { + id: root + + required property string settingKey + required property string label + property string description: "" + property var fields: [] + property var items: [] + + width: parent.width + spacing: Theme.spacingM + + Component.onCompleted: { + const settings = findSettings() + if (settings) { + items = settings.loadValue(settingKey, []) + } + } + + onItemsChanged: { + const settings = findSettings() + if (settings) { + settings.saveValue(settingKey, items) + } + } + + function findSettings() { + let item = parent + while (item) { + if (item.saveValue !== undefined && item.loadValue !== undefined) { + return item + } + item = item.parent + } + return null + } + + function addItem(item) { + items = items.concat([item]) + } + + function removeItem(index) { + const newItems = items.slice() + newItems.splice(index, 1) + items = newItems + } + + StyledText { + text: root.label + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: root.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + visible: root.description !== "" + } + + Flow { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: root.fields + + StyledText { + text: modelData.label + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + width: modelData.width || 200 + } + } + } + + Flow { + id: inputRow + width: parent.width + spacing: Theme.spacingS + + property var inputFields: [] + + Repeater { + id: inputRepeater + model: root.fields + + DankTextField { + width: modelData.width || 200 + placeholderText: modelData.placeholder || "" + + Component.onCompleted: { + inputRow.inputFields.push(this) + } + + Keys.onReturnPressed: { + addButton.clicked() + } + } + } + + DankButton { + id: addButton + width: 50 + height: 36 + text: "Add" + + onClicked: { + let newItem = {} + let hasValue = false + + for (let i = 0; i < root.fields.length; i++) { + const field = root.fields[i] + const input = inputRow.inputFields[i] + const value = input.text.trim() + + if (value !== "") { + hasValue = true + } + + if (field.required && value === "") { + return + } + + newItem[field.id] = value || (field.default || "") + } + + if (hasValue) { + root.addItem(newItem) + for (let i = 0; i < inputRow.inputFields.length; i++) { + inputRow.inputFields[i].text = "" + } + if (inputRow.inputFields.length > 0) { + inputRow.inputFields[0].forceActiveFocus() + } + } + } + } + } + + StyledText { + text: "Current Items" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + visible: root.items.length > 0 + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: root.items + + StyledRect { + width: parent.width + height: 40 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + border.width: 0 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Repeater { + model: root.fields + + StyledText { + text: { + const value = root.items[index][modelData.id] + return value || "" + } + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + width: modelData.width || 200 + elide: Text.ElideRight + } + } + } + + Rectangle { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + width: 60 + height: 28 + color: removeArea.containsMouse ? Theme.errorHover : Theme.error + radius: Theme.cornerRadius + + StyledText { + anchors.centerIn: parent + text: "Remove" + color: Theme.errorText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + } + + MouseArea { + id: removeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.removeItem(index) + } + } + } + } + } + + StyledText { + text: "No items added yet" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: root.items.length === 0 + } + } +} diff --git a/Modules/Plugins/PluginComponent.qml b/Modules/Plugins/PluginComponent.qml new file mode 100644 index 00000000..8f9b3986 --- /dev/null +++ b/Modules/Plugins/PluginComponent.qml @@ -0,0 +1,69 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property var axis: null + property string section: "center" + property var parentScreen: null + property real widgetThickness: 30 + property real barThickness: 48 + + property Component horizontalBarPill: null + property Component verticalBarPill: null + property Component popoutContent: null + property real popoutWidth: 400 + property real popoutHeight: 400 + + readonly property bool isVertical: axis?.isVertical ?? false + readonly property bool hasHorizontalPill: horizontalBarPill !== null + readonly property bool hasVerticalPill: verticalBarPill !== null + readonly property bool hasPopout: popoutContent !== null + + width: isVertical ? (hasVerticalPill ? verticalPill.width : 0) : (hasHorizontalPill ? horizontalPill.width : 0) + height: isVertical ? (hasVerticalPill ? verticalPill.height : 0) : (hasHorizontalPill ? horizontalPill.height : 0) + + BaseHorizontalPill { + id: horizontalPill + visible: !isVertical && hasHorizontalPill + axis: root.axis + section: root.section + popoutTarget: hasPopout ? pluginPopout : null + parentScreen: root.parentScreen + widgetThickness: root.widgetThickness + barThickness: root.barThickness + content: root.horizontalBarPill + onClicked: { + if (hasPopout) { + pluginPopout.toggle() + } + } + } + + BaseVerticalPill { + id: verticalPill + visible: isVertical && hasVerticalPill + axis: root.axis + section: root.section + popoutTarget: hasPopout ? pluginPopout : null + parentScreen: root.parentScreen + widgetThickness: root.widgetThickness + barThickness: root.barThickness + content: root.verticalBarPill + onClicked: { + if (hasPopout) { + pluginPopout.toggle() + } + } + } + + PluginPopout { + id: pluginPopout + contentWidth: root.popoutWidth + contentHeight: root.popoutHeight + pluginContent: root.popoutContent + } +} diff --git a/Modules/Plugins/PluginPopout.qml b/Modules/Plugins/PluginPopout.qml new file mode 100644 index 00000000..59267284 --- /dev/null +++ b/Modules/Plugins/PluginPopout.qml @@ -0,0 +1,116 @@ +import QtQuick +import qs.Common +import qs.Widgets + +DankPopout { + id: root + + property var triggerScreen: null + property Component pluginContent: null + property real contentWidth: 400 + property real contentHeight: 400 + + function setTriggerPosition(x, y, width, section, screen) { + triggerX = x + triggerY = y + triggerWidth = width + triggerSection = section + triggerScreen = screen + } + + popupWidth: contentWidth + popupHeight: popoutContent.item ? popoutContent.item.implicitHeight : contentHeight + screen: triggerScreen + shouldBeVisible: false + visible: shouldBeVisible + + content: Component { + Rectangle { + id: popoutContainer + + implicitHeight: popoutColumn.implicitHeight + Theme.spacingL * 2 + color: Theme.popupBackground() + radius: Theme.cornerRadius + border.width: 0 + antialiasing: true + smooth: true + focus: true + + Component.onCompleted: { + if (root.shouldBeVisible) { + forceActiveFocus() + } + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + root.close() + event.accepted = true + } + } + + Connections { + target: root + function onShouldBeVisibleChanged() { + if (root.shouldBeVisible) { + Qt.callLater(() => { + popoutContainer.forceActiveFocus() + }) + } + } + } + + Column { + id: popoutColumn + width: parent.width - Theme.spacingL * 2 + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + + Row { + width: parent.width + height: 32 + visible: closeButton.visible + + Item { + width: parent.width - 32 + height: 32 + } + + Rectangle { + id: closeButton + width: 32 + height: 32 + radius: 16 + color: closeArea.containsMouse ? Theme.errorHover : "transparent" + visible: true + + DankIcon { + anchors.centerIn: parent + name: "close" + size: Theme.iconSize - 4 + color: closeArea.containsMouse ? Theme.error : Theme.surfaceText + } + + MouseArea { + id: closeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + root.close() + } + } + } + } + + Loader { + id: popoutContent + width: parent.width + sourceComponent: root.pluginContent + } + } + } + } +} diff --git a/Modules/Plugins/PluginSettings.qml b/Modules/Plugins/PluginSettings.qml new file mode 100644 index 00000000..cc506370 --- /dev/null +++ b/Modules/Plugins/PluginSettings.qml @@ -0,0 +1,33 @@ +import QtQuick +import qs.Common +import qs.Services + +Item { + id: root + + required property string pluginId + property var pluginService: null + default property alias content: settingsColumn.children + + implicitHeight: settingsColumn.implicitHeight + height: implicitHeight + + function saveValue(key, value) { + if (pluginService && pluginService.savePluginData) { + pluginService.savePluginData(pluginId, key, value) + } + } + + function loadValue(key, defaultValue) { + if (pluginService && pluginService.loadPluginData) { + return pluginService.loadPluginData(pluginId, key, defaultValue) + } + return defaultValue + } + + Column { + id: settingsColumn + width: parent.width + spacing: Theme.spacingM + } +} diff --git a/Modules/Plugins/SelectionSetting.qml b/Modules/Plugins/SelectionSetting.qml new file mode 100644 index 00000000..981cd180 --- /dev/null +++ b/Modules/Plugins/SelectionSetting.qml @@ -0,0 +1,113 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Column { + id: root + + required property string settingKey + required property string label + property string description: "" + required property var options + property string defaultValue: "" + property string value: defaultValue + + width: parent.width + spacing: Theme.spacingS + + readonly property var optionLabels: { + const labels = [] + for (let i = 0; i < options.length; i++) { + labels.push(options[i].label || options[i]) + } + return labels + } + + readonly property var valueToLabel: { + const map = {} + for (let i = 0; i < options.length; i++) { + const opt = options[i] + if (typeof opt === 'object') { + map[opt.value] = opt.label + } else { + map[opt] = opt + } + } + return map + } + + readonly property var labelToValue: { + const map = {} + for (let i = 0; i < options.length; i++) { + const opt = options[i] + if (typeof opt === 'object') { + map[opt.label] = opt.value + } else { + map[opt] = opt + } + } + return map + } + + Component.onCompleted: { + const settings = findSettings() + if (settings) { + value = settings.loadValue(settingKey, defaultValue) + } + } + + onValueChanged: { + const settings = findSettings() + if (settings) { + settings.saveValue(settingKey, value) + } + } + + function findSettings() { + let item = parent + while (item) { + if (item.saveValue !== undefined && item.loadValue !== undefined) { + return item + } + item = item.parent + } + return null + } + + Row { + width: parent.width + spacing: Theme.spacingM + + Column { + width: parent.width * 0.4 + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: root.label + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: root.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + visible: root.description !== "" + } + } + + DankDropdown { + width: parent.width * 0.6 - Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + currentValue: root.valueToLabel[root.value] || root.value + options: root.optionLabels + onValueChanged: newValue => { + root.value = root.labelToValue[newValue] || newValue + } + } + } +} diff --git a/Modules/Plugins/StringSetting.qml b/Modules/Plugins/StringSetting.qml new file mode 100644 index 00000000..795a2270 --- /dev/null +++ b/Modules/Plugins/StringSetting.qml @@ -0,0 +1,65 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Column { + id: root + + required property string settingKey + required property string label + property string description: "" + property string placeholder: "" + property string defaultValue: "" + property string value: defaultValue + + width: parent.width + spacing: Theme.spacingS + + Component.onCompleted: { + const settings = findSettings() + if (settings) { + value = settings.loadValue(settingKey, defaultValue) + textField.text = value + } + } + + function findSettings() { + let item = parent + while (item) { + if (item.saveValue !== undefined && item.loadValue !== undefined) { + return item + } + item = item.parent + } + return null + } + + StyledText { + text: root.label + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: root.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + visible: root.description !== "" + } + + DankTextField { + id: textField + width: parent.width + placeholderText: root.placeholder + onEditingFinished: { + root.value = text + const settings = findSettings() + if (settings) { + settings.saveValue(settingKey, text) + } + } + } +} diff --git a/Modules/Plugins/ToggleSetting.qml b/Modules/Plugins/ToggleSetting.qml new file mode 100644 index 00000000..cfca5264 --- /dev/null +++ b/Modules/Plugins/ToggleSetting.qml @@ -0,0 +1,72 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Row { + id: root + + required property string settingKey + required property string label + property string description: "" + property bool defaultValue: false + property bool value: defaultValue + + width: parent.width + spacing: Theme.spacingM + + Component.onCompleted: { + const settings = findSettings() + if (settings) { + value = settings.loadValue(settingKey, defaultValue) + } + } + + onValueChanged: { + const settings = findSettings() + if (settings) { + settings.saveValue(settingKey, value) + } + } + + function findSettings() { + let item = parent + while (item) { + if (item.saveValue !== undefined && item.loadValue !== undefined) { + return item + } + item = item.parent + } + return null + } + + Column { + width: parent.width - toggle.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: root.label + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: root.description + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + visible: root.description !== "" + } + } + + DankToggle { + id: toggle + anchors.verticalCenter: parent.verticalCenter + checked: root.value + onToggled: isChecked => { + root.value = isChecked + } + } +} diff --git a/Modules/Settings/PluginsTab.qml b/Modules/Settings/PluginsTab.qml index a11901e7..4efabae9 100644 --- a/Modules/Settings/PluginsTab.qml +++ b/Modules/Settings/PluginsTab.qml @@ -160,21 +160,17 @@ Item { font.weight: Font.Medium } - Item { + Column { width: parent.width - height: Math.max(150, pluginListView.contentHeight) + spacing: Theme.spacingM - ListView { - id: pluginListView - - anchors.fill: parent + Repeater { + id: pluginRepeater model: PluginService.getAvailablePlugins() - spacing: Theme.spacingM - clip: true - delegate: StyledRect { + StyledRect { id: pluginDelegate - width: pluginListView.width + width: parent.width height: pluginItemColumn.implicitHeight + Theme.spacingM * 2 + settingsContainer.height radius: Theme.cornerRadius @@ -194,23 +190,9 @@ Item { console.log("Plugin", pluginId, "isExpanded changed to:", isExpanded) } - color: pluginMouseArea.containsMouse ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHighest : Theme.surfaceContainer) + color: pluginMouseArea.containsMouse ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh) border.width: 0 - Behavior on height { - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.standardEasing - } - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - MouseArea { id: pluginMouseArea anchors.fill: parent @@ -238,7 +220,7 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.margins: Theme.spacingM - spacing: Theme.spacingS + spacing: Theme.spacingM Row { width: parent.width @@ -252,9 +234,9 @@ Item { } Column { + width: parent.width - Theme.iconSize - Theme.spacingM - pluginToggle.width - Theme.spacingM + spacing: Theme.spacingXS anchors.verticalCenter: parent.verticalCenter - spacing: 2 - width: parent.width - 250 Row { spacing: Theme.spacingXS @@ -262,7 +244,7 @@ Item { StyledText { text: pluginDelegate.pluginName - font.pixelSize: Theme.fontSizeMedium + font.pixelSize: Theme.fontSizeLarge color: Theme.surfaceText font.weight: Font.Medium anchors.verticalCenter: parent.verticalCenter @@ -274,13 +256,6 @@ Item { color: pluginDelegate.hasSettings ? Theme.primary : "transparent" anchors.verticalCenter: parent.verticalCenter visible: pluginDelegate.hasSettings - - Behavior on rotation { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } } } @@ -289,16 +264,11 @@ Item { font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText width: parent.width - elide: Text.ElideRight } } - Item { - width: 10 - height: 1 - } - DankToggle { + id: pluginToggle anchors.verticalCenter: parent.verticalCenter checked: PluginService.isPluginLoaded(pluginDelegate.pluginId) onToggled: (isChecked) => { @@ -367,20 +337,9 @@ Item { anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - height: pluginDelegate.isExpanded && pluginDelegate.hasSettings ? Math.min(500, settingsLoader.item ? settingsLoader.item.implicitHeight + Theme.spacingL * 2 : 200) : 0 + height: pluginDelegate.isExpanded && pluginDelegate.hasSettings ? (settingsLoader.item ? settingsLoader.item.implicitHeight + Theme.spacingL * 2 : 0) : 0 clip: true - onHeightChanged: { - console.log("Settings container height changed:", height, "for plugin:", pluginDelegate.pluginId) - } - - Behavior on height { - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.standardEasing - } - } - Rectangle { anchors.fill: parent color: Theme.surfaceContainerHighest @@ -389,78 +348,61 @@ Item { border.width: 0 } - DankFlickable { + Loader { + id: settingsLoader anchors.fill: parent anchors.margins: Theme.spacingL - contentHeight: settingsLoader.height - contentWidth: width - clip: true + active: pluginDelegate.isExpanded && pluginDelegate.hasSettings && PluginService.isPluginLoaded(pluginDelegate.pluginId) + asynchronous: false - Loader { - id: settingsLoader - width: parent.width - active: pluginDelegate.isExpanded && pluginDelegate.hasSettings && PluginService.isPluginLoaded(pluginDelegate.pluginId) - asynchronous: false + onActiveChanged: { + console.log("Settings loader active changed to:", active, "for plugin:", pluginDelegate.pluginId, + "isExpanded:", pluginDelegate.isExpanded, "hasSettings:", pluginDelegate.hasSettings, + "isLoaded:", PluginService.isPluginLoaded(pluginDelegate.pluginId)) + } - onActiveChanged: { - console.log("Settings loader active changed to:", active, "for plugin:", pluginDelegate.pluginId, - "isExpanded:", pluginDelegate.isExpanded, "hasSettings:", pluginDelegate.hasSettings, - "isLoaded:", PluginService.isPluginLoaded(pluginDelegate.pluginId)) - } - - source: { - if (active && pluginDelegate.pluginSettingsPath) { - console.log("Loading plugin settings from:", pluginDelegate.pluginSettingsPath) - var path = pluginDelegate.pluginSettingsPath - if (!path.startsWith("file://")) { - path = "file://" + path - } - return path + source: { + if (active && pluginDelegate.pluginSettingsPath) { + console.log("Loading plugin settings from:", pluginDelegate.pluginSettingsPath) + var path = pluginDelegate.pluginSettingsPath + if (!path.startsWith("file://")) { + path = "file://" + path } - return "" + return path } + return "" + } - onStatusChanged: { - console.log("Settings loader status changed:", status, "for plugin:", pluginDelegate.pluginId) - if (status === Loader.Error) { - console.error("Failed to load plugin settings:", pluginDelegate.pluginSettingsPath) - } else if (status === Loader.Ready) { - console.log("Settings successfully loaded for plugin:", pluginDelegate.pluginId) + onStatusChanged: { + console.log("Settings loader status changed:", status, "for plugin:", pluginDelegate.pluginId) + if (status === Loader.Error) { + console.error("Failed to load plugin settings:", pluginDelegate.pluginSettingsPath) + } else if (status === Loader.Ready) { + console.log("Settings successfully loaded for plugin:", pluginDelegate.pluginId) + } + } + + onLoaded: { + if (item) { + console.log("Plugin settings loaded for:", pluginDelegate.pluginId) + + if (typeof PluginService !== "undefined") { + console.log("Making PluginService available to plugin settings") + console.log("PluginService functions available:", + "savePluginData" in PluginService, + "loadPluginData" in PluginService) + item.pluginService = PluginService + console.log("PluginService assignment completed, item.pluginService:", item.pluginService !== null) + } else { + console.error("PluginService not available in PluginsTab context") } - } - onLoaded: { - if (item) { - console.log("Plugin settings loaded for:", pluginDelegate.pluginId) - - // Make PluginService available to the loaded component - if (typeof PluginService !== "undefined") { - console.log("Making PluginService available to plugin settings") - console.log("PluginService functions available:", - "savePluginData" in PluginService, - "loadPluginData" in PluginService) - item.pluginService = PluginService - console.log("PluginService assignment completed, item.pluginService:", item.pluginService !== null) - } else { - console.error("PluginService not available in PluginsTab context") - } - - // Connect to height changes for dynamic resizing - if (item.implicitHeightChanged) { - item.implicitHeightChanged.connect(function() { - console.log("Plugin settings height changed:", item.implicitHeight) - }) - } - - // Force load timezones for WorldClock plugin - if (item.loadTimezones) { - console.log("Calling loadTimezones for WorldClock plugin") - item.loadTimezones() - } - // Generic initialization for any plugin - if (item.initializeSettings) { - item.initializeSettings() - } + if (item.loadTimezones) { + console.log("Calling loadTimezones for WorldClock plugin") + item.loadTimezones() + } + if (item.initializeSettings) { + item.initializeSettings() } } } @@ -482,12 +424,12 @@ Item { } StyledText { - anchors.centerIn: parent + width: parent.width text: "No plugins found.\nPlace plugins in " + PluginService.pluginDirectory font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceVariantText horizontalAlignment: Text.AlignHCenter - visible: pluginListView.model.length === 0 + visible: pluginRepeater.model.length === 0 } } } @@ -498,10 +440,10 @@ Item { Connections { target: PluginService function onPluginLoaded() { - pluginListView.model = PluginService.getAvailablePlugins() + pluginRepeater.model = PluginService.getAvailablePlugins() } function onPluginUnloaded() { - pluginListView.model = PluginService.getAvailablePlugins() + pluginRepeater.model = PluginService.getAvailablePlugins() if (pluginsTab.expandedPluginId !== "" && !PluginService.isPluginLoaded(pluginsTab.expandedPluginId)) { pluginsTab.expandedPluginId = "" } diff --git a/Services/PluginService.qml b/Services/PluginService.qml index e51436df..39e7a613 100644 --- a/Services/PluginService.qml +++ b/Services/PluginService.qml @@ -22,6 +22,8 @@ Singleton { } return configDirStr + "/DankMaterialShell/plugins" } + property string systemPluginDirectory: "/etc/xdg/quickshell/dms-plugins" + property var pluginDirectories: [pluginDirectory, systemPluginDirectory] signal pluginLoaded(string pluginId) signal pluginUnloaded(string pluginId) @@ -35,37 +37,54 @@ Singleton { scanPlugins() } + property int currentScanIndex: 0 + property var scanResults: [] + property var lsProcess: Process { id: dirScanner stdout: StdioCollector { onStreamFinished: { var output = text.trim() + var currentDir = pluginDirectories[currentScanIndex] if (output) { var directories = output.split('\n') for (var i = 0; i < directories.length; i++) { var dir = directories[i].trim() if (dir) { - var manifestPath = pluginDirectory + "/" + dir + "/plugin.json" + var manifestPath = currentDir + "/" + dir + "/plugin.json" console.log("PluginService: Found plugin directory:", dir, "checking manifest at:", manifestPath) loadPluginManifest(manifestPath) } } } else { - console.log("PluginService: No directories found in plugin directory") + console.log("PluginService: No directories found in:", currentDir) } } } onExited: function(exitCode) { if (exitCode !== 0) { - console.error("PluginService: Failed to scan plugin directory, exit code:", exitCode) + console.log("PluginService: Directory scan failed for:", pluginDirectories[currentScanIndex], "exit code:", exitCode) + } + currentScanIndex++ + if (currentScanIndex < pluginDirectories.length) { + scanNextDirectory() + } else { + currentScanIndex = 0 } } } function scanPlugins() { - lsProcess.command = ["find", pluginDirectory, "-maxdepth", "1", "-type", "d", "-not", "-path", pluginDirectory, "-exec", "basename", "{}", ";"] + currentScanIndex = 0 + scanNextDirectory() + } + + function scanNextDirectory() { + var dir = pluginDirectories[currentScanIndex] + console.log("PluginService: Scanning directory:", dir) + lsProcess.command = ["find", "-L", dir, "-maxdepth", "1", "-type", "d", "-not", "-path", dir, "-exec", "basename", "{}", ";"] lsProcess.running = true } diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md index 5c33c032..b88a8f7f 100644 --- a/docs/PLUGINS.md +++ b/docs/PLUGINS.md @@ -93,94 +93,301 @@ The manifest file defines plugin metadata and configuration: ### Widget Component -The main widget component is displayed in the DankBar. It receives several properties from the shell: +The main widget component uses the **PluginComponent** wrapper which provides automatic property injection and bar integration: ```qml import QtQuick +import qs.Common +import qs.Widgets +import qs.Modules.Plugins -Rectangle { - id: root +PluginComponent { + // Define horizontal bar pill (optional) + horizontalBarPill: Component { + StyledRect { + width: content.implicitWidth + Theme.spacingM * 2 + height: parent.widgetThickness + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh - // Standard properties provided by DankBar - property bool compactMode: false - property string section: "center" // "left", "center", or "right" - property var popupTarget: null - property var parentScreen: null - property real barHeight: 48 - property real widgetHeight: 30 - - // Widget dimensions - width: content.implicitWidth + horizontalPadding * 2 - height: widgetHeight - - // PluginService is injected by PluginsTab when loading settings - property var pluginService - - // Access plugin data - Component.onCompleted: { - if (pluginService) { - var savedData = pluginService.loadPluginData("yourPlugin", "dataKey", defaultValue) + StyledText { + id: content + anchors.centerIn: parent + text: "Hello World" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + } } } - // Save plugin data - function saveData(key, value) { - if (pluginService) { - pluginService.savePluginData("yourPlugin", key, value) + // Define vertical bar pill (optional) + verticalBarPill: Component { + // Same as horizontal but optimized for vertical layout + } + + // Define popout content (optional) + popoutContent: Component { + Column { + width: parent.width + spacing: Theme.spacingM + padding: Theme.spacingM + + StyledText { + text: "Popout Content" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + } } } + + // Popout dimensions (required if popoutContent is set) + popoutWidth: 400 + popoutHeight: 300 } ``` -**Important Properties:** -- `compactMode`: Whether the bar is in compact display mode -- `section`: Which bar section the widget is in -- `barHeight`: Height of the entire bar -- `widgetHeight`: Recommended widget height -- `parentScreen`: Reference to the screen object +**PluginComponent Properties (automatically injected):** +- `axis`: Bar axis information (horizontal/vertical) +- `section`: Bar section ("left", "center", "right") +- `parentScreen`: Screen reference for multi-monitor support +- `widgetThickness`: Recommended widget size perpendicular to bar +- `barThickness`: Bar thickness parallel to edge + +**Component Options:** +- `horizontalBarPill`: Component shown in horizontal bars +- `verticalBarPill`: Component shown in vertical bars +- `popoutContent`: Optional popout window content +- `popoutWidth`: Popout window width +- `popoutHeight`: Popout window height + +The PluginComponent automatically handles: +- Bar orientation detection +- Click handlers for popouts +- Proper positioning and anchoring +- Theme integration ### Settings Component -Optional settings UI loaded inline in the PluginsTab accordion interface: +Optional settings UI loaded inline in the PluginsTab accordion interface. Use the simplified settings API with auto-storage components: ```qml import QtQuick -import QtQuick.Controls import qs.Common -import qs.Services import qs.Widgets +import qs.Modules.Plugins -Column { +PluginSettings { id: root + pluginId: "yourPlugin" - // PluginService is injected by PluginsTab - property var pluginService - - spacing: Theme.spacingM - - DankTextField { - id: settingInput - width: parent.width - label: "Setting Label" - text: pluginService ? pluginService.loadPluginData("yourPlugin", "settingKey", "default") : "" - onTextChanged: { - if (pluginService) { - pluginService.savePluginData("yourPlugin", "settingKey", text) - } - } + StringSetting { + settingKey: "apiKey" + label: "API Key" + description: "Your API key for accessing the service" + placeholder: "Enter API key..." } - DankToggle { - checked: pluginService ? pluginService.loadPluginData("yourPlugin", "enabled", true) : false - onToggled: { - if (pluginService) { - pluginService.savePluginData("yourPlugin", "enabled", checked) + ToggleSetting { + settingKey: "notifications" + label: "Enable Notifications" + description: "Show desktop notifications for updates" + defaultValue: true + } + + SelectionSetting { + settingKey: "updateInterval" + label: "Update Interval" + description: "How often to refresh data" + options: [ + {label: "1 minute", value: "60"}, + {label: "5 minutes", value: "300"}, + {label: "15 minutes", value: "900"} + ] + defaultValue: "300" + } + + ListSetting { + id: itemList + settingKey: "items" + label: "Saved Items" + description: "List of configured items" + delegate: Component { + StyledRect { + width: parent.width + height: 40 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + text: modelData.name + color: Theme.surfaceText + } + + Rectangle { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + width: 60 + height: 28 + color: removeArea.containsMouse ? Theme.errorHover : Theme.error + radius: Theme.cornerRadius + + StyledText { + anchors.centerIn: parent + text: "Remove" + color: Theme.errorText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + } + + MouseArea { + id: removeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: itemList.removeItem(index) + } + } } } } } ``` +**Available Setting Components:** + +All settings automatically save on change and load on component creation. No manual `pluginService.savePluginData()` calls needed! + +1. **PluginSettings** - Root wrapper for all plugin settings + - `pluginId`: Your plugin ID (required) + - Auto-handles storage and provides saveValue/loadValue to children + - Place all other setting components inside this wrapper + +2. **StringSetting** - Text input field + - `settingKey`: Storage key (required) + - `label`: Display label (required) + - `description`: Help text (optional) + - `placeholder`: Input placeholder (optional) + - `defaultValue`: Default value (optional) + - Layout: Vertical stack (label, description, input field) + +3. **ToggleSetting** - Boolean toggle switch + - `settingKey`: Storage key (required) + - `label`: Display label (required) + - `description`: Help text (optional) + - `defaultValue`: Default boolean (optional) + - Layout: Horizontal (label/description left, toggle right) + +4. **SelectionSetting** - Dropdown menu + - `settingKey`: Storage key (required) + - `label`: Display label (required) + - `description`: Help text (optional) + - `options`: Array of `{label, value}` objects or simple strings (required) + - `defaultValue`: Default value (optional) + - Layout: Horizontal (label/description left, dropdown right) + - Stores the `value` field, displays the `label` field + +5. **ListSetting** - Manage list of items (manual add/remove) + - `settingKey`: Storage key (required) + - `label`: Display label (required) + - `description`: Help text (optional) + - `delegate`: Custom item delegate Component (optional) + - `addItem(item)`: Add item to list + - `removeItem(index)`: Remove item from list + - Use when you need custom UI for adding items + +6. **ListSettingWithInput** - Complete list management with built-in form + - `settingKey`: Storage key (required) + - `label`: Display label (required) + - `description`: Help text (optional) + - `fields`: Array of field definitions (required) + - `id`: Field ID in saved object (required) + - `label`: Column header text (required) + - `placeholder`: Input placeholder (optional) + - `width`: Column width in pixels (optional, default 200) + - `required`: Must have value to add (optional, default false) + - `default`: Default value if empty (optional) + - Automatically generates: + - Column headers from field labels + - Input fields with placeholders + - Add button with validation + - List display showing all field values + - Remove buttons for each item + - Best for collecting structured data (servers, locations, etc.) + +**Complete Settings Example:** + +```qml +import QtQuick +import qs.Common +import qs.Widgets +import qs.Modules.Plugins + +PluginSettings { + pluginId: "myPlugin" + + // Section header (optional) + StyledText { + width: parent.width + text: "General Settings" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + color: Theme.surfaceText + } + + // Text input + StringSetting { + settingKey: "apiKey" + label: "API Key" + description: "Your service API key" + placeholder: "sk-..." + defaultValue: "" + } + + // Toggle switches + ToggleSetting { + settingKey: "enabled" + label: "Enable Feature" + description: "Turn this feature on or off" + defaultValue: true + } + + // Dropdown selection + SelectionSetting { + settingKey: "theme" + label: "Theme" + description: "Choose your preferred theme" + options: [ + {label: "Dark", value: "dark"}, + {label: "Light", value: "light"}, + {label: "Auto", value: "auto"} + ] + defaultValue: "dark" + } + + // Structured list with multi-field input + ListSettingWithInput { + settingKey: "locations" + label: "Locations" + description: "Track multiple locations" + fields: [ + {id: "name", label: "Name", placeholder: "Home", width: 150, required: true}, + {id: "timezone", label: "Timezone", placeholder: "America/New_York", width: 200, required: true} + ] + } +} +``` + +**Key Benefits:** +- Zero boilerplate - just define your settings +- Automatic persistence to `settings.json` +- Clean, consistent UI across all plugins +- No manual `pluginService` calls needed +- Proper layout and spacing handled automatically + ## PluginService API ### Properties @@ -262,72 +469,84 @@ Create `MyWidget.qml`: ```qml import QtQuick -import qs.Services +import qs.Common +import qs.Widgets +import qs.Modules.Plugins -Rectangle { - id: root +PluginComponent { + horizontalBarPill: Component { + StyledRect { + width: textItem.implicitWidth + Theme.spacingM * 2 + height: parent.widgetThickness + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh - property bool compactMode: false - property string section: "center" - property real widgetHeight: 30 - property string displayText: "Hello World" - - width: textItem.implicitWidth + 16 - height: widgetHeight - radius: 8 - color: "#20FFFFFF" - - Component.onCompleted: { - displayText = PluginService.loadPluginData("myPlugin", "text", "Hello World") + StyledText { + id: textItem + anchors.centerIn: parent + text: "Hello World" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + } + } } - Text { - id: textItem - anchors.centerIn: parent - text: root.displayText - color: "#FFFFFF" - font.pixelSize: 13 - } + verticalBarPill: Component { + StyledRect { + width: parent.widgetThickness + height: textItem.implicitWidth + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh - MouseArea { - anchors.fill: parent - onClicked: console.log("Plugin clicked!") + StyledText { + id: textItem + anchors.centerIn: parent + text: "Hello" + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeSmall + rotation: 90 + } + } } } ``` +**Note:** Use `PluginComponent` wrapper for automatic property injection and bar integration. Define separate components for horizontal and vertical orientations. + ### Step 4: Create Settings Component (Optional) Create `MySettings.qml`: ```qml import QtQuick -import QtQuick.Controls import qs.Common -import qs.Services import qs.Widgets +import qs.Modules.Plugins -Column { - // PluginService is injected by PluginsTab - property var pluginService - - spacing: Theme.spacingM +PluginSettings { + pluginId: "myPlugin" StyledText { - text: "Plugin Settings" - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Medium + width: parent.width + text: "Configure your plugin settings" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap } - DankTextField { - width: parent.width + StringSetting { + settingKey: "text" label: "Display Text" - text: pluginService ? pluginService.loadPluginData("myPlugin", "text", "Hello World") : "" - onTextChanged: { - if (pluginService) { - pluginService.savePluginData("myPlugin", "text", text) - } - } + description: "Text shown in the bar widget" + placeholder: "Hello World" + defaultValue: "Hello World" + } + + ToggleSetting { + settingKey: "showIcon" + label: "Show Icon" + description: "Display an icon next to the text" + defaultValue: true } } ```