import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell import qs.Commons import qs.Services.Compositor import qs.Services.Noctalia import qs.Services.UI import qs.Widgets ColumnLayout { id: root spacing: Style.marginL // Available widgets model - declared early so Repeater delegates can access it property alias availableWidgetsModel: availableWidgets ListModel { id: availableWidgets } // Listen for plugin widget registration/unregistration Connections { target: DesktopWidgetRegistry function onPluginWidgetRegistryUpdated() { updateAvailableWidgetsModel(); } } NToggle { Layout.fillWidth: true label: I18n.tr("panels.desktop-widgets.enabled-label") description: I18n.tr("panels.desktop-widgets.enabled-description") checked: Settings.data.desktopWidgets.enabled defaultValue: Settings.getDefaultValue("desktopWidgets.enabled") onToggled: checked => Settings.data.desktopWidgets.enabled = checked } NToggle { Layout.fillWidth: true enabled: Settings.data.desktopWidgets.enabled label: I18n.tr("panels.desktop-widgets.overview-enabled-label") description: I18n.tr("panels.desktop-widgets.overview-enabled-description") checked: Settings.data.desktopWidgets.overviewEnabled defaultValue: Settings.getDefaultValue("desktopWidgets.overviewEnabled") onToggled: checked => Settings.data.desktopWidgets.overviewEnabled = checked } ColumnLayout { enabled: Settings.data.desktopWidgets.enabled NLabel { description: I18n.tr("panels.desktop-widgets.cpu-intensive-note") } NButton { Layout.fillWidth: true Layout.topMargin: Style.marginM Layout.bottomMargin: Style.marginM text: DesktopWidgetRegistry.editMode ? I18n.tr("panels.desktop-widgets.edit-mode-exit-button") : I18n.tr("panels.desktop-widgets.edit-mode-button-label") icon: "edit" onClicked: { DesktopWidgetRegistry.editMode = !DesktopWidgetRegistry.editMode; if (DesktopWidgetRegistry.editMode && Settings.data.ui.settingsPanelMode !== "window") { var item = root.parent; while (item) { if (item.closeRequested !== undefined) { item.closeRequested(); break; } item = item.parent; } } } } // One NSectionEditor per monitor Repeater { model: Quickshell.screens NSectionEditor { required property var modelData readonly property real compositorScale: { const info = CompositorService.displayScales[modelData.name]; return (info && info.scale) ? info.scale : 1.0; } Layout.fillWidth: true sectionName: modelData.name sectionSubtitle: { // Format scale to 2 decimal places to prevent overly long text var formattedScale = compositorScale.toFixed(2); return "(" + modelData.width + "x" + modelData.height + " @ " + formattedScale + "x)"; } sectionId: modelData.name screen: modelData settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml") widgetRegistry: DesktopWidgetRegistry widgetModel: getWidgetsForMonitor(modelData.name) availableWidgets: root.availableWidgetsModel availableSections: root.getScreenNames() sectionLabels: root.getScreenLabels() sectionIcons: root.getScreenIcons() draggable: false // Desktop widgets are positioned by X,Y, not list order crossSectionDraggable: true maxWidgets: -1 onAddWidget: (widgetId, section) => _addWidgetToMonitor(modelData.name, widgetId) onRemoveWidget: (section, index) => _removeWidgetFromMonitor(modelData.name, index) onMoveWidget: (fromSection, index, toSection) => _moveWidgetToMonitor(fromSection, index, toSection) onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsForMonitor(modelData.name, index, settings) pluginSettingsEntryPoints: ["desktopWidgetSettings", "settings"] onOpenPluginSettingsRequested: (manifest, entryPoint) => pluginSettingsDialog.openPluginSettings(manifest, entryPoint) } } } // Shared Plugin Settings Popup NPluginSettingsPopup { id: pluginSettingsDialog parent: Overlay.overlay showToastOnSave: false } Component.onCompleted: { // Use Qt.callLater to ensure DesktopWidgetRegistry is ready Qt.callLater(updateAvailableWidgetsModel); } // Helper to get screen names array function getScreenNames() { var names = []; for (var i = 0; i < Quickshell.screens.length; i++) { names.push(Quickshell.screens[i].name); } return names; } // Helper to get screen labels map (just screen names, NSectionEditor adds "Move to" prefix) function getScreenLabels() { var labels = {}; for (var i = 0; i < Quickshell.screens.length; i++) { var screen = Quickshell.screens[i]; labels[screen.name] = screen.name; } return labels; } // Helper to get screen icons map function getScreenIcons() { var icons = {}; for (var i = 0; i < Quickshell.screens.length; i++) { icons[Quickshell.screens[i].name] = "device-desktop"; } return icons; } function updateAvailableWidgetsModel() { availableWidgets.clear(); try { if (typeof DesktopWidgetRegistry === "undefined" || !DesktopWidgetRegistry) { Logger.e("DesktopWidgetsTab", "DesktopWidgetRegistry is not available"); // Retry after a short delay Qt.callLater(function () { if (typeof DesktopWidgetRegistry !== "undefined" && DesktopWidgetRegistry) { updateAvailableWidgetsModel(); } }); return; } var widgetIds = DesktopWidgetRegistry.getAvailableWidgets(); Logger.d("DesktopWidgetsTab", "Found widgets:", widgetIds, "count:", widgetIds ? widgetIds.length : 0); if (!widgetIds || widgetIds.length === 0) { Logger.w("DesktopWidgetsTab", "No widgets found in registry"); return; } for (var i = 0; i < widgetIds.length; i++) { var widgetId = widgetIds[i]; var displayName = widgetId; // Get plugin name for plugin widgets var isPlugin = false; if (DesktopWidgetRegistry.isPluginWidget(widgetId)) { isPlugin = true; var pluginId = widgetId.replace("plugin:", ""); var manifest = PluginRegistry.getPluginManifest(pluginId); if (manifest && manifest.name) { displayName = manifest.name; } } // Add plugin badge first (with custom color) const badges = []; if (isPlugin) { badges.push({ "icon": "plugin", "color": Color.mSecondary }); } if (DesktopWidgetRegistry.isCpuIntensive(widgetId)) { badges.push({ "icon": "cpu-intensive", "color": Color.mSecondary }); } availableWidgets.append({ "key": widgetId, "name": displayName, "badges": badges }); } Logger.d("DesktopWidgetsTab", "Available widgets model count:", availableWidgets.count); } catch (e) { Logger.e("DesktopWidgetsTab", "Error updating available widgets:", e, e.stack); } } // Get widgets for a specific monitor function getWidgetsForMonitor(monitorName) { var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; for (var i = 0; i < monitorWidgets.length; i++) { if (monitorWidgets[i].name === monitorName) { return monitorWidgets[i].widgets || []; } } return []; } // Set widgets for a specific monitor function setWidgetsForMonitor(monitorName, widgets) { var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; var newMonitorWidgets = monitorWidgets.slice(); var found = false; for (var i = 0; i < newMonitorWidgets.length; i++) { if (newMonitorWidgets[i].name === monitorName) { newMonitorWidgets[i] = { "name": monitorName, "widgets": widgets }; found = true; break; } } if (!found) { newMonitorWidgets.push({ "name": monitorName, "widgets": widgets }); } Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets; } function _addWidgetToMonitor(monitorName, widgetId) { var newWidget = { "id": widgetId }; if (DesktopWidgetRegistry.widgetHasUserSettings(widgetId)) { var metadata = DesktopWidgetRegistry.widgetMetadata[widgetId]; if (metadata) { Object.keys(metadata).forEach(function (key) { newWidget[key] = metadata[key]; }); } } // Set default positions if (widgetId === "Clock") { newWidget.x = 50; newWidget.y = 50; } else if (widgetId === "MediaPlayer") { newWidget.x = 100; newWidget.y = 200; } else if (widgetId === "AudioVisualizer") { newWidget.x = 120; newWidget.y = 280; } else if (widgetId === "Weather") { newWidget.x = 100; newWidget.y = 300; } else { // Default position for plugin widgets newWidget.x = 150; newWidget.y = 150; } var widgets = getWidgetsForMonitor(monitorName).slice(); widgets.push(newWidget); setWidgetsForMonitor(monitorName, widgets); } function _removeWidgetFromMonitor(monitorName, index) { var widgets = getWidgetsForMonitor(monitorName); if (index >= 0 && index < widgets.length) { var newArray = widgets.slice(); newArray.splice(index, 1); setWidgetsForMonitor(monitorName, newArray); } } function _updateWidgetSettingsForMonitor(monitorName, index, settings) { var widgets = getWidgetsForMonitor(monitorName); if (index >= 0 && index < widgets.length) { var newArray = widgets.slice(); newArray[index] = Object.assign({}, newArray[index], settings); setWidgetsForMonitor(monitorName, newArray); } } function _moveWidgetToMonitor(fromMonitor, index, toMonitor) { Logger.i("DesktopWidgetsTab", "Moving widget from", fromMonitor, "index", index, "to", toMonitor); var sourceWidgets = getWidgetsForMonitor(fromMonitor); if (index < 0 || index >= sourceWidgets.length) { Logger.e("DesktopWidgetsTab", "Invalid index", index, "for monitor", fromMonitor); return; } // Get the widget to move var newSourceWidgets = sourceWidgets.slice(); var widget = Object.assign({}, newSourceWidgets.splice(index, 1)[0]); // Reset position and scale to ensure widget is accessible on new screen widget.x = 100; widget.y = 100; widget.scale = 1.0; // Add to destination monitor var destWidgets = getWidgetsForMonitor(toMonitor).slice(); destWidgets.push(widget); // Update both monitors setWidgetsForMonitor(fromMonitor, newSourceWidgets); setWidgetsForMonitor(toMonitor, destWidgets); } }