mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
PluginSystem: plugins hot reload enabled when using NOCTALIA_DEBUG=1 as env var.
This commit is contained in:
@@ -2112,7 +2112,12 @@
|
||||
"update-incompatible": "Requires Noctalia v{version} or higher",
|
||||
"update-success": "Updated {plugin} to v{version}",
|
||||
"update-version": "v{current} → v{new}",
|
||||
"updating": "Updating {plugin}..."
|
||||
"updating": "Updating {plugin}...",
|
||||
"hot-reload": {
|
||||
"description": "Automatically reload plugins when their files change. Useful for plugin development.",
|
||||
"label": "Hot reload (dev mode)"
|
||||
},
|
||||
"hot-reloaded": "Reloaded plugin: {name}"
|
||||
},
|
||||
"screen-recorder": {
|
||||
"audio": {
|
||||
|
||||
@@ -51,12 +51,34 @@ Item {
|
||||
return root.widgetId !== "" && BarWidgetRegistry.hasWidget(root.widgetId);
|
||||
}
|
||||
|
||||
// Force reload counter - incremented when plugin widget registry changes
|
||||
property int reloadCounter: 0
|
||||
|
||||
// Listen for plugin widget registry changes to force reload
|
||||
Connections {
|
||||
target: BarWidgetRegistry
|
||||
enabled: BarWidgetRegistry.isPluginWidget(root.widgetId)
|
||||
|
||||
function onPluginWidgetRegistryUpdated() {
|
||||
// Force the loader to reload by toggling active
|
||||
if (BarWidgetRegistry.hasWidget(root.widgetId)) {
|
||||
root.reloadCounter++;
|
||||
Logger.d("BarWidgetLoader", "Plugin widget registry updated, reloading:", root.widgetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
anchors.fill: parent
|
||||
asynchronous: false
|
||||
active: root.checkWidgetExists()
|
||||
sourceComponent: root.checkWidgetExists() ? BarWidgetRegistry.getWidget(root.widgetId) : null
|
||||
// Include reloadCounter in the binding to force re-evaluation
|
||||
active: root.checkWidgetExists() && (root.reloadCounter >= 0)
|
||||
sourceComponent: {
|
||||
// Depend on reloadCounter to force re-fetch of component
|
||||
var _ = root.reloadCounter;
|
||||
return root.checkWidgetExists() ? BarWidgetRegistry.getWidget(root.widgetId) : null;
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (!item)
|
||||
|
||||
@@ -16,6 +16,18 @@ Variants {
|
||||
// Direct binding to registry's widgets property for reactivity
|
||||
readonly property var registeredWidgets: DesktopWidgetRegistry.widgets
|
||||
|
||||
// Force reload counter - incremented when plugin widget registry changes
|
||||
property int pluginReloadCounter: 0
|
||||
|
||||
Connections {
|
||||
target: DesktopWidgetRegistry
|
||||
|
||||
function onPluginWidgetRegistryUpdated() {
|
||||
root.pluginReloadCounter++;
|
||||
Logger.d("DesktopWidgets", "Plugin widget registry updated, reload counter:", root.pluginReloadCounter);
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Loader {
|
||||
id: screenLoader
|
||||
required property ShellScreen modelData
|
||||
@@ -67,14 +79,15 @@ Variants {
|
||||
|
||||
delegate: Loader {
|
||||
id: widgetLoader
|
||||
// Bind to registeredWidgets to re-evaluate when plugins register/unregister
|
||||
active: (modelData.id in root.registeredWidgets)
|
||||
// Bind to registeredWidgets and pluginReloadCounter to re-evaluate when plugins register/unregister
|
||||
active: (modelData.id in root.registeredWidgets) && (root.pluginReloadCounter >= 0)
|
||||
|
||||
property var widgetData: modelData
|
||||
property int widgetIndex: index
|
||||
|
||||
sourceComponent: {
|
||||
// Access registeredWidgets to create reactive binding
|
||||
// Access registeredWidgets and pluginReloadCounter to create reactive binding
|
||||
var _ = root.pluginReloadCounter;
|
||||
var widgets = root.registeredWidgets;
|
||||
return widgets[modelData.id] || null;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ Singleton {
|
||||
signal pluginUnloaded(string pluginId)
|
||||
signal pluginEnabled(string pluginId)
|
||||
signal pluginDisabled(string pluginId)
|
||||
signal pluginReloaded(string pluginId)
|
||||
signal availablePluginsUpdated
|
||||
signal allPluginsLoaded
|
||||
|
||||
@@ -38,6 +39,16 @@ Singleton {
|
||||
property var pluginErrors: ({})
|
||||
signal pluginLoadError(string pluginId, string entryPoint, string error)
|
||||
|
||||
// Hot reload: file watchers for plugin directories
|
||||
property var pluginFileWatchers: ({}) // { pluginId: FileView }
|
||||
property bool hotReloadEnabled: Settings.isDebug
|
||||
|
||||
onHotReloadEnabledChanged: {
|
||||
if (root.initialized) {
|
||||
setHotReloadEnabled(root.hotReloadEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
// Track active fetches
|
||||
property var activeFetches: ({})
|
||||
|
||||
@@ -581,10 +592,14 @@ Singleton {
|
||||
|
||||
Logger.i("PluginService", "Plugin loaded:", pluginId);
|
||||
root.pluginLoaded(pluginId);
|
||||
|
||||
// Set up hot reload watcher if enabled
|
||||
setupPluginFileWatcher(pluginId);
|
||||
}
|
||||
|
||||
// Unload a plugin
|
||||
function unloadPlugin(pluginId) {
|
||||
// preserveSettings: if true, don't remove desktop widget settings (used for hot reload)
|
||||
function unloadPlugin(pluginId, preserveSettings) {
|
||||
var plugin = root.loadedPlugins[pluginId];
|
||||
if (!plugin) {
|
||||
Logger.w("PluginService", "Plugin not loaded:", pluginId);
|
||||
@@ -593,14 +608,20 @@ Singleton {
|
||||
|
||||
Logger.i("PluginService", "Unloading plugin:", pluginId);
|
||||
|
||||
// Remove hot reload watcher
|
||||
removePluginFileWatcher(pluginId);
|
||||
|
||||
// Unregister from BarWidgetRegistry
|
||||
if (plugin.manifest.entryPoints && plugin.manifest.entryPoints.barWidget) {
|
||||
BarWidgetRegistry.unregisterPluginWidget(pluginId);
|
||||
}
|
||||
|
||||
// Unregister from DesktopWidgetRegistry and clean up saved widget instances
|
||||
// Unregister from DesktopWidgetRegistry
|
||||
if (plugin.manifest.entryPoints && plugin.manifest.entryPoints.desktopWidget) {
|
||||
removePluginDesktopWidgetsFromSettings(pluginId);
|
||||
// Only remove settings when uninstalling, not during hot reload
|
||||
if (!preserveSettings) {
|
||||
removePluginDesktopWidgetsFromSettings(pluginId);
|
||||
}
|
||||
DesktopWidgetRegistry.unregisterPluginWidget(pluginId);
|
||||
}
|
||||
|
||||
@@ -1211,4 +1232,188 @@ Singleton {
|
||||
function hasPluginError(pluginId) {
|
||||
return pluginId in root.pluginErrors;
|
||||
}
|
||||
|
||||
// ----- Hot reload functions -----
|
||||
|
||||
// Set up file watcher for a plugin directory
|
||||
function setupPluginFileWatcher(pluginId) {
|
||||
if (!root.hotReloadEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't create duplicate watchers
|
||||
if (root.pluginFileWatchers[pluginId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var manifest = PluginRegistry.getPluginManifest(pluginId);
|
||||
if (!manifest) {
|
||||
return;
|
||||
}
|
||||
|
||||
var pluginDir = PluginRegistry.getPluginDir(pluginId);
|
||||
|
||||
// Create a debounce timer for this plugin
|
||||
var debounceTimer = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
Timer {
|
||||
property string targetPluginId: ""
|
||||
property var reloadCallback: null
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (reloadCallback) reloadCallback(targetPluginId);
|
||||
}
|
||||
}
|
||||
`, root, "HotReloadDebounce_" + pluginId);
|
||||
|
||||
// Set properties after creation to pass the callback
|
||||
debounceTimer.targetPluginId = pluginId;
|
||||
debounceTimer.reloadCallback = root.reloadPlugin;
|
||||
|
||||
// Watch the manifest file - changes here indicate plugin updates
|
||||
var manifestWatcher = Qt.createQmlObject(`
|
||||
import Quickshell.Io
|
||||
FileView {
|
||||
path: "${pluginDir}/manifest.json"
|
||||
watchChanges: true
|
||||
}
|
||||
`, root, "ManifestWatcher_" + pluginId);
|
||||
|
||||
var watchers = [manifestWatcher];
|
||||
|
||||
// Only watch entry points that actually exist in the manifest
|
||||
var entryPoints = manifest.entryPoints || {};
|
||||
var entryPointFiles = [];
|
||||
|
||||
if (entryPoints.main)
|
||||
entryPointFiles.push(entryPoints.main);
|
||||
if (entryPoints.barWidget)
|
||||
entryPointFiles.push(entryPoints.barWidget);
|
||||
if (entryPoints.desktopWidget)
|
||||
entryPointFiles.push(entryPoints.desktopWidget);
|
||||
if (entryPoints.panel)
|
||||
entryPointFiles.push(entryPoints.panel);
|
||||
if (entryPoints.settings)
|
||||
entryPointFiles.push(entryPoints.settings);
|
||||
|
||||
for (var i = 0; i < entryPointFiles.length; i++) {
|
||||
var entryPointFile = entryPointFiles[i];
|
||||
var watcher = Qt.createQmlObject(`
|
||||
import Quickshell.Io
|
||||
FileView {
|
||||
path: "${pluginDir}/${entryPointFile}"
|
||||
watchChanges: true
|
||||
}
|
||||
`, root, "FileWatcher_" + pluginId + "_" + i);
|
||||
watchers.push(watcher);
|
||||
}
|
||||
|
||||
// Connect all watchers to the debounce timer
|
||||
for (var j = 0; j < watchers.length; j++) {
|
||||
watchers[j].fileChanged.connect(function () {
|
||||
debounceTimer.restart();
|
||||
});
|
||||
}
|
||||
|
||||
root.pluginFileWatchers[pluginId] = {
|
||||
watchers: watchers,
|
||||
debounceTimer: debounceTimer
|
||||
};
|
||||
|
||||
Logger.d("PluginService", "Set up hot reload watcher for plugin:", pluginId);
|
||||
}
|
||||
|
||||
// Remove file watcher for a plugin
|
||||
function removePluginFileWatcher(pluginId) {
|
||||
var watcherData = root.pluginFileWatchers[pluginId];
|
||||
if (!watcherData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Destroy all watchers
|
||||
if (watcherData.watchers) {
|
||||
for (var i = 0; i < watcherData.watchers.length; i++) {
|
||||
if (watcherData.watchers[i]) {
|
||||
watcherData.watchers[i].destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy debounce timer
|
||||
if (watcherData.debounceTimer) {
|
||||
watcherData.debounceTimer.destroy();
|
||||
}
|
||||
|
||||
delete root.pluginFileWatchers[pluginId];
|
||||
Logger.d("PluginService", "Removed hot reload watcher for plugin:", pluginId);
|
||||
}
|
||||
|
||||
// Reload a plugin (hot reload)
|
||||
function reloadPlugin(pluginId) {
|
||||
if (!root.loadedPlugins[pluginId]) {
|
||||
Logger.w("PluginService", "Cannot reload: plugin not loaded:", pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.i("PluginService", "Hot reloading plugin:", pluginId);
|
||||
|
||||
var manifest = PluginRegistry.getPluginManifest(pluginId);
|
||||
if (!manifest) {
|
||||
Logger.e("PluginService", "Cannot reload: manifest not found for:", pluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unregister widget instances from the bar
|
||||
BarService.destroyPluginWidgetInstances(pluginId);
|
||||
|
||||
// Unload the plugin (destroys components and instances)
|
||||
// Pass true to preserve desktop widget settings during hot reload
|
||||
unloadPlugin(pluginId, true);
|
||||
|
||||
// Increment load version to invalidate Qt's component cache
|
||||
PluginRegistry.incrementPluginLoadVersion(pluginId);
|
||||
|
||||
// Use Qt.callLater to ensure destruction is complete before reloading
|
||||
// This prevents IPC handler conflicts and other timing issues
|
||||
Qt.callLater(function () {
|
||||
// Reload the plugin
|
||||
loadPlugin(pluginId);
|
||||
|
||||
// Re-setup file watcher (it was destroyed during unload)
|
||||
setupPluginFileWatcher(pluginId);
|
||||
|
||||
// Emit signal
|
||||
root.pluginReloaded(pluginId);
|
||||
|
||||
// Show toast notification
|
||||
var pluginName = manifest.name || pluginId;
|
||||
ToastService.showNotice(I18n.tr("settings.plugins.hot-reloaded", {
|
||||
"name": pluginName
|
||||
}), "");
|
||||
|
||||
Logger.i("PluginService", "Hot reload complete for plugin:", pluginId);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Enable/disable hot reload for all loaded plugins
|
||||
function setHotReloadEnabled(enabled) {
|
||||
root.hotReloadEnabled = enabled;
|
||||
|
||||
if (enabled) {
|
||||
// Set up watchers for all loaded plugins
|
||||
for (var pluginId in root.loadedPlugins) {
|
||||
setupPluginFileWatcher(pluginId);
|
||||
}
|
||||
Logger.i("PluginService", "Hot reload enabled for all plugins");
|
||||
} else {
|
||||
// Remove all watchers
|
||||
for (var pluginId in root.pluginFileWatchers) {
|
||||
removePluginFileWatcher(pluginId);
|
||||
}
|
||||
Logger.i("PluginService", "Hot reload disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +226,32 @@ Singleton {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unregister all widget instances for a plugin (used during hot reload)
|
||||
// Note: We don't destroy instances here - the Loader manages that when the component is unregistered
|
||||
function destroyPluginWidgetInstances(pluginId) {
|
||||
var widgetId = "plugin:" + pluginId;
|
||||
var keysToRemove = [];
|
||||
|
||||
// Find all instances of this plugin's widget
|
||||
for (var key in widgetInstances) {
|
||||
var widget = widgetInstances[key];
|
||||
if (widget && widget.widgetId === widgetId) {
|
||||
keysToRemove.push(key);
|
||||
Logger.d("BarService", "Unregistering plugin widget instance:", key);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from registry
|
||||
for (var i = 0; i < keysToRemove.length; i++) {
|
||||
delete widgetInstances[keysToRemove[i]];
|
||||
}
|
||||
|
||||
if (keysToRemove.length > 0) {
|
||||
Logger.i("BarService", "Unregistered", keysToRemove.length, "instance(s) of plugin widget:", widgetId);
|
||||
root.activeWidgetsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// Get pill direction for a widget instance
|
||||
function getPillDirection(widgetInstance) {
|
||||
try {
|
||||
|
||||
@@ -8,6 +8,9 @@ import qs.Modules.Bar.Widgets
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Signal emitted when plugin widgets are registered/unregistered
|
||||
signal pluginWidgetRegistryUpdated
|
||||
|
||||
// Widget registry object mapping widget names to components
|
||||
property var widgets: ({
|
||||
"ActiveWindow": activeWindowComponent,
|
||||
@@ -381,6 +384,7 @@ Singleton {
|
||||
widgetMetadata[widgetId] = metadata || {};
|
||||
|
||||
Logger.i("BarWidgetRegistry", "Registered plugin widget:", widgetId);
|
||||
root.pluginWidgetRegistryUpdated();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -399,6 +403,7 @@ Singleton {
|
||||
delete widgetMetadata[widgetId];
|
||||
|
||||
Logger.i("BarWidgetRegistry", "Unregistered plugin widget:", widgetId);
|
||||
root.pluginWidgetRegistryUpdated();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user