import QtQuick import QtQuick.Controls import QtQuick.Effects import QtQuick.Layouts import Quickshell import qs.Commons import qs.Services.Noctalia import qs.Services.UI import qs.Widgets Popup { id: root property int widgetIndex: -1 property var widgetData: null property string widgetId: "" property string sectionId: "" // Not used for desktop widgets, but required by NSectionEditor property var screen: null property var settingsCache: ({}) readonly property real maxHeight: (screen ? screen.height : (parent ? parent.height : 800)) * 0.8 signal updateWidgetSettings(string section, int index, var settings) width: Math.max(content.implicitWidth + padding * 2, 500) height: Math.min(content.implicitHeight + padding * 2, maxHeight) padding: Style.marginXL modal: true closePolicy: Popup.NoAutoClose dim: false // Center in parent x: Math.round((parent.width - width) / 2) y: Math.round((parent.height - height) / 2) onOpened: { if (widgetData && widgetId) { loadWidgetSettings(); } forceActiveFocus(); } background: Rectangle { id: bgRect color: Color.mSurface radius: Style.radiusL border.color: Color.mPrimary border.width: Style.borderM } contentItem: FocusScope { id: focusScope focus: true ColumnLayout { id: content anchors.fill: parent spacing: Style.marginM RowLayout { id: titleRow Layout.fillWidth: true Layout.preferredHeight: implicitHeight NText { text: I18n.tr("system.widget-settings-title", { "widget": DesktopWidgetRegistry.getWidgetDisplayName(root.widgetId) }) pointSize: Style.fontSizeL font.weight: Style.fontWeightBold color: Color.mPrimary Layout.fillWidth: true } NIconButton { icon: "close" tooltipText: I18n.tr("common.close") onClicked: saveAndClose() } } Rectangle { id: separator Layout.fillWidth: true Layout.preferredHeight: 1 color: Color.mOutline } // Scrollable settings area NScrollView { id: scrollView Layout.fillWidth: true Layout.fillHeight: true Layout.minimumHeight: 100 gradientColor: Color.mSurface ColumnLayout { width: scrollView.availableWidth spacing: Style.marginM Loader { id: settingsLoader Layout.fillWidth: true onLoaded: { if (item) { Qt.callLater(() => { var firstInput = findFirstFocusable(item); if (firstInput) { firstInput.forceActiveFocus(); } else { focusScope.forceActiveFocus(); } }); } } function findFirstFocusable(item) { if (!item) return null; if (item.focus !== undefined && item.focus === true) return item; if (item.children) { for (var i = 0; i < item.children.length; i++) { var child = item.children[i]; if (child && child.focus !== undefined && child.focus === true) return child; var found = findFirstFocusable(child); if (found) return found; } } return null; } } } } } } // Mouse area for a draggable popup MouseArea { x: titleRow.x y: titleRow.y width: titleRow.width height: titleRow.height z: -1 cursorShape: Qt.OpenHandCursor property real pressX: 0 property real pressY: 0 onPressed: mouse => { pressX = mouse.x; pressY = mouse.y; cursorShape = Qt.ClosedHandCursor; } onReleased: { cursorShape = Qt.OpenHandCursor; } onPositionChanged: mouse => { if (pressed) { var deltaX = mouse.x - pressX; var deltaY = mouse.y - pressY; root.x += deltaX; root.y += deltaY; } } } Timer { id: saveTimer running: false interval: 150 onTriggered: { root.updateWidgetSettings(root.sectionId, root.widgetIndex, root.settingsCache); } } Connections { target: settingsLoader.item ignoreUnknownSignals: true function onSettingsChanged(newSettings) { if (newSettings) { root.settingsCache = newSettings; saveTimer.start(); } } function onSettingsSaved(newSettings) { if (newSettings) { root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings); } } } function saveAndClose() { if (settingsLoader.item && typeof settingsLoader.item.saveSettings === 'function') { var newSettings = settingsLoader.item.saveSettings(); if (newSettings) { root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings); } } root.close(); } function loadWidgetSettings() { // Handle plugin widgets if (DesktopWidgetRegistry.isPluginWidget(widgetId)) { var pluginId = widgetId.replace("plugin:", ""); var manifest = PluginRegistry.getPluginManifest(pluginId); var pluginDir = PluginRegistry.getPluginDir(pluginId); var loadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0; var api = PluginService.getPluginAPI(pluginId); var settingsPath; if (manifest && manifest.entryPoints && manifest.entryPoints.desktopWidgetSettings) { settingsPath = "file://" + pluginDir + "/" + manifest.entryPoints.desktopWidgetSettings; var widgetSettings = {}; widgetSettings.data = widgetData || {}; widgetSettings.metadata = DesktopWidgetRegistry.widgetMetadata[widgetId] || {}; widgetSettings.save = function () { var newSettings = Object.assign({}, widgetSettings.data); root.settingsCache = newSettings; saveTimer.start(); }; settingsLoader.setSource(settingsPath + "?v=" + loadVersion, { "pluginApi": api, "widgetSettings": widgetSettings }); } else { Logger.w("DesktopWidgetSettingsDialog", "Plugin does not have desktop widget settings:", pluginId); // Fallback to the plugin settings if (manifest && manifest.entryPoints && manifest.entryPoints.settings) { settingsPath = "file://" + pluginDir + "/" + manifest.entryPoints.settings; settingsLoader.setSource(settingsPath + "?v=" + loadVersion, { "pluginApi": api }); } else { Logger.w("DesktopWidgetSettingsDialog", "Plugin does not have settings:", pluginId); } } return; } // Handle core widgets const source = DesktopWidgetRegistry.widgetSettingsMap[widgetId]; if (source) { var currentWidgetData = widgetData; var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; for (var i = 0; i < monitorWidgets.length; i++) { if (monitorWidgets[i].name === sectionId) { var widgets = monitorWidgets[i].widgets || []; if (widgetIndex >= 0 && widgetIndex < widgets.length) { currentWidgetData = widgets[widgetIndex]; } break; } } var fullPath = Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/DesktopWidgets/" + source); settingsLoader.setSource(fullPath, { "widgetData": currentWidgetData, "widgetMetadata": DesktopWidgetRegistry.widgetMetadata[widgetId] }); } } }