pragma Singleton import QtQuick import Quickshell import qs.Commons import qs.Modules.DesktopWidgets.Widgets import qs.Services.Noctalia Singleton { id: root // Transient state - not persisted, resets on shell restart property bool editMode: false // Signal emitted when plugin widgets are registered/unregistered signal pluginWidgetRegistryUpdated // Component definitions property Component clockComponent: Component { DesktopClock {} } property Component mediaPlayerComponent: Component { DesktopMediaPlayer {} } property Component weatherComponent: Component { DesktopWeather {} } property Component systemStatComponent: Component { DesktopSystemStat {} } property Component audioVisualizerComponent: Component { DesktopAudioVisualizer {} } // Widget registry object mapping widget names to components // Created in Component.onCompleted to ensure Components are ready property var widgets: ({}) // Explicit URLs for core widgets. Inline Component.url returns the parent // file's URL, so we need these for Loader.setSource() with initial properties. // Plugin widgets use their Component.url directly (loaded from actual files). readonly property string _widgetsDir: Quickshell.shellDir + "/Modules/DesktopWidgets/Widgets/" property var widgetUrls: ({}) Component.onCompleted: { // Initialize widgets object after Components are ready var widgetsObj = {}; widgetsObj["Clock"] = clockComponent; widgetsObj["MediaPlayer"] = mediaPlayerComponent; widgetsObj["Weather"] = weatherComponent; widgetsObj["SystemStat"] = systemStatComponent; widgetsObj["AudioVisualizer"] = audioVisualizerComponent; widgets = widgetsObj; var urlsObj = {}; urlsObj["Clock"] = _widgetsDir + "DesktopClock.qml"; urlsObj["MediaPlayer"] = _widgetsDir + "DesktopMediaPlayer.qml"; urlsObj["Weather"] = _widgetsDir + "DesktopWeather.qml"; urlsObj["SystemStat"] = _widgetsDir + "DesktopSystemStat.qml"; urlsObj["AudioVisualizer"] = _widgetsDir + "DesktopAudioVisualizer.qml"; widgetUrls = urlsObj; Logger.i("DesktopWidgetRegistry", "Service started"); } property var widgetSettingsMap: ({ "Clock": "WidgetSettings/ClockSettings.qml", "MediaPlayer": "WidgetSettings/MediaPlayerSettings.qml", "Weather": "WidgetSettings/WeatherSettings.qml", "SystemStat": "WidgetSettings/SystemStatSettings.qml", "AudioVisualizer": "WidgetSettings/AudioVisualizerSettings.qml" }) property var widgetMetadata: ({ "Clock": { "showBackground": true, "roundedCorners": true, "clockStyle": "digital", "clockColor": "none", "useCustomFont": false, "customFont": "", "format": "HH:mm\\nd MMMM yyyy" }, "MediaPlayer": { "showBackground": true, "roundedCorners": true, "visualizerType": "linear", "hideMode": "visible", "showButtons": true, "showAlbumArt": true, "showVisualizer": true }, "Weather": { "showBackground": true, "roundedCorners": true }, "SystemStat": { "showBackground": true, "roundedCorners": true, "statType": "CPU", "diskPath": "/", "layout": "bottom" }, "AudioVisualizer": { "showBackground": true, "roundedCorners": true, "width": 320, "height": 72, "visualizerType": "linear", "hideWhenIdle": false, "colorName": "primary" } }) property var cpuIntensiveWidgets: ["SystemStat", "AudioVisualizer"] // Plugin widget storage (mirroring BarWidgetRegistry pattern) property var pluginWidgets: ({}) property var pluginWidgetMetadata: ({}) function init() { Logger.i("DesktopWidgetRegistry", "Service started"); } // Helper function to get widget component by name function getWidget(id) { return widgets[id] || null; } // Helper function to check if widget exists function hasWidget(id) { return id in widgets; } // Get list of available widget ids function getAvailableWidgets() { var keys = Object.keys(widgets); Logger.d("DesktopWidgetRegistry", "getAvailableWidgets() called, returning:", keys); return keys; } // Helper function to check if widget has user settings function widgetHasUserSettings(id) { return widgetMetadata[id] !== undefined; } function isCpuIntensive(id) { if (pluginWidgetMetadata[id]?.cpuIntensive) return true; return cpuIntensiveWidgets.indexOf(id) >= 0; } // Check if a widget is a plugin widget function isPluginWidget(id) { return id.startsWith("plugin:"); } // Get list of plugin widget IDs function getPluginWidgets() { return Object.keys(pluginWidgets); } // Get display name for a widget ID function getWidgetDisplayName(widgetId) { if (widgetId.startsWith("plugin:")) { var pluginId = widgetId.replace("plugin:", ""); var manifest = PluginRegistry.getPluginManifest(pluginId); return manifest ? manifest.name : pluginId; } // Core widgets - return as-is (Clock, MediaPlayer, Weather, SystemStat) return widgetId; } // Register a plugin desktop widget function registerPluginWidget(pluginId, component, metadata) { if (!pluginId || !component) { Logger.e("DesktopWidgetRegistry", "Cannot register plugin widget: invalid parameters"); return false; } var widgetId = "plugin:" + pluginId; // Create new objects to trigger QML property change detection var newPluginWidgets = Object.assign({}, pluginWidgets); newPluginWidgets[widgetId] = component; pluginWidgets = newPluginWidgets; var newPluginMetadata = Object.assign({}, pluginWidgetMetadata); newPluginMetadata[widgetId] = metadata || {}; pluginWidgetMetadata = newPluginMetadata; // Also add to main widgets object for unified access - reassign to trigger change var newWidgets = Object.assign({}, widgets); newWidgets[widgetId] = component; widgets = newWidgets; var newMetadata = Object.assign({}, widgetMetadata); newMetadata[widgetId] = Object.assign({}, { "showBackground": true }, metadata || {}); widgetMetadata = newMetadata; Logger.i("DesktopWidgetRegistry", "Registered plugin widget:", widgetId); root.pluginWidgetRegistryUpdated(); return true; } // Unregister a plugin desktop widget function unregisterPluginWidget(pluginId) { var widgetId = "plugin:" + pluginId; if (!pluginWidgets[widgetId]) { Logger.w("DesktopWidgetRegistry", "Plugin widget not registered:", widgetId); return false; } // Create new objects without the widget to trigger QML property change detection var newPluginWidgets = Object.assign({}, pluginWidgets); delete newPluginWidgets[widgetId]; pluginWidgets = newPluginWidgets; var newPluginMetadata = Object.assign({}, pluginWidgetMetadata); delete newPluginMetadata[widgetId]; pluginWidgetMetadata = newPluginMetadata; var newWidgets = Object.assign({}, widgets); delete newWidgets[widgetId]; widgets = newWidgets; var newMetadata = Object.assign({}, widgetMetadata); delete newMetadata[widgetId]; widgetMetadata = newMetadata; Logger.i("DesktopWidgetRegistry", "Unregistered plugin widget:", widgetId); root.pluginWidgetRegistryUpdated(); return true; } function updateWidgetData(monitorName, widgetIndex, properties) { if (widgetIndex < 0 || !monitorName) { return; } var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; var newMonitorWidgets = monitorWidgets.slice(); for (var i = 0; i < newMonitorWidgets.length; i++) { if (newMonitorWidgets[i].name === monitorName) { var widgets = (newMonitorWidgets[i].widgets || []).slice(); if (widgetIndex < widgets.length) { widgets[widgetIndex] = Object.assign({}, widgets[widgetIndex], properties); newMonitorWidgets[i] = Object.assign({}, newMonitorWidgets[i], { "widgets": widgets }); Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets; } break; } } } property var currentSettingsDialog: null function openWidgetSettings(screen, widgetIndex, widgetId, widgetData) { if (!widgetId || !screen) { return; } if (root.currentSettingsDialog) { root.currentSettingsDialog.close(); root.currentSettingsDialog.destroy(); root.currentSettingsDialog = null; } var hasSettings = false; if (root.isPluginWidget(widgetId)) { var pluginId = widgetId.replace("plugin:", ""); var manifest = PluginRegistry.getPluginManifest(pluginId); if (manifest && manifest.entryPoints && (manifest.entryPoints.desktopWidgetSettings || manifest.entryPoints.settings)) { hasSettings = true; } } else { hasSettings = root.widgetSettingsMap[widgetId] !== undefined; } if (!hasSettings) { Logger.w("DesktopWidgetRegistry", "Widget does not have settings:", widgetId); return; } var popupMenuWindow = PanelService.getPopupMenuWindow(screen); if (!popupMenuWindow) { Logger.e("DesktopWidgetRegistry", "No popup menu window found for screen"); return; } if (popupMenuWindow.hideDynamicMenu) { popupMenuWindow.hideDynamicMenu(); } var component = Qt.createComponent(Quickshell.shellDir + "/Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml"); function instantiateAndOpen() { var dialog = component.createObject(popupMenuWindow.dialogParent, { "widgetIndex": widgetIndex, "widgetData": widgetData, "widgetId": widgetId, "sectionId": screen.name, "screen": screen }); if (dialog) { root.currentSettingsDialog = dialog; dialog.updateWidgetSettings.connect((sec, idx, settings) => { root.updateWidgetData(sec, idx, settings); }); popupMenuWindow.hasDialog = true; dialog.closed.connect(() => { popupMenuWindow.hasDialog = false; popupMenuWindow.close(); if (root.currentSettingsDialog === dialog) { root.currentSettingsDialog = null; } dialog.destroy(); }); dialog.open(); } else { Logger.e("DesktopWidgetRegistry", "Failed to create widget settings dialog"); } } if (component.status === Component.Ready) { instantiateAndOpen(); } else if (component.status === Component.Error) { Logger.e("DesktopWidgetRegistry", "Error loading settings dialog component:", component.errorString()); } else { component.statusChanged.connect(() => { if (component.status === Component.Ready) { instantiateAndOpen(); } else if (component.status === Component.Error) { Logger.e("DesktopWidgetRegistry", "Error loading settings dialog component:", component.errorString()); } }); } } }