From a10d55e7f5106508c81b13eb26b52340df4b2412 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 24 Aug 2025 23:50:09 -0400 Subject: [PATCH] Bar widgets: modular loading refactoring via BarWidgetRegistry+NWidgetLoader - Hot reload is working again. - Should also be more memory efficient on multi monitors. --- Commons/WidgetLoader.qml | 88 ---------- Modules/Bar/Bar.qml | 89 ++-------- Modules/Bar/{Widgets => Extras}/TrayMenu.qml | 0 Modules/Bar/Widgets/ActiveWindow.qml | 9 +- Modules/Bar/Widgets/ArchUpdater.qml | 16 +- Modules/Bar/Widgets/Battery.qml | 155 ++++++++++-------- Modules/Bar/Widgets/Bluetooth.qml | 9 +- Modules/Bar/Widgets/Brightness.qml | 9 +- Modules/Bar/Widgets/Clock.qml | 15 +- Modules/Bar/Widgets/KeyboardLayout.qml | 9 +- Modules/Bar/Widgets/MediaMini.qml | 4 + Modules/Bar/Widgets/NotificationHistory.qml | 8 +- Modules/Bar/Widgets/PowerProfile.qml | 4 +- .../Bar/Widgets/ScreenRecorderIndicator.qml | 13 +- Modules/Bar/Widgets/SidePanelToggle.qml | 9 +- Modules/Bar/Widgets/SystemMonitor.qml | 4 + Modules/Bar/Widgets/Tray.qml | 15 +- Modules/Bar/Widgets/Volume.qml | 8 +- Modules/Bar/Widgets/WiFi.qml | 9 +- Modules/Bar/Widgets/Workspace.qml | 64 ++++---- Modules/Calendar/Calendar.qml | 11 -- Modules/SettingsPanel/Tabs/BarTab.qml | 41 ++--- Modules/SidePanel/Cards/UtilitiesCard.qml | 1 + Services/BarWidgetRegistry.qml | 99 +++++++++++ Services/CavaService.qml | 2 +- Services/CompositorService.qml | 2 +- Services/GitHubService.qml | 3 +- Services/LocationService.qml | 3 +- Services/NotificationService.qml | 3 +- Services/PanelService.qml | 31 +++- Services/ScreenRecorderService.qml | 1 - Widgets/NPanel.qml | 8 +- Widgets/NPill.qml | 30 ++-- .../{NWidgetCard.qml => NSectionEditor.qml} | 51 +++--- Widgets/NWidgetLoader.qml | 46 ++++++ shell.qml | 91 +++++----- 36 files changed, 514 insertions(+), 446 deletions(-) delete mode 100644 Commons/WidgetLoader.qml rename Modules/Bar/{Widgets => Extras}/TrayMenu.qml (100%) create mode 100644 Services/BarWidgetRegistry.qml rename Widgets/{NWidgetCard.qml => NSectionEditor.qml} (83%) create mode 100644 Widgets/NWidgetLoader.qml diff --git a/Commons/WidgetLoader.qml b/Commons/WidgetLoader.qml deleted file mode 100644 index 0a2b01c..0000000 --- a/Commons/WidgetLoader.qml +++ /dev/null @@ -1,88 +0,0 @@ -import QtQuick -import qs.Commons - -QtObject { - id: root - - // Signal emitted when widget loading status changes - signal widgetLoaded(string widgetName) - signal widgetFailed(string widgetName, string error) - signal loadingComplete(int total, int loaded, int failed) - - // Properties to track loading status - property int totalWidgets: 0 - property int loadedWidgets: 0 - property int failedWidgets: 0 - - // Auto-discover widget components - function getWidgetComponent(widgetName) { - if (!widgetName || widgetName.trim() === "") { - return null - } - - const widgetPath = `../Modules/Bar/Widgets/${widgetName}.qml` - - // Try to load the widget directly from file - const component = Qt.createComponent(widgetPath) - if (component.status === Component.Ready) { - return component - } - - const errorMsg = `Failed to load ${widgetName}.qml widget, status: ${component.status}, error: ${component.errorString( - )}` - Logger.error("WidgetLoader", errorMsg) - return null - } - - // Initialize loading tracking - function initializeLoading(widgetList) { - totalWidgets = widgetList.length - loadedWidgets = 0 - failedWidgets = 0 - } - - // Track widget loading success - function onWidgetLoaded(widgetName) { - loadedWidgets++ - widgetLoaded(widgetName) - - if (loadedWidgets + failedWidgets === totalWidgets) { - Logger.log("WidgetLoader", `Loaded ${loadedWidgets} widgets`) - loadingComplete(totalWidgets, loadedWidgets, failedWidgets) - } - } - - // Track widget loading failure - function onWidgetFailed(widgetName, error) { - failedWidgets++ - widgetFailed(widgetName, error) - - if (loadedWidgets + failedWidgets === totalWidgets) { - loadingComplete(totalWidgets, loadedWidgets, failedWidgets) - } - } - - // This is where you should add your Modules/Bar/Widgets/ - // so it gets registered in the BarTab - function discoverAvailableWidgets() { - const widgetFiles = ["ActiveWindow", "ArchUpdater", "Battery", "Bluetooth", "Brightness", "Clock", "KeyboardLayout", "MediaMini", "NotificationHistory", "PowerProfile", "ScreenRecorderIndicator", "SidePanelToggle", "SystemMonitor", "Tray", "Volume", "WiFi", "Workspace"] - - const availableWidgets = [] - - widgetFiles.forEach(widgetName => { - // Test if the widget can be loaded - const component = getWidgetComponent(widgetName) - if (component) { - availableWidgets.push({ - "key": widgetName, - "name": widgetName - }) - } - }) - - // Sort alphabetically - availableWidgets.sort((a, b) => a.name.localeCompare(b.name)) - - return availableWidgets - } -} diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 48ed76e..eccb872 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -48,6 +48,7 @@ Variants { layer.enabled: true } + // ------------------------------ // Left Section - Dynamic Widgets Row { id: leftSection @@ -61,30 +62,19 @@ Variants { Repeater { model: Settings.data.bar.widgets.left delegate: Loader { - id: leftWidgetLoader - sourceComponent: widgetLoader.getWidgetComponent(modelData) active: true - visible: { - if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) - return false - if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) - return false - if (modelData === "Battery" && !shouldShowBattery()) - return false - return true - } - anchors.verticalCenter: parent.verticalCenter - onStatusChanged: { - if (status === Loader.Error) { - widgetLoader.onWidgetFailed(modelData, "Loader error") - } else if (status === Loader.Ready) { - widgetLoader.onWidgetLoaded(modelData) + sourceComponent: NWidgetLoader { + widgetName: modelData + widgetProps: { + "screen": screen } } + anchors.verticalCenter: parent.verticalCenter } } } + // ------------------------------ // Center Section - Dynamic Widgets Row { id: centerSection @@ -97,30 +87,19 @@ Variants { Repeater { model: Settings.data.bar.widgets.center delegate: Loader { - id: centerWidgetLoader - sourceComponent: widgetLoader.getWidgetComponent(modelData) active: true - visible: { - if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) - return false - if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) - return false - if (modelData === "Battery" && !shouldShowBattery()) - return false - return true - } - anchors.verticalCenter: parent.verticalCenter - onStatusChanged: { - if (status === Loader.Error) { - widgetLoader.onWidgetFailed(modelData, "Loader error") - } else if (status === Loader.Ready) { - widgetLoader.onWidgetLoaded(modelData) + sourceComponent: NWidgetLoader { + widgetName: modelData + widgetProps: { + "screen": screen } } + anchors.verticalCenter: parent.verticalCenter } } } + // ------------------------------ // Right Section - Dynamic Widgets Row { id: rightSection @@ -134,49 +113,17 @@ Variants { Repeater { model: Settings.data.bar.widgets.right delegate: Loader { - id: rightWidgetLoader - sourceComponent: widgetLoader.getWidgetComponent(modelData) active: true - visible: { - if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) - return false - if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) - return false - return true - } - anchors.verticalCenter: parent.verticalCenter - onStatusChanged: { - if (status === Loader.Error) { - widgetLoader.onWidgetFailed(modelData, "Loader error") - } else if (status === Loader.Ready) { - widgetLoader.onWidgetLoaded(modelData) + sourceComponent: NWidgetLoader { + widgetName: modelData + widgetProps: { + "screen": screen } } + anchors.verticalCenter: parent.verticalCenter } } } } - - // Helper function to check if battery widget should be visible (same logic as Battery.qml) - function shouldShowBattery() { - // For now, always show battery widget and let it handle its own visibility - // The Battery widget has its own testMode and visibility logic - return true - } - - // Widget loader instance - WidgetLoader { - id: widgetLoader - - onWidgetFailed: function (widgetName, error) { - Logger.error("Bar", `Widget failed: ${widgetName} - ${error}`) - } - } - - // Initialize widget loading tracking - Component.onCompleted: { - const allWidgets = [...Settings.data.bar.widgets.left, ...Settings.data.bar.widgets.center, ...Settings.data.bar.widgets.right] - widgetLoader.initializeLoading(allWidgets) - } } } diff --git a/Modules/Bar/Widgets/TrayMenu.qml b/Modules/Bar/Extras/TrayMenu.qml similarity index 100% rename from Modules/Bar/Widgets/TrayMenu.qml rename to Modules/Bar/Extras/TrayMenu.qml diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 904b5b4..8e41fcc 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -9,13 +9,16 @@ import qs.Widgets Row { id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + property bool showingFullTitle: false + property int lastWindowIndex: -1 + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling visible: getTitle() !== "" - property bool showingFullTitle: false - property int lastWindowIndex: -1 - // Timer to hide full title after window switch Timer { id: fullTitleTimer diff --git a/Modules/Bar/Widgets/ArchUpdater.qml b/Modules/Bar/Widgets/ArchUpdater.qml index 6b09817..4636840 100644 --- a/Modules/Bar/Widgets/ArchUpdater.qml +++ b/Modules/Bar/Widgets/ArchUpdater.qml @@ -1,16 +1,18 @@ -import qs.Commons -import qs.Services -import qs.Widgets import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets NIconButton { id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + sizeMultiplier: 0.8 - - readonly property real scaling: ScalingService.scale(screen) - colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface colorBorder: Color.transparent @@ -64,7 +66,7 @@ NIconButton { if (ArchUpdaterService.updatePackages.length > 0) { // Show confirmation dialog for updates - PanelService.updatePanel.toggle(screen) + PanelService.getPanel("archUpdaterPanel").toggle(screen) } else { // Just refresh if no updates available ArchUpdaterService.doPoll() diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index 8349da5..46f5701 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -6,89 +6,100 @@ import qs.Commons import qs.Services import qs.Widgets -NPill { +Item { id: root - // Test mode - property bool testMode: false - property int testPercent: 49 - property bool testCharging: false + property ShellScreen screen + property real scaling: ScalingService.scale(screen) - property var battery: UPower.displayDevice - property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) - property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) - property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) - property bool show: isReady && percent > 0 + implicitWidth: pill.width + implicitHeight: pill.height - // Choose icon based on charge and charging state - function batteryIcon() { + NPill { + id: pill - if (charging) - return "battery_android_bolt" + // Test mode + property bool testMode: false + property int testPercent: 49 + property bool testCharging: false - if (percent >= 95) - return "battery_android_full" + property var battery: UPower.displayDevice + property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) + property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) + property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) - // Hardcoded battery symbols - if (percent >= 85) - return "battery_android_6" - if (percent >= 70) - return "battery_android_5" - if (percent >= 55) - return "battery_android_4" - if (percent >= 40) - return "battery_android_3" - if (percent >= 25) - return "battery_android_2" - if (percent >= 10) - return "battery_android_1" - if (percent >= 0) - return "battery_android_0" - } + // Choose icon based on charge and charging state + function batteryIcon() { - visible: testMode || (isReady && battery.isLaptopBattery) + if (!isReady || !battery.isLaptopBattery) + return "battery_android_alert" - icon: root.batteryIcon() - text: Math.round(root.percent) + "%" - textColor: charging ? Color.mPrimary : Color.mOnSurface - forceShown: Settings.data.bar.alwaysShowBatteryPercentage - tooltipText: { - let lines = [] + if (charging) + return "battery_android_bolt" - if (testMode) { - lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345)) + if (percent >= 95) + return "battery_android_full" + + // Hardcoded battery symbols + if (percent >= 85) + return "battery_android_6" + if (percent >= 70) + return "battery_android_5" + if (percent >= 55) + return "battery_android_4" + if (percent >= 40) + return "battery_android_3" + if (percent >= 25) + return "battery_android_2" + if (percent >= 10) + return "battery_android_1" + if (percent >= 0) + return "battery_android_0" + } + + icon: batteryIcon() + text: (isReady && battery.isLaptopBattery) ? Math.round(percent) + "%" : "-" + textColor: charging ? Color.mPrimary : Color.mOnSurface + forceOpen: isReady && battery.isLaptopBattery && Settings.data.bar.alwaysShowBatteryPercentage + disableOpen: (!isReady || !battery.isLaptopBattery) + tooltipText: { + let lines = [] + + if (testMode) { + lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(12345)) + return lines.join("\n") + } + + if (!isReady || !battery.isLaptopBattery) { + return "No Battery Detected" + } + + if (battery.timeToEmpty > 0) { + lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty)) + } + + if (battery.timeToFull > 0) { + lines.push("Time Until Full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull)) + } + + if (battery.changeRate !== undefined) { + const rate = battery.changeRate + if (rate > 0) { + lines.push(charging ? "Charging Rate: " + rate.toFixed(2) + " W" : "Discharging Rate: " + rate.toFixed( + 2) + " W") + } else if (rate < 0) { + lines.push("Discharging Rate: " + Math.abs(rate).toFixed(2) + " W") + } else { + lines.push("Estimating...") + } + } else { + lines.push(charging ? "Charging" : "Discharging") + } + + if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) { + lines.push("Health: " + Math.round(battery.healthPercentage) + "%") + } return lines.join("\n") } - - if (!root.isReady) { - return "" - } - - if (root.battery.timeToEmpty > 0) { - lines.push("Time left: " + Time.formatVagueHumanReadableDuration(root.battery.timeToEmpty)) - } - - if (root.battery.timeToFull > 0) { - lines.push("Time until full: " + Time.formatVagueHumanReadableDuration(root.battery.timeToFull)) - } - - if (root.battery.changeRate !== undefined) { - const rate = root.battery.changeRate - if (rate > 0) { - lines.push(root.charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed( - 2) + " W") - } else if (rate < 0) { - lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W") - } else { - lines.push("Estimating...") - } - } else { - lines.push(root.charging ? "Charging" : "Discharging") - } - - if (root.battery.healthPercentage !== undefined && root.battery.healthPercentage > 0) { - lines.push("Health: " + Math.round(root.battery.healthPercentage) + "%") - } - return lines.join("\n") } } diff --git a/Modules/Bar/Widgets/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml index bbe35f1..f977128 100644 --- a/Modules/Bar/Widgets/Bluetooth.qml +++ b/Modules/Bar/Widgets/Bluetooth.qml @@ -10,8 +10,11 @@ import qs.Widgets NIconButton { id: root - sizeMultiplier: 0.8 + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + visible: Settings.data.network.bluetoothEnabled + sizeMultiplier: 0.8 colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface colorBorder: Color.transparent @@ -28,7 +31,5 @@ NIconButton { } } tooltipText: "Bluetooth Devices" - onClicked: { - bluetoothPanel.toggle(screen) - } + onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen) } diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index 58c8cc6..5091e66 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -8,13 +8,16 @@ import qs.Widgets Item { id: root - width: pill.width - height: pill.height - visible: getMonitor() !== null + property ShellScreen screen + property real scaling: ScalingService.scale(screen) // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false + width: pill.width + height: pill.height + visible: getMonitor() !== null + function getMonitor() { return BrightnessService.getMonitorForScreen(screen) || null } diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index 1a269c9..ced71d8 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -1,16 +1,21 @@ import QtQuick +import Quickshell import qs.Commons import qs.Services import qs.Widgets -// Clock Icon with attached calendar Rectangle { id: root - width: clock.width + Style.marginM * 2 * scaling - height: Math.round(Style.capsuleHeight * scaling) + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + + implicitWidth: clock.width + Style.marginM * 2 * scaling + implicitHeight: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant + // Clock Icon with attached calendar NClock { id: clock anchors.verticalCenter: parent.verticalCenter @@ -24,7 +29,7 @@ Rectangle { } onEntered: { - if (!calendarPanel.active) { + if (!PanelService.getPanel("calendarPanel")?.active) { tooltip.show() } } @@ -33,7 +38,7 @@ Rectangle { } onClicked: { tooltip.hide() - calendarPanel.toggle(screen) + PanelService.getPanel("calendarPanel")?.toggle(screen) } } } diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml index 1ae3acb..0ee56e5 100644 --- a/Modules/Bar/Widgets/KeyboardLayout.qml +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -6,15 +6,18 @@ import qs.Commons import qs.Services import qs.Widgets -Item { +Row { id: root - width: pill.width - height: pill.height + property ShellScreen screen + property real scaling: ScalingService.scale(screen) // Use the shared service for keyboard layout property string currentLayout: KeyboardLayoutService.currentLayout + width: pill.width + height: pill.height + NPill { id: pill icon: "keyboard_alt" diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index b884196..dee6c42 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -9,6 +9,10 @@ import qs.Widgets Row { id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling visible: MediaService.currentPlayer !== null && MediaService.canPlay diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 8cf0502..15adeea 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -10,6 +10,9 @@ import qs.Widgets NIconButton { id: root + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + sizeMultiplier: 0.8 icon: "notifications" tooltipText: "Notification History" @@ -17,8 +20,5 @@ NIconButton { colorFg: Color.mOnSurface colorBorder: Color.transparent colorBorderHover: Color.transparent - - onClicked: { - notificationHistoryPanel.toggle(screen) - } + onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen) } diff --git a/Modules/Bar/Widgets/PowerProfile.qml b/Modules/Bar/Widgets/PowerProfile.qml index 7d3afa9..47fab43 100644 --- a/Modules/Bar/Widgets/PowerProfile.qml +++ b/Modules/Bar/Widgets/PowerProfile.qml @@ -1,7 +1,7 @@ import QtQuick +import QtQuick.Layouts import Quickshell import Quickshell.Services.UPower -import QtQuick.Layouts import qs.Commons import qs.Services import qs.Widgets @@ -9,6 +9,8 @@ import qs.Widgets NIconButton { id: root + property ShellScreen screen + property real scaling: ScalingService.scale(screen) property var powerProfiles: PowerProfiles readonly property bool hasPP: powerProfiles.hasPerformanceProfile diff --git a/Modules/Bar/Widgets/ScreenRecorderIndicator.qml b/Modules/Bar/Widgets/ScreenRecorderIndicator.qml index f7606c9..2df0ef3 100644 --- a/Modules/Bar/Widgets/ScreenRecorderIndicator.qml +++ b/Modules/Bar/Widgets/ScreenRecorderIndicator.qml @@ -1,18 +1,21 @@ +import Quickshell import qs.Commons import qs.Services import qs.Widgets // Screen Recording Indicator NIconButton { - id: screenRecordingIndicator + id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + + visible: ScreenRecorderService.isRecording icon: "videocam" tooltipText: "Screen Recording Active\nClick To Stop Recording" sizeMultiplier: 0.8 colorBg: Color.mPrimary colorFg: Color.mOnPrimary - visible: ScreenRecorderService.isRecording anchors.verticalCenter: parent.verticalCenter - onClicked: { - ScreenRecorderService.toggleRecording() - } + onClicked: ScreenRecorderService.toggleRecording() } diff --git a/Modules/Bar/Widgets/SidePanelToggle.qml b/Modules/Bar/Widgets/SidePanelToggle.qml index 42c634c..5b985b8 100644 --- a/Modules/Bar/Widgets/SidePanelToggle.qml +++ b/Modules/Bar/Widgets/SidePanelToggle.qml @@ -1,9 +1,14 @@ import Quickshell import qs.Commons import qs.Widgets +import qs.Services NIconButton { - id: sidePanelToggle + id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + icon: "widgets" tooltipText: "Open Side Panel" sizeMultiplier: 0.8 @@ -14,5 +19,5 @@ NIconButton { colorBorderHover: Color.transparent anchors.verticalCenter: parent.verticalCenter - onClicked: sidePanel.toggle(screen) + onClicked: PanelService.getPanel("sidePanel")?.toggle(screen) } diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index 18bb8b6..610a940 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -6,6 +6,10 @@ import qs.Widgets Row { id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 553e56b..3d97c85 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -6,15 +6,20 @@ import Quickshell import Quickshell.Services.SystemTray import Quickshell.Widgets import qs.Commons +import qs.Modules.Bar.Extras import qs.Services import qs.Widgets Rectangle { + id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) readonly property real itemSize: 24 * scaling visible: SystemTray.items.values.length > 0 - width: tray.width + Style.marginM * scaling * 2 - height: Math.round(Style.capsuleHeight * scaling) + implicitWidth: tray.width + Style.marginM * scaling * 2 + implicitHeight: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant @@ -134,9 +139,7 @@ Rectangle { function open() { visible = true - // Register into the panel service - // so this will autoclose if we open another panel - PanelService.registerOpen(trayPanel) + PanelService.willOpenPanel(trayPanel) } function close() { @@ -152,7 +155,7 @@ Rectangle { Loader { id: trayMenu - source: "TrayMenu.qml" + source: "../Extras/TrayMenu.qml" } } } diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 36dbbb1..e115102 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -9,12 +9,15 @@ import qs.Widgets Item { id: root - width: pill.width - height: pill.height + property ShellScreen screen + property real scaling: ScalingService.scale(screen) // Used to avoid opening the pill on Quickshell startup property bool firstVolumeReceived: false + implicitWidth: pill.width + implicitHeight: pill.height + function getIcon() { if (AudioService.muted) { return "volume_off" @@ -64,6 +67,7 @@ Item { } } onClicked: { + var settingsPanel = PanelService.getPanel("settingsPanel") settingsPanel.requestedTab = SettingsPanel.Tab.AudioService settingsPanel.open(screen) } diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 5b94a38..845a110 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -10,6 +10,11 @@ import qs.Widgets NIconButton { id: root + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + + visible: Settings.data.network.wifiEnabled + sizeMultiplier: 0.8 Component.onCompleted: { @@ -44,11 +49,11 @@ NIconButton { return "signal_wifi_bad" } } - tooltipText: "WiFi Networks" + tooltipText: "Network / WiFi" onClicked: { try { Logger.log("WiFi", "Button clicked, toggling panel") - wifiPanel.toggle(screen) + PanelService.getPanel("wifiPanel")?.toggle(screen) } catch (error) { Logger.error("WiFi", "Error toggling panel:", error) } diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 220a6b3..0871ce7 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -10,6 +10,10 @@ import qs.Services Item { id: root + + property ShellScreen screen: null + property real scaling: ScalingService.scale(screen) + property bool isDestroying: false property bool hovered: false @@ -23,7 +27,8 @@ Item { signal workspaceChanged(int workspaceId, color accentColor) - width: { + implicitHeight: Math.round(36 * scaling) + implicitWidth: { let total = 0 for (var i = 0; i < localWorkspaces.count; i++) { const ws = localWorkspaces.get(i) @@ -39,34 +44,35 @@ Item { return total } - height: Math.round(36 * scaling) - Component.onCompleted: { - localWorkspaces.clear() - for (var i = 0; i < WorkspaceService.workspaces.count; i++) { - const ws = WorkspaceService.workspaces.get(i) - if (ws.output.toLowerCase() === screen.name.toLowerCase()) { - localWorkspaces.append(ws) - } - } - workspaceRepeater.model = localWorkspaces - updateWorkspaceFocus() + refreshWorkspaces() } + Component.onDestruction: { + root.isDestroying = true + } + + onScreenChanged: refreshWorkspaces() + Connections { target: WorkspaceService function onWorkspacesChanged() { - localWorkspaces.clear() + refreshWorkspaces() + } + } + + function refreshWorkspaces() { + localWorkspaces.clear() + if (screen !== null) { for (var i = 0; i < WorkspaceService.workspaces.count; i++) { const ws = WorkspaceService.workspaces.get(i) if (ws.output.toLowerCase() === screen.name.toLowerCase()) { localWorkspaces.append(ws) } } - - workspaceRepeater.model = localWorkspaces - updateWorkspaceFocus() } + workspaceRepeater.model = localWorkspaces + updateWorkspaceFocus() } function triggerUnifiedWave() { @@ -74,6 +80,17 @@ Item { masterAnimation.restart() } + function updateWorkspaceFocus() { + for (var i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i) + if (ws.isFocused === true) { + root.triggerUnifiedWave() + root.workspaceChanged(ws.id, Color.mPrimary) + break + } + } + } + SequentialAnimation { id: masterAnimation PropertyAction { @@ -101,17 +118,6 @@ Item { } } - function updateWorkspaceFocus() { - for (var i = 0; i < localWorkspaces.count; i++) { - const ws = localWorkspaces.get(i) - if (ws.isFocused === true) { - root.triggerUnifiedWave() - root.workspaceChanged(ws.id, Color.mPrimary) - break - } - } - } - Rectangle { id: workspaceBackground width: parent.width - Style.marginS * scaling * 2 @@ -254,8 +260,4 @@ Item { } } } - - Component.onDestruction: { - root.isDestroying = true - } } diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 762c8ff..ea5b260 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -104,17 +104,6 @@ NPanel { year: Time.date.getFullYear() locale: Qt.locale() // Use system locale - // Optionally, update when the panel becomes visible - Connections { - target: calendarPanel - function onVisibleChanged() { - if (calendarPanel.visible) { - grid.month = Time.date.getMonth() - grid.year = Time.date.getFullYear() - } - } - } - delegate: Rectangle { width: (Style.baseWidgetSize * scaling) height: (Style.baseWidgetSize * scaling) diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index d0476c6..efee796 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -163,7 +163,7 @@ ColumnLayout { spacing: Style.marginM * scaling // Left Section - NWidgetCard { + NSectionEditor { sectionName: "Left" widgetModel: Settings.data.bar.widgets.left availableWidgets: availableWidgets @@ -174,7 +174,7 @@ ColumnLayout { } // Center Section - NWidgetCard { + NSectionEditor { sectionName: "Center" widgetModel: Settings.data.bar.widgets.center availableWidgets: availableWidgets @@ -185,7 +185,7 @@ ColumnLayout { } // Right Section - NWidgetCard { + NSectionEditor { sectionName: "Right" widgetModel: Settings.data.bar.widgets.right availableWidgets: availableWidgets @@ -228,15 +228,6 @@ ColumnLayout { // Assign the new array Settings.data.bar.widgets[section] = newArray - - // Force a settings save - //Logger.log("BarTab", "Settings updated, triggering save...") - - // Verify the change was applied - Qt.setTimeout(function () { - var updatedArray = Settings.data.bar.widgets[section] - //Logger.log("BarTab", "Verification - updated section array:", JSON.stringify(updatedArray)) - }, 100) } else { //Logger.log("BarTab", "Invalid section or index:", section, index, "array length:", @@ -262,29 +253,19 @@ ColumnLayout { } } - // Widget loader for discovering available widgets - WidgetLoader { - id: widgetLoader - } - + // Base list model for all combo boxes ListModel { id: availableWidgets } Component.onCompleted: { - discoverWidgets() - } - - // Automatically discover available widgets using WidgetLoader - function discoverWidgets() { + // Fill out availableWidgets ListModel availableWidgets.clear() - - // Use WidgetLoader to discover available widgets - const discoveredWidgets = widgetLoader.discoverAvailableWidgets() - - // Add discovered widgets to the ListModel - discoveredWidgets.forEach(widget => { - availableWidgets.append(widget) - }) + BarWidgetRegistry.getAvailableWidgets().forEach(entry => { + availableWidgets.append({ + "key": entry, + "name": entry + }) + }) } } diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 8d3b67a..26f3b4e 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -50,6 +50,7 @@ NBox { icon: "image" tooltipText: "Open Wallpaper Selector" onClicked: { + var settingsPanel = PanelService.getPanel("settingsPanel") settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector settingsPanel.open(screen) } diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml new file mode 100644 index 0000000..b6e735c --- /dev/null +++ b/Services/BarWidgetRegistry.qml @@ -0,0 +1,99 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Modules.Bar.Widgets + +Singleton { + id: root + + // Widget registry object mapping widget names to components + property var widgets: ({ + "ActiveWindow": activeWindowComponent, + "ArchUpdater": archUpdaterComponent, + "Battery": batteryComponent, + "Bluetooth": bluetoothComponent, + "Brightness": brightnessComponent, + "Clock": clockComponent, + "KeyboardLayout": keyboardLayoutComponent, + "MediaMini": mediaMiniComponent, + "NotificationHistory": notificationHistoryComponent, + "PowerProfile": powerProfileComponent, + "ScreenRecorderIndicator": screenRecorderIndicatorComponent, + "SidePanelToggle": sidePanelToggleComponent, + "SystemMonitor": systemMonitorComponent, + "Tray": trayComponent, + "Volume": volumeComponent, + "WiFi": wiFiComponent, + "Workspace": workspaceComponent + }) + + // Component definitions - these are loaded once at startup + property Component activeWindowComponent: Component { + ActiveWindow {} + } + property Component archUpdaterComponent: Component { + ArchUpdater {} + } + property Component batteryComponent: Component { + Battery {} + } + property Component bluetoothComponent: Component { + Bluetooth {} + } + property Component brightnessComponent: Component { + Brightness {} + } + property Component clockComponent: Component { + Clock {} + } + property Component keyboardLayoutComponent: Component { + KeyboardLayout {} + } + property Component mediaMiniComponent: Component { + MediaMini {} + } + property Component notificationHistoryComponent: Component { + NotificationHistory {} + } + property Component powerProfileComponent: Component { + PowerProfile {} + } + property Component screenRecorderIndicatorComponent: Component { + ScreenRecorderIndicator {} + } + property Component sidePanelToggleComponent: Component { + SidePanelToggle {} + } + property Component systemMonitorComponent: Component { + SystemMonitor {} + } + property Component trayComponent: Component { + Tray {} + } + property Component volumeComponent: Component { + Volume {} + } + property Component wiFiComponent: Component { + WiFi {} + } + property Component workspaceComponent: Component { + Workspace {} + } + + // ------------------------------ + // Helper function to get widget component by name + function getWidget(name) { + return widgets[name] || null + } + + // Helper function to check if widget exists + function hasWidget(name) { + return name in widgets + } + + // Get list of available widget names + function getAvailableWidgets() { + return Object.keys(widgets) + } +} diff --git a/Services/CavaService.qml b/Services/CavaService.qml index cb7b603..6cfb735 100644 --- a/Services/CavaService.qml +++ b/Services/CavaService.qml @@ -38,7 +38,7 @@ Singleton { id: process stdinEnabled: true running: (Settings.data.audio.visualizerType !== "none") - && (PanelService.sidePanel.active || Settings.data.audio.showMiniplayerCava + && (PanelService.getPanel("sidePanel").active || Settings.data.audio.showMiniplayerCava || (PanelService.lockScreen && PanelService.lockScreen.active)) command: ["cava", "-p", "/dev/stdin"] onExited: { diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index a1ec62b..feb52bc 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -19,7 +19,7 @@ Singleton { property ListModel workspaces: ListModel {} property var windows: [] property int focusedWindowIndex: -1 - property string focusedWindowTitle: "(No active window)" + property string focusedWindowTitle: "n/a" property bool inOverview: false // Generic events diff --git a/Services/GitHubService.qml b/Services/GitHubService.qml index 98bf6e4..cb75563 100644 --- a/Services/GitHubService.qml +++ b/Services/GitHubService.qml @@ -1,9 +1,10 @@ +pragma Singleton + import QtQuick import Quickshell import Quickshell.Io import qs.Commons import qs.Services -pragma Singleton // GitHub API logic and caching Singleton { diff --git a/Services/LocationService.qml b/Services/LocationService.qml index c758856..2e29e88 100644 --- a/Services/LocationService.qml +++ b/Services/LocationService.qml @@ -1,9 +1,10 @@ +pragma Singleton + import QtQuick import Quickshell import Quickshell.Io import qs.Commons import qs.Services -pragma Singleton // Weather logic and caching Singleton { diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 3f4f44e..0354df5 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -1,10 +1,11 @@ +pragma Singleton + import QtQuick import Quickshell import Quickshell.Io import qs.Commons import qs.Services import Quickshell.Services.Notifications -pragma Singleton QtObject { id: root diff --git a/Services/PanelService.qml b/Services/PanelService.qml index 435a45a..9a37aeb 100644 --- a/Services/PanelService.qml +++ b/Services/PanelService.qml @@ -1,23 +1,38 @@ pragma Singleton import Quickshell +import qs.Commons Singleton { id: root - // A ref. to the sidePanel, so it's accessible from other services - property var sidePanel: null - - // A ref. to the lockScreen, so it's accessible from other services + // A ref. to the lockScreen, so it's accessible from anywhere + // This is not a panel... property var lockScreen: null - // A ref. to the updatePanel, so it's accessible from other services - property var updatePanel: null - // Currently opened panel property var openedPanel: null - function registerOpen(panel) { + property var registeredPanels: ({}) + + // Register this panel + function registerPanel(panel) { + registeredPanels[panel.objectName] = panel + Logger.log("PanelService", "Registered:", panel.objectName) + } + + // Returns a panel + function getPanel(name) { + return registeredPanels[name] || null + } + + // Check if a panel exists + function hasPanel(name) { + return name in registeredPanels + } + + // Helper to keep only one panel open at any time + function willOpenPanel(panel) { if (openedPanel && openedPanel != panel) { openedPanel.close() } diff --git a/Services/ScreenRecorderService.qml b/Services/ScreenRecorderService.qml index 5837314..ed35b38 100644 --- a/Services/ScreenRecorderService.qml +++ b/Services/ScreenRecorderService.qml @@ -46,7 +46,6 @@ Singleton { //Logger.log("ScreenRecorder", command) Quickshell.execDetached(["sh", "-c", command]) Logger.log("ScreenRecorder", "Started recording") - //Logger.log("ScreenRecorder", command) } // Stop recording using Quickshell.execDetached diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 97a8705..dc8e162 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -31,6 +31,12 @@ Loader { signal opened signal closed + Component.onCompleted: { + // console.log("Oh Yeah") + // console.log(objectName) + PanelService.registerPanel(root) + } + // ----------------------------------------- function toggle(aScreen) { if (!active || isClosing) { @@ -53,7 +59,7 @@ Loader { opacityValue = 1.0 } - PanelService.registerOpen(root) + PanelService.willOpenPanel(root) active = true root.opened() diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index b3739eb..8f6c3d4 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -16,10 +16,11 @@ Item { property color collapsedIconColor: Color.mOnSurface property real sizeMultiplier: 0.8 property bool autoHide: false - // When true, keep the pill expanded regardless of hover state - property bool forceShown: false + property bool forceOpen: false + property bool disableOpen: false + // Effective shown state (true if hovered/animated open or forced) - readonly property bool effectiveShown: forceShown || showPill + readonly property bool effectiveShown: forceOpen || showPill signal shown signal hidden @@ -85,7 +86,7 @@ Item { height: iconSize radius: width * 0.5 // When forced shown, match pill background; otherwise use accent when hovered - color: forceShown ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant) + color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant) anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right @@ -100,7 +101,7 @@ Item { text: root.icon font.pointSize: Style.fontSizeM * scaling // When forced shown, use pill text color; otherwise accent color when hovered - color: forceShown ? textColor : (showPill ? iconTextColor : Color.mOnSurface) + color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface) anchors.centerIn: parent } } @@ -194,18 +195,21 @@ Item { anchors.fill: parent hoverEnabled: true onEntered: { - if (!forceShown) { + root.entered() + tooltip.show() + if (disableOpen) { + return + } + if (!forceOpen) { showDelayed() } - tooltip.show() - root.entered() } onExited: { - if (!forceShown) { + root.exited() + if (!forceOpen) { hide() } tooltip.hide() - root.exited() } onClicked: { root.clicked() @@ -226,7 +230,7 @@ Item { } function hide() { - if (forceShown) { + if (forceOpen) { return } if (showPill) { @@ -245,8 +249,8 @@ Item { } } - onForceShownChanged: { - if (forceShown) { + onForceOpenChanged: { + if (forceOpen) { // Immediately lock open without animations showAnim.stop() hideAnim.stop() diff --git a/Widgets/NWidgetCard.qml b/Widgets/NSectionEditor.qml similarity index 83% rename from Widgets/NWidgetCard.qml rename to Widgets/NSectionEditor.qml index abf53c7..9103dcd 100644 --- a/Widgets/NWidgetCard.qml +++ b/Widgets/NSectionEditor.qml @@ -90,6 +90,7 @@ NBox { colorFgHover: Color.mOnSecondary enabled: comboBox.selectedKey !== "" Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Style.marginS * scaling onClicked: { if (comboBox.currentKey !== "") { addWidget(comboBox.currentKey, sectionName.toLowerCase()) @@ -174,27 +175,27 @@ NBox { anchors.fill: parent drag.target: parent - onPressed: { - // Check if the click is on the close button area - const closeButtonX = widgetContent.x + widgetContent.width - 20 * scaling - const closeButtonY = widgetContent.y - const closeButtonWidth = 20 * scaling - const closeButtonHeight = 20 * scaling + onPressed: mouse => { + // Check if the click is on the close button area + const closeButtonX = widgetContent.x + widgetContent.width - 20 * scaling + const closeButtonY = widgetContent.y + const closeButtonWidth = 20 * scaling + const closeButtonHeight = 20 * scaling - if (mouseX >= closeButtonX && mouseX <= closeButtonX + closeButtonWidth && mouseY >= closeButtonY - && mouseY <= closeButtonY + closeButtonHeight) { - // Click is on the close button, don't start drag - mouse.accepted = false - return - } + if (mouseX >= closeButtonX && mouseX <= closeButtonX + closeButtonWidth + && mouseY >= closeButtonY && mouseY <= closeButtonY + closeButtonHeight) { + // Click is on the close button, don't start drag + mouse.accepted = false + return + } - Logger.log("NWidgetCard", `Started dragging widget: ${modelData} at index ${index}`) - // Bring to front when starting drag - widgetItem.z = 1000 - } + Logger.log("NSectionEditor", `Started dragging widget: ${modelData} at index ${index}`) + // Bring to front when starting drag + widgetItem.z = 1000 + } onReleased: { - Logger.log("NWidgetCard", `Released widget: ${modelData} at index ${index}`) + Logger.log("NSectionEditor", `Released widget: ${modelData} at index ${index}`) // Reset z-index when drag ends widgetItem.z = 0 @@ -232,12 +233,12 @@ NBox { const fromIndex = index const toIndex = targetIndex Logger.log( - "NWidgetCard", + "NSectionEditor", `Dropped widget from index ${fromIndex} to position ${toIndex} (distance: ${minDistance.toFixed( 2)})`) reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex) } else { - Logger.log("NWidgetCard", `No valid drop target found for widget at index ${index}`) + Logger.log("NSectionEditor", `No valid drop target found for widget at index ${index}`) } } } @@ -264,16 +265,16 @@ NBox { } onEntered: function (drag) { - Logger.log("NWidgetCard", "Entered start drop zone") + Logger.log("NSectionEditor", "Entered start drop zone") } onDropped: function (drop) { - Logger.log("NWidgetCard", "Dropped on start zone") + Logger.log("NSectionEditor", "Dropped on start zone") if (drop.source && drop.source.widgetIndex !== undefined) { const fromIndex = drop.source.widgetIndex const toIndex = 0 // Insert at the beginning if (fromIndex !== toIndex) { - Logger.log("NWidgetCard", `Dropped widget from index ${fromIndex} to beginning`) + Logger.log("NSectionEditor", `Dropped widget from index ${fromIndex} to beginning`) reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex) } } @@ -299,16 +300,16 @@ NBox { } onEntered: function (drag) { - Logger.log("NWidgetCard", "Entered end drop zone") + Logger.log("NSectionEditor", "Entered end drop zone") } onDropped: function (drop) { - Logger.log("NWidgetCard", "Dropped on end zone") + Logger.log("NSectionEditor", "Dropped on end zone") if (drop.source && drop.source.widgetIndex !== undefined) { const fromIndex = drop.source.widgetIndex const toIndex = widgetModel.length // Insert at the end if (fromIndex !== toIndex) { - Logger.log("NWidgetCard", `Dropped widget from index ${fromIndex} to end`) + Logger.log("NSectionEditor", `Dropped widget from index ${fromIndex} to end`) reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex) } } diff --git a/Widgets/NWidgetLoader.qml b/Widgets/NWidgetLoader.qml new file mode 100644 index 0000000..600b5e7 --- /dev/null +++ b/Widgets/NWidgetLoader.qml @@ -0,0 +1,46 @@ +import QtQuick +import Quickshell +import qs.Services + +Item { + id: root + + property string widgetName: "" + property var widgetProps: ({}) + property bool enabled: true + + // Don't reserve space unless the loaded widget is really visible + implicitWidth: loader.item ? loader.item.visible ? loader.item.implicitWidth : 0 : 0 + implicitHeight: loader.item ? loader.item.visible ? loader.item.implicitHeight : 0 : 0 + + Loader { + id: loader + + anchors.fill: parent + active: enabled && widgetName !== "" + sourceComponent: { + if (!active) { + return null + } + return BarWidgetRegistry.getWidget(widgetName) + } + + onLoaded: { + if (item && widgetProps) { + // Apply properties to loaded widget + for (var prop in widgetProps) { + if (item.hasOwnProperty(prop)) { + item[prop] = widgetProps[prop] + } + } + } + } + } + + // Error handling + onWidgetNameChanged: { + if (widgetName && !BarWidgetRegistry.hasWidget(widgetName)) { + Logger.warn("WidgetLoader", "Widget not found in registry:", widgetName) + } + } +} diff --git a/shell.qml b/shell.qml index 6eafab7..370b788 100644 --- a/shell.qml +++ b/shell.qml @@ -40,64 +40,69 @@ ShellRoot { Bar {} Dock {} - Launcher { - id: launcherPanel - } - - SidePanel { - id: sidePanel - } - - Calendar { - id: calendarPanel - } - - SettingsPanel { - id: settingsPanel - } - Notification { id: notification } - NotificationHistoryPanel { - id: notificationHistoryPanel - } - LockScreen { id: lockScreen } - PowerPanel { - id: powerPanel - } - - WiFiPanel { - id: wifiPanel - } - - BluetoothPanel { - id: bluetoothPanel - } - - ArchUpdaterPanel { - id: updatePanel - } - ToastManager {} IPCManager {} + // ------------------------------ + // All the panels + Launcher { + id: launcherPanel + objectName: "launcherPanel" + } + + SidePanel { + id: sidePanel + objectName: "sidePanel" + } + + Calendar { + id: calendarPanel + objectName: "calendarPanel" + } + + SettingsPanel { + id: settingsPanel + objectName: "settingsPanel" + } + + NotificationHistoryPanel { + id: notificationHistoryPanel + objectName: "notificationHistoryPanel" + } + + PowerPanel { + id: powerPanel + objectName: "powerPanel" + } + + WiFiPanel { + id: wifiPanel + objectName: "wifiPanel" + } + + BluetoothPanel { + id: bluetoothPanel + objectName: "bluetoothPanel" + } + + ArchUpdaterPanel { + id: archUpdaterPanel + objectName: "archUpdaterPanel" + } + Component.onCompleted: { - // Save a ref. to our sidePanel so we can access it from services - PanelService.sidePanel = sidePanel - - // Save a ref. to our lockScreen so we can access it from services + // Save a ref. to our lockScreen so we can access it easily PanelService.lockScreen = lockScreen - // Save a ref. to our updatePanel so we can access it from services - PanelService.updatePanel = updatePanel - // Ensure our singleton is created as soon as possible so we start fetching weather asap LocationService.init() }