mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
PluginSystem: general improvements to the way we load plugins and the pluginApi.
This commit is contained in:
@@ -29,23 +29,6 @@ Item {
|
||||
return (item && item.visible) ? Math.round(item[prop]) : 0;
|
||||
}
|
||||
|
||||
// Create a dummy pluginApi that returns empty strings to avoid undefined warnings
|
||||
property var _dummyApi: QtObject {
|
||||
property var pluginSettings: ({})
|
||||
property var manifest: ({
|
||||
metadata: {
|
||||
defaultSettings: {}
|
||||
}
|
||||
})
|
||||
|
||||
function tr(key) {
|
||||
return "";
|
||||
}
|
||||
function trp(key, count) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Only load if widget exists in registry
|
||||
function checkWidgetExists(): bool {
|
||||
return root.widgetId !== "" && BarWidgetRegistry.hasWidget(root.widgetId);
|
||||
@@ -84,13 +67,6 @@ Item {
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
// Inject dummy API immediately for plugin widgets before any other code runs
|
||||
if (BarWidgetRegistry.isPluginWidget(widgetId) && item.hasOwnProperty("pluginApi")) {
|
||||
if (!item.pluginApi) {
|
||||
item.pluginApi = root._dummyApi;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.d("BarWidgetLoader", "Loading widget", widgetId, "on screen:", widgetScreen.name);
|
||||
|
||||
// Apply properties to loaded widget
|
||||
@@ -113,11 +89,11 @@ Item {
|
||||
}
|
||||
|
||||
// Inject plugin API for plugin widgets
|
||||
// The API is fully populated (settings/translations already loaded) by PluginService
|
||||
if (BarWidgetRegistry.isPluginWidget(widgetId)) {
|
||||
var pluginId = widgetId.replace("plugin:", "");
|
||||
var api = PluginService.getPluginAPI(pluginId);
|
||||
if (api && item.hasOwnProperty("pluginApi")) {
|
||||
// Inject API into widget
|
||||
item.pluginApi = api;
|
||||
Logger.d("BarWidgetLoader", "Injected plugin API for", widgetId);
|
||||
}
|
||||
|
||||
@@ -64,34 +64,11 @@ SmartPanel {
|
||||
id: pluginContentItem
|
||||
anchors.fill: parent
|
||||
|
||||
// Plugin content loader, pluginApi is injected synchronously in loadPluginPanel()
|
||||
Loader {
|
||||
id: pluginContentLoader
|
||||
anchors.fill: parent
|
||||
active: false
|
||||
|
||||
// Create a dummy pluginApi that returns empty strings to avoid undefined warnings
|
||||
property var _dummyApi: QtObject {
|
||||
property var pluginSettings: ({})
|
||||
property var manifest: ({
|
||||
metadata: {
|
||||
defaultSettings: {}
|
||||
}
|
||||
})
|
||||
|
||||
function tr(key) {
|
||||
return "";
|
||||
}
|
||||
function trp(key, count) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
// Inject the dummy API immediately to prevent undefined warnings
|
||||
if (item && item.hasOwnProperty("pluginApi") && !item.pluginApi) {
|
||||
item.pluginApi = _dummyApi;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -600,6 +600,7 @@ ColumnLayout {
|
||||
// Add source dialog
|
||||
Popup {
|
||||
id: addSourceDialog
|
||||
parent: Overlay.overlay
|
||||
modal: true
|
||||
dim: false
|
||||
anchors.centerIn: parent
|
||||
@@ -673,6 +674,7 @@ ColumnLayout {
|
||||
// Uninstall confirmation dialog
|
||||
Popup {
|
||||
id: uninstallDialog
|
||||
parent: Overlay.overlay
|
||||
modal: true
|
||||
dim: false
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -115,6 +115,9 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Track pending plugin loads for init completion
|
||||
property int _pendingPluginLoads: 0
|
||||
|
||||
function init() {
|
||||
if (root.initialized) {
|
||||
Logger.d("PluginService", "Already initialized, skipping");
|
||||
@@ -133,12 +136,12 @@ Singleton {
|
||||
var enabledIds = PluginRegistry.getEnabledPluginIds();
|
||||
Logger.i("PluginService", "Found", enabledIds.length, "enabled plugins:", JSON.stringify(enabledIds));
|
||||
|
||||
// Count plugins that need to be loaded
|
||||
var pluginsToLoad = [];
|
||||
for (var i = 0; i < enabledIds.length; i++) {
|
||||
Logger.d("PluginService", "Attempting to load plugin:", enabledIds[i]);
|
||||
var manifest = PluginRegistry.getPluginManifest(enabledIds[i]);
|
||||
if (manifest) {
|
||||
Logger.d("PluginService", "Manifest found for", enabledIds[i]);
|
||||
loadPlugin(enabledIds[i]);
|
||||
pluginsToLoad.push(enabledIds[i]);
|
||||
} else {
|
||||
Logger.w("PluginService", "Plugin", enabledIds[i], "is enabled but not found on disk - cleaning up");
|
||||
// Plugin was deleted from disk but still marked as enabled
|
||||
@@ -149,13 +152,38 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Mark plugins as fully loaded
|
||||
root.pluginsFullyLoaded = true;
|
||||
Logger.i("PluginService", "All plugins loaded");
|
||||
root.allPluginsLoaded();
|
||||
// If no plugins to load, mark as complete immediately
|
||||
if (pluginsToLoad.length === 0) {
|
||||
root.pluginsFullyLoaded = true;
|
||||
Logger.i("PluginService", "No plugins to load");
|
||||
root.allPluginsLoaded();
|
||||
refreshAvailablePlugins();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch available plugins from all sources
|
||||
refreshAvailablePlugins();
|
||||
// Track pending loads
|
||||
root._pendingPluginLoads = pluginsToLoad.length;
|
||||
|
||||
// Load all plugins (async - they will call _onPluginLoadComplete when done)
|
||||
for (var j = 0; j < pluginsToLoad.length; j++) {
|
||||
Logger.d("PluginService", "Attempting to load plugin:", pluginsToLoad[j]);
|
||||
loadPlugin(pluginsToLoad[j]);
|
||||
}
|
||||
}
|
||||
|
||||
// Called when a plugin finishes loading (success or failure)
|
||||
function _onPluginLoadComplete() {
|
||||
root._pendingPluginLoads--;
|
||||
|
||||
if (root._pendingPluginLoads <= 0) {
|
||||
// All plugins finished loading
|
||||
root.pluginsFullyLoaded = true;
|
||||
Logger.i("PluginService", "All plugins loaded");
|
||||
root.allPluginsLoaded();
|
||||
|
||||
// Fetch available plugins from all sources
|
||||
refreshAvailablePlugins();
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh available plugins from all sources
|
||||
@@ -223,13 +251,20 @@ Singleton {
|
||||
}
|
||||
|
||||
Logger.i("PluginService", "Loaded", registry.plugins.length, "plugins from", source.name);
|
||||
|
||||
// Remove from active fetches BEFORE emitting signal so handler sees correct count
|
||||
delete activeFetches[source.url];
|
||||
fetchProcess.destroy();
|
||||
|
||||
root.availablePluginsUpdated();
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.e("PluginService", "Failed to parse registry from", source.name, ":", e);
|
||||
Logger.e("PluginService", "Response was:", response ? response.substring(0, 200) : "null");
|
||||
}
|
||||
|
||||
// Clean up on error or empty response
|
||||
delete activeFetches[source.url];
|
||||
fetchProcess.destroy();
|
||||
});
|
||||
@@ -487,6 +522,19 @@ Singleton {
|
||||
return changed;
|
||||
}
|
||||
|
||||
// Load plugin settings and translations before instantiating components
|
||||
// This ensures pluginApi is fully populated before being passed to createObject()
|
||||
function loadPluginData(pluginId, manifest, callback) {
|
||||
// Load settings first
|
||||
loadPluginSettings(pluginId, function (settings) {
|
||||
// Then load translations
|
||||
loadPluginTranslationsAsync(pluginId, manifest, I18n.langCode, function (translations) {
|
||||
// Both ready - call back with complete data
|
||||
callback(settings, translations);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Load a plugin
|
||||
function loadPlugin(pluginId) {
|
||||
if (root.loadedPlugins[pluginId]) {
|
||||
@@ -504,97 +552,98 @@ Singleton {
|
||||
|
||||
Logger.i("PluginService", "Loading plugin:", pluginId);
|
||||
|
||||
// Create plugin API object
|
||||
var pluginApi = createPluginAPI(pluginId, manifest);
|
||||
// Load settings and translations FIRST, then create API and instantiate components
|
||||
loadPluginData(pluginId, manifest, function (settings, translations) {
|
||||
// Create plugin API object with pre-loaded data
|
||||
var pluginApi = createPluginAPI(pluginId, manifest, settings, translations);
|
||||
|
||||
// Initialize plugin entry with API and manifest
|
||||
root.loadedPlugins[pluginId] = {
|
||||
barWidget: null,
|
||||
desktopWidget: null,
|
||||
mainInstance: null,
|
||||
api: pluginApi,
|
||||
manifest: manifest
|
||||
};
|
||||
// Initialize plugin entry with API and manifest
|
||||
root.loadedPlugins[pluginId] = {
|
||||
barWidget: null,
|
||||
desktopWidget: null,
|
||||
mainInstance: null,
|
||||
api: pluginApi,
|
||||
manifest: manifest
|
||||
};
|
||||
|
||||
// Clear any previous errors for this plugin
|
||||
root.clearPluginError(pluginId);
|
||||
// Clear any previous errors for this plugin
|
||||
root.clearPluginError(pluginId);
|
||||
|
||||
// Load Main.qml entry point if it exists
|
||||
if (manifest.entryPoints && manifest.entryPoints.main) {
|
||||
var mainPath = pluginDir + "/" + manifest.entryPoints.main;
|
||||
var loadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
|
||||
var mainComponent = Qt.createComponent("file://" + mainPath + "?v=" + loadVersion);
|
||||
// Load Main.qml entry point if it exists
|
||||
if (manifest.entryPoints && manifest.entryPoints.main) {
|
||||
var mainPath = pluginDir + "/" + manifest.entryPoints.main;
|
||||
var loadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
|
||||
var mainComponent = Qt.createComponent("file://" + mainPath + "?v=" + loadVersion);
|
||||
|
||||
if (mainComponent.status === Component.Ready) {
|
||||
// Get the plugin container from shell.qml (must be in graphics scene)
|
||||
if (!root.pluginContainer) {
|
||||
Logger.e("PluginService", "Plugin container not set. Shell must set PluginService.pluginContainer.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Instantiate Main.qml with container as parent (places it in graphics scene)
|
||||
var mainInstance = mainComponent.createObject(root.pluginContainer);
|
||||
|
||||
if (mainInstance) {
|
||||
// Set pluginApi property after creation
|
||||
if (mainInstance.hasOwnProperty('pluginApi')) {
|
||||
mainInstance.pluginApi = pluginApi;
|
||||
} else {
|
||||
Logger.w("PluginService", "Main.qml for", pluginId, "should declare 'property var pluginApi: null'");
|
||||
if (mainComponent.status === Component.Ready) {
|
||||
// Get the plugin container from shell.qml (must be in graphics scene)
|
||||
if (!root.pluginContainer) {
|
||||
Logger.e("PluginService", "Plugin container not set. Shell must set PluginService.pluginContainer.");
|
||||
return;
|
||||
}
|
||||
|
||||
root.loadedPlugins[pluginId].mainInstance = mainInstance;
|
||||
pluginApi.mainInstance = mainInstance;
|
||||
Logger.i("PluginService", "Loaded Main.qml for plugin:", pluginId);
|
||||
} else {
|
||||
root.recordPluginError(pluginId, "main", "Failed to instantiate Main.qml");
|
||||
// Instantiate Main.qml with pluginApi passed directly in createObject
|
||||
var mainInstance = mainComponent.createObject(root.pluginContainer, {
|
||||
pluginApi: pluginApi
|
||||
});
|
||||
|
||||
if (mainInstance) {
|
||||
root.loadedPlugins[pluginId].mainInstance = mainInstance;
|
||||
pluginApi.mainInstance = mainInstance;
|
||||
Logger.i("PluginService", "Loaded Main.qml for plugin:", pluginId);
|
||||
} else {
|
||||
root.recordPluginError(pluginId, "main", "Failed to instantiate Main.qml");
|
||||
}
|
||||
} else if (mainComponent.status === Component.Error) {
|
||||
root.recordPluginError(pluginId, "main", mainComponent.errorString());
|
||||
}
|
||||
} else if (mainComponent.status === Component.Error) {
|
||||
root.recordPluginError(pluginId, "main", mainComponent.errorString());
|
||||
}
|
||||
}
|
||||
|
||||
// Load bar widget component if provided (don't instantiate - BarWidgetRegistry will do that)
|
||||
if (manifest.entryPoints && manifest.entryPoints.barWidget) {
|
||||
var widgetPath = pluginDir + "/" + manifest.entryPoints.barWidget;
|
||||
var widgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
|
||||
var widgetComponent = Qt.createComponent("file://" + widgetPath + "?v=" + widgetLoadVersion);
|
||||
// Load bar widget component if provided (don't instantiate - BarWidgetRegistry will do that)
|
||||
if (manifest.entryPoints && manifest.entryPoints.barWidget) {
|
||||
var widgetPath = pluginDir + "/" + manifest.entryPoints.barWidget;
|
||||
var widgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
|
||||
var widgetComponent = Qt.createComponent("file://" + widgetPath + "?v=" + widgetLoadVersion);
|
||||
|
||||
if (widgetComponent.status === Component.Ready) {
|
||||
root.loadedPlugins[pluginId].barWidget = widgetComponent;
|
||||
pluginApi.barWidget = widgetComponent;
|
||||
if (widgetComponent.status === Component.Ready) {
|
||||
root.loadedPlugins[pluginId].barWidget = widgetComponent;
|
||||
pluginApi.barWidget = widgetComponent;
|
||||
|
||||
// Register with BarWidgetRegistry
|
||||
BarWidgetRegistry.registerPluginWidget(pluginId, widgetComponent, manifest.metadata);
|
||||
Logger.i("PluginService", "Loaded bar widget for plugin:", pluginId);
|
||||
} else if (widgetComponent.status === Component.Error) {
|
||||
root.recordPluginError(pluginId, "barWidget", widgetComponent.errorString());
|
||||
// Register with BarWidgetRegistry
|
||||
BarWidgetRegistry.registerPluginWidget(pluginId, widgetComponent, manifest.metadata);
|
||||
Logger.i("PluginService", "Loaded bar widget for plugin:", pluginId);
|
||||
} else if (widgetComponent.status === Component.Error) {
|
||||
root.recordPluginError(pluginId, "barWidget", widgetComponent.errorString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load desktop widget component if provided (don't instantiate - DesktopWidgetRegistry will do that)
|
||||
if (manifest.entryPoints && manifest.entryPoints.desktopWidget) {
|
||||
var desktopWidgetPath = pluginDir + "/" + manifest.entryPoints.desktopWidget;
|
||||
var desktopWidgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
|
||||
var desktopWidgetComponent = Qt.createComponent("file://" + desktopWidgetPath + "?v=" + desktopWidgetLoadVersion);
|
||||
// Load desktop widget component if provided (don't instantiate - DesktopWidgetRegistry will do that)
|
||||
if (manifest.entryPoints && manifest.entryPoints.desktopWidget) {
|
||||
var desktopWidgetPath = pluginDir + "/" + manifest.entryPoints.desktopWidget;
|
||||
var desktopWidgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
|
||||
var desktopWidgetComponent = Qt.createComponent("file://" + desktopWidgetPath + "?v=" + desktopWidgetLoadVersion);
|
||||
|
||||
if (desktopWidgetComponent.status === Component.Ready) {
|
||||
root.loadedPlugins[pluginId].desktopWidget = desktopWidgetComponent;
|
||||
pluginApi.desktopWidget = desktopWidgetComponent;
|
||||
if (desktopWidgetComponent.status === Component.Ready) {
|
||||
root.loadedPlugins[pluginId].desktopWidget = desktopWidgetComponent;
|
||||
pluginApi.desktopWidget = desktopWidgetComponent;
|
||||
|
||||
// Register with DesktopWidgetRegistry
|
||||
DesktopWidgetRegistry.registerPluginWidget(pluginId, desktopWidgetComponent, manifest.metadata);
|
||||
Logger.i("PluginService", "Loaded desktop widget for plugin:", pluginId);
|
||||
} else if (desktopWidgetComponent.status === Component.Error) {
|
||||
root.recordPluginError(pluginId, "desktopWidget", desktopWidgetComponent.errorString());
|
||||
// Register with DesktopWidgetRegistry
|
||||
DesktopWidgetRegistry.registerPluginWidget(pluginId, desktopWidgetComponent, manifest.metadata);
|
||||
Logger.i("PluginService", "Loaded desktop widget for plugin:", pluginId);
|
||||
} else if (desktopWidgetComponent.status === Component.Error) {
|
||||
root.recordPluginError(pluginId, "desktopWidget", desktopWidgetComponent.errorString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.i("PluginService", "Plugin loaded:", pluginId);
|
||||
root.pluginLoaded(pluginId);
|
||||
Logger.i("PluginService", "Plugin loaded:", pluginId);
|
||||
root.pluginLoaded(pluginId);
|
||||
|
||||
// Set up hot reload watcher if enabled
|
||||
setupPluginFileWatcher(pluginId);
|
||||
// Set up hot reload watcher if enabled
|
||||
setupPluginFileWatcher(pluginId);
|
||||
|
||||
// Notify that this plugin finished loading (for init tracking)
|
||||
root._onPluginLoadComplete();
|
||||
});
|
||||
}
|
||||
|
||||
// Unload a plugin
|
||||
@@ -635,10 +684,9 @@ Singleton {
|
||||
Logger.i("PluginService", "Unloaded plugin:", pluginId);
|
||||
}
|
||||
|
||||
// Create plugin API object
|
||||
function createPluginAPI(pluginId, manifest) {
|
||||
// Create plugin API object with pre-loaded settings and translations
|
||||
function createPluginAPI(pluginId, manifest, settings, translations) {
|
||||
var pluginDir = PluginRegistry.getPluginDir(pluginId);
|
||||
var settingsFile = PluginRegistry.getPluginSettingsFile(pluginId);
|
||||
|
||||
var api = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
@@ -679,15 +727,9 @@ Singleton {
|
||||
// Set current language (can't use binding in Qt.createQmlObject string)
|
||||
api.currentLanguage = I18n.langCode;
|
||||
|
||||
// Load plugin settings
|
||||
loadPluginSettings(pluginId, function (settings) {
|
||||
api.pluginSettings = settings;
|
||||
});
|
||||
|
||||
// Load plugin translations for current language
|
||||
loadPluginTranslationsAsync(pluginId, manifest, I18n.langCode, function (translations) {
|
||||
api.pluginTranslations = translations;
|
||||
});
|
||||
// Set pre-loaded settings and translations (available immediately!)
|
||||
api.pluginSettings = settings || {};
|
||||
api.pluginTranslations = translations || {};
|
||||
|
||||
// ----------------------------------------
|
||||
// Helper function to get nested property by dot notation
|
||||
|
||||
@@ -62,38 +62,10 @@ Popup {
|
||||
color: Color.mOutline
|
||||
}
|
||||
|
||||
// Settings loader
|
||||
// Settings loader - pluginApi is passed via setSource() in openPluginSettings()
|
||||
Loader {
|
||||
id: settingsLoader
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Create a dummy pluginApi that returns empty strings to avoid undefined warnings
|
||||
property var _dummyApi: QtObject {
|
||||
property var pluginSettings: ({})
|
||||
property var manifest: ({
|
||||
metadata: {
|
||||
defaultSettings: {}
|
||||
}
|
||||
})
|
||||
|
||||
function tr(key) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function trp(key, count) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
// Inject dummy API immediately to prevent undefined warnings during initialization
|
||||
if (item && item.hasOwnProperty("pluginApi") && !item.pluginApi) {
|
||||
item.pluginApi = _dummyApi;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
|
||||
@@ -143,18 +143,6 @@ ShellRoot {
|
||||
PluginService.screenDetector = screenDetector;
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for when available plugins are fetched, then check for updates
|
||||
Connections {
|
||||
target: PluginService
|
||||
property bool hasCheckedOnStartup: false
|
||||
function onAvailablePluginsUpdated() {
|
||||
if (!hasCheckedOnStartup) {
|
||||
hasCheckedOnStartup = true;
|
||||
PluginService.checkForUpdates();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user