From 57fee687938af9a1dd51a7c30a0f043bf24efcbb Mon Sep 17 00:00:00 2001 From: quadbyte Date: Wed, 20 Aug 2025 08:45:48 -0400 Subject: [PATCH] NPanel refactor - 1st pass: SidePanel and settings an new logic --- Commons/Settings.qml | 9 +- Modules/Bar/SidePanelToggle.qml | 37 +- Modules/Bar/Tray.qml | 1 - Modules/Bar/Volume.qml | 2 +- Modules/SettingsPanel/SettingsPanel.qml | 550 ++++++++---------- Modules/SidePanel/Cards/PowerProfilesCard.qml | 3 +- Modules/SidePanel/Cards/ProfileCard.qml | 7 +- Modules/SidePanel/Cards/UtilitiesCard.qml | 7 +- Modules/SidePanel/SidePanel.qml | 242 ++------ Services/CavaService.qml | 2 +- Services/PanelService.qml | 10 +- Services/ScalingService.qml | 10 +- Widgets/NLoader.qml | 5 +- Widgets/NPanel.qml | 231 +++++--- 14 files changed, 500 insertions(+), 616 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 6fb29dd..1f5d45c 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -30,9 +30,6 @@ Singleton { // Flag to prevent unnecessary wallpaper calls during reloads property bool isInitialLoad: true - // Needed to only have one NPanel loaded at a time. <--- VERY BROKEN - //property var openPanel: null - // Function to validate monitor configurations function validateMonitorConfigurations() { var availableScreenNames = [] @@ -86,14 +83,14 @@ Singleton { if (isInitialLoad) { Logger.log("Settings", "OnLoaded") // Only set wallpaper on initial load, not on reloads - if (adapter.wallpaper.current !== "") { + if (adapter.wallpaper.current !== "") { Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current) WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true) } // Validate monitor configurations, only once // if none of the configured monitors exist, clear the lists - validateMonitorConfigurations() + validateMonitorConfigurations() } isInitialLoad = false @@ -128,7 +125,7 @@ Singleton { general: JsonObject { property string avatarImage: defaultAvatar - property bool dimDesktop: true + property bool dimDesktop: false property bool showScreenCorners: false property real radiusRatio: 1.0 } diff --git a/Modules/Bar/SidePanelToggle.qml b/Modules/Bar/SidePanelToggle.qml index 4fcb347..542c5b2 100644 --- a/Modules/Bar/SidePanelToggle.qml +++ b/Modules/Bar/SidePanelToggle.qml @@ -15,22 +15,25 @@ NIconButton { anchors.verticalCenter: parent.verticalCenter onClicked: { - // Map this button's center to the screen and open the side panel below it - const localCenterX = width / 2 - const localCenterY = height / 2 - const globalPoint = mapToItem(null, localCenterX, localCenterY) - if (sidePanel.isLoaded) { - // Call hide() instead of directly setting isLoaded to false - if (sidePanel.item && sidePanel.item.hide) { - sidePanel.item.hide() - } else { - sidePanel.isLoaded = false - } - } else if (sidePanel.openAt) { - sidePanel.openAt(globalPoint.x, screen) - } else { - // Fallback: toggle if API unavailable - sidePanel.isLoaded = true - } + sidePanel.toggle(screen) + // sidePanel.isLoaded = !sidePanel.isLoaded + // Logger.log("SidePanelToggle", sidePanel.isLoaded) + // // Map this button's center to the screen and open the side panel below it + // const localCenterX = width / 2 + // const localCenterY = height / 2 + // const globalPoint = mapToItem(null, localCenterX, localCenterY) + // if (sidePanel.isLoaded) { + // // Call hide() instead of directly setting isLoaded to false + // if (sidePanel.item && sidePanel.item.hide) { + // sidePanel.item.hide() + // } else { + // sidePanel.isLoaded = false + // } + // } else if (sidePanel.openAt) { + // sidePanel.openAt(globalPoint.x, screen) + // } else { + // // Fallback: toggle if API unavailable + // sidePanel.isLoaded = true + // } } } diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index 6718dd8..85f79c3 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -129,7 +129,6 @@ Rectangle { // Wrapped in NPanel so we can detect click outside of the menu to close the TrayMenu NPanel { id: trayPanel - showOverlay: false // no colors overlay even if activated in settings // Override hide function to animate first function hide() { diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Volume.qml index 8eb6196..36dbbb1 100644 --- a/Modules/Bar/Volume.qml +++ b/Modules/Bar/Volume.qml @@ -65,7 +65,7 @@ Item { } onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.AudioService - settingsPanel.isLoaded = true + settingsPanel.open(screen) } } } diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 7012838..b4b5ff4 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -8,9 +8,13 @@ import qs.Commons import qs.Services import qs.Widgets -NLoader { +NPanel { id: root + rWidth: Math.max(screen?.width * 0.5, 1280) * scaling + rHeight: Math.max(screen?.height * 0.5, 720) * scaling + rAnchorCentered: true + // Tabs enumeration, order is NOT relevant enum Tab { About, @@ -28,344 +32,264 @@ NLoader { } property int requestedTab: SettingsPanel.Tab.General + property int currentTabIndex: 0 - content: Component { - NPanel { - id: panel + Component { + id: generalTab + Tabs.GeneralTab {} + } + Component { + id: barTab + Tabs.BarTab {} + } + Component { + id: audioTab + Tabs.AudioTab {} + } + Component { + id: brightnessTab + Tabs.BrightnessTab {} + } + Component { + id: displayTab + Tabs.DisplayTab {} + } + Component { + id: networkTab + Tabs.NetworkTab {} + } + Component { + id: timeWeatherTab + Tabs.TimeWeatherTab {} + } + Component { + id: colorSchemeTab + Tabs.ColorSchemeTab {} + } + Component { + id: wallpaperTab + Tabs.WallpaperTab {} + } + Component { + id: wallpaperSelectorTab + Tabs.WallpaperSelectorTab {} + } + Component { + id: screenRecorderTab + Tabs.ScreenRecorderTab {} + } + Component { + id: aboutTab + Tabs.AboutTab {} + } - property int currentTabIndex: 0 + // Order *DOES* matter + property var tabsModel: [{ + "id": SettingsPanel.Tab.General, + "label": "General", + "icon": "tune", + "source": generalTab + }, { + "id": SettingsPanel.Tab.Bar, + "label": "Bar", + "icon": "web_asset", + "source": barTab + }, { + "id": SettingsPanel.Tab.AudioService, + "label": "Audio", + "icon": "volume_up", + "source": audioTab + }, { + "id": SettingsPanel.Tab.Display, + "label": "Display", + "icon": "monitor", + "source": displayTab + }, { + "id": SettingsPanel.Tab.Network, + "label": "Network", + "icon": "lan", + "source": networkTab + }, { + "id": SettingsPanel.Tab.Brightness, + "label": "Brightness", + "icon": "brightness_6", + "source": brightnessTab + }, { + "id": SettingsPanel.Tab.TimeWeather, + "label": "Time & Weather", + "icon": "schedule", + "source": timeWeatherTab + }, { + "id": SettingsPanel.Tab.ColorScheme, + "label": "Color Scheme", + "icon": "palette", + "source": colorSchemeTab + }, { + "id": SettingsPanel.Tab.Wallpaper, + "label": "Wallpaper", + "icon": "image", + "source": wallpaperTab + }, { + "id": SettingsPanel.Tab.WallpaperSelector, + "label": "Wallpaper Selector", + "icon": "wallpaper_slideshow", + "source": wallpaperSelectorTab + }, { + "id": SettingsPanel.Tab.ScreenRecorder, + "label": "Screen Recorder", + "icon": "videocam", + "source": screenRecorderTab + }, { + "id": SettingsPanel.Tab.About, + "label": "About", + "icon": "info", + "source": aboutTab + }] - // Override hide function to animate first - function hide() { - // Start hide animation - bgRect.scaleValue = 0.8 - bgRect.opacityValue = 0.0 - // Hide after animation completes - hideTimer.start() - } - - // Connect to NPanel's dismissed signal to handle external close events - Connections { - target: panel - function onDismissed() { - hide() + // When the panel opens, choose the appropriate tab + onOpened: { + var initialIndex = SettingsPanel.Tab.General + if (root.requestedTab !== null) { + for (var i = 0; i < root.tabsModel.length; i++) { + if (root.tabsModel[i].id === root.requestedTab) { + initialIndex = i + break } } + } + // Now that the UI is settled, set the current tab index. + root.currentTabIndex = initialIndex + } - // Timer to hide panel after animation - Timer { - id: hideTimer - interval: Style.animationSlow - repeat: false - onTriggered: { - panel.visible = false - panel.dismissed() - } - } + panelContent: Rectangle { + anchors.fill: parent + anchors.margins: Style.marginL * scaling + color: Color.transparent - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - Component { - id: generalTab - Tabs.GeneralTab {} - } - Component { - id: barTab - Tabs.BarTab {} - } - Component { - id: audioTab - Tabs.AudioTab {} - } - Component { - id: brightnessTab - Tabs.BrightnessTab {} - } - Component { - id: displayTab - Tabs.DisplayTab {} - } - Component { - id: networkTab - Tabs.NetworkTab {} - } - Component { - id: timeWeatherTab - Tabs.TimeWeatherTab {} - } - Component { - id: colorSchemeTab - Tabs.ColorSchemeTab {} - } - Component { - id: wallpaperTab - Tabs.WallpaperTab {} - } - Component { - id: wallpaperSelectorTab - Tabs.WallpaperSelectorTab {} - } - Component { - id: screenRecorderTab - Tabs.ScreenRecorderTab {} - } - Component { - id: aboutTab - Tabs.AboutTab {} - } - - // Order *DOES* matter - property var tabsModel: [{ - "id": SettingsPanel.Tab.General, - "label": "General", - "icon": "tune", - "source": generalTab - }, { - "id": SettingsPanel.Tab.Bar, - "label": "Bar", - "icon": "web_asset", - "source": barTab - }, { - "id": SettingsPanel.Tab.AudioService, - "label": "Audio", - "icon": "volume_up", - "source": audioTab - }, { - "id": SettingsPanel.Tab.Display, - "label": "Display", - "icon": "monitor", - "source": displayTab - }, { - "id": SettingsPanel.Tab.Network, - "label": "Network", - "icon": "lan", - "source": networkTab - }, { - "id": SettingsPanel.Tab.Brightness, - "label": "Brightness", - "icon": "brightness_6", - "source": brightnessTab - }, { - "id": SettingsPanel.Tab.TimeWeather, - "label": "Time & Weather", - "icon": "schedule", - "source": timeWeatherTab - }, { - "id": SettingsPanel.Tab.ColorScheme, - "label": "Color Scheme", - "icon": "palette", - "source": colorSchemeTab - }, { - "id": SettingsPanel.Tab.Wallpaper, - "label": "Wallpaper", - "icon": "image", - "source": wallpaperTab - }, { - "id": SettingsPanel.Tab.WallpaperSelector, - "label": "Wallpaper Selector", - "icon": "wallpaper_slideshow", - "source": wallpaperSelectorTab - }, { - "id": SettingsPanel.Tab.ScreenRecorder, - "label": "Screen Recorder", - "icon": "videocam", - "source": screenRecorderTab - }, { - "id": SettingsPanel.Tab.About, - "label": "About", - "icon": "info", - "source": aboutTab - }] - - Component.onCompleted: { - var initialIndex = 0 - if (root.requestedTab !== null) { - for (var i = 0; i < panel.tabsModel.length; i++) { - if (panel.tabsModel[i].id === root.requestedTab) { - initialIndex = i - break - } - } - } - // Now that the UI is settled, set the current tab index. - panel.currentTabIndex = initialIndex - show() - } - - onVisibleChanged: { - if (!visible && (bgRect.opacityValue > 0)) { - hide() - } - } + RowLayout { + anchors.fill: parent + spacing: Style.marginM * scaling Rectangle { - id: bgRect - color: Color.mSurface - radius: Style.radiusL * scaling + id: sidebar + Layout.preferredWidth: Style.sliderWidth * 1.3 * scaling + Layout.fillHeight: true + color: Color.mSurfaceVariant border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) - layer.enabled: true - width: Math.max(screen.width * 0.5, 1280) * scaling - height: Math.max(screen.height * 0.5, 720) * scaling - anchors.centerIn: parent + radius: Style.radiusM * scaling - // Animation properties - property real scaleValue: 0.8 - property real opacityValue: 0.0 - - scale: scaleValue - opacity: opacityValue - - // Animate in when component is completed - Component.onCompleted: { - scaleValue = 1.0 - opacityValue = 1.0 - } - - MouseArea { + Column { anchors.fill: parent - } + anchors.margins: Style.marginS * scaling + spacing: Style.marginXS * 1.5 * scaling - Behavior on scale { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.OutExpo - } - } - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad - } - } - - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginL * scaling - - Rectangle { - id: sidebar - Layout.preferredWidth: Style.sliderWidth * 1.3 * scaling - Layout.fillHeight: true - color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - radius: Style.radiusM * scaling - - Column { - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginXS * 1.5 * scaling - - Repeater { - id: sections - model: panel.tabsModel - delegate: Rectangle { - id: tabItem - width: parent.width - height: 32 * scaling - radius: Style.radiusS * scaling - color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent) - readonly property bool selected: index === currentTabIndex - property bool hovering: false - property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface) - RowLayout { - anchors.fill: parent - anchors.leftMargin: Style.marginS * scaling - anchors.rightMargin: Style.marginS * scaling - spacing: Style.marginS * scaling - // Tab icon on the left side - NIcon { - text: modelData.icon - color: tabTextColor - font.pointSize: Style.fontSizeL * scaling - } - // Tab label on the left side - NText { - text: modelData.label - color: tabTextColor - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - Layout.fillWidth: true - } - } - MouseArea { - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton - onEntered: tabItem.hovering = true - onExited: tabItem.hovering = false - onCanceled: tabItem.hovering = false - onClicked: currentTabIndex = index - } + Repeater { + id: sections + model: root.tabsModel + delegate: Rectangle { + id: tabItem + width: parent.width + height: 32 * scaling + radius: Style.radiusS * scaling + color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent) + readonly property bool selected: index === currentTabIndex + property bool hovering: false + property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface) + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginS * scaling + anchors.rightMargin: Style.marginS * scaling + spacing: Style.marginS * scaling + // Tab icon on the left side + NIcon { + text: modelData.icon + color: tabTextColor + font.pointSize: Style.fontSizeL * scaling } + // Tab label on the left side + NText { + text: modelData.label + color: tabTextColor + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + Layout.fillWidth: true + } + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton + onEntered: tabItem.hovering = true + onExited: tabItem.hovering = false + onCanceled: tabItem.hovering = false + onClicked: currentTabIndex = index } } } + } + } - // Content - Rectangle { - id: contentPane + // Content + Rectangle { + id: contentPane + Layout.fillWidth: true + Layout.fillHeight: true + radius: Style.radiusM * scaling + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + clip: true + + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginS * scaling + + RowLayout { + id: headerRow + Layout.fillWidth: true + spacing: Style.marginS * scaling + + // Tab label on the main right side + NText { + text: root.tabsModel[currentTabIndex].label + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + NIconButton { + icon: "close" + tooltipText: "Close" + Layout.alignment: Qt.AlignVCenter + onClicked: root.close() + } + } + + NDivider { + Layout.fillWidth: true + } + + Item { Layout.fillWidth: true Layout.fillHeight: true - radius: Style.radiusM * scaling - color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) clip: true - ColumnLayout { - id: contentLayout - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginS * scaling + Repeater { + model: root.tabsModel - RowLayout { - id: headerRow - Layout.fillWidth: true - spacing: Style.marginS * scaling - - // Tab label on the main right side - NText { - text: panel.tabsModel[currentTabIndex].label - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - Layout.fillWidth: true - } - NIconButton { - icon: "close" - tooltipText: "Close" - Layout.alignment: Qt.AlignVCenter - onClicked: panel.hide() - } + onItemAdded: function (index, item) { + item.sourceComponent = root.tabsModel[index].source } - NDivider { - Layout.fillWidth: true - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - - Repeater { - model: panel.tabsModel - - onItemAdded: function (index, item) { - item.sourceComponent = panel.tabsModel[index].source - } - - delegate: Loader { - // All loaders will occupy the same space, stacked on top of each other. - anchors.fill: parent - visible: index === panel.currentTabIndex - // The loader is only active (and uses memory) when its page is visible. - active: visible - } - } + delegate: Loader { + // All loaders will occupy the same space, stacked on top of each other. + anchors.fill: parent + visible: index === root.currentTabIndex + // The loader is only active (and uses memory) when its page is visible. + active: visible } } } diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml index 4c574b0..2c36642 100644 --- a/Modules/SidePanel/Cards/PowerProfilesCard.qml +++ b/Modules/SidePanel/Cards/PowerProfilesCard.qml @@ -16,12 +16,13 @@ NBox { // PowerProfiles service property var powerProfiles: PowerProfiles readonly property bool hasPP: powerProfiles.hasPerformanceProfile + property real spacing: 0 RowLayout { id: powerRow anchors.fill: parent anchors.margins: Style.marginS * scaling - spacing: sidePanel.cardSpacing + spacing: spacing Item { Layout.fillWidth: true } diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 1b39336..803565a 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -61,7 +61,7 @@ NBox { tooltipText: "Open Settings" onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.General - settingsPanel.isLoaded = !settingsPanel.isLoaded + settingsPanel.open(screen) } } @@ -78,8 +78,9 @@ NBox { PowerMenu { id: powerMenu - anchors.top: powerButton.bottom - anchors.right: powerButton.right + // TBC + // anchors.top: powerButton.bottom + // anchors.right: powerButton.right } // ---------------------------------- diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 9bc0739..2037ebb 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -9,6 +9,9 @@ import qs.Widgets // Utilities: record & wallpaper NBox { + + property real spacing: 0 + Layout.fillWidth: true Layout.preferredWidth: 1 implicitHeight: utilRow.implicitHeight + Style.marginM * 2 * scaling @@ -16,7 +19,7 @@ NBox { id: utilRow anchors.fill: parent anchors.margins: Style.marginS * scaling - spacing: sidePanel.cardSpacing + spacing: spacing Item { Layout.fillWidth: true } @@ -37,7 +40,7 @@ NBox { tooltipText: "Open Wallpaper Selector" onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector - settingsPanel.isLoaded = true + settingsPanel.open(screen) } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 04bfa3f..58b3065 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -7,205 +7,79 @@ import qs.Commons import qs.Services import qs.Widgets -NLoader { - id: root +NPanel { + id: panel - // X coordinate on screen (in pixels) where the panel should align its center. - // Set via openAt(x) from the bar button. - property real anchorX: 0 - // Target screen to open on - property var targetScreen: null + rWidth: 460 * scaling + rHeight: 700 * scaling + rAnchorRight: true - function openAt(x, screen) { - anchorX = x - targetScreen = screen - isLoaded = true - // If the panel is already instantiated, update immediately - if (item) { - if (item.anchorX !== undefined) - item.anchorX = anchorX - if (item.screen !== undefined) - item.screen = targetScreen - } - } + // rectX: Math.max(Style.marginS * scaling, Math.min(parent.width - width - Style.marginS * scaling, + // Math.round(anchorX - width / 2))) + // rectY: Settings.data.bar.position === "top" ? Style.marginS * scaling : undefined + panelContent: Item { + id: content - content: Component { - NPanel { - id: sidePanel + property real cardSpacing: Style.marginL * scaling - // Single source of truth for spacing between cards (both axes) - property real cardSpacing: Style.marginL * scaling - // X coordinate from the bar to align this panel under - property real anchorX: root.anchorX - // Ensure this panel attaches to the intended screen - screen: root.targetScreen + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: content.cardSpacing + implicitHeight: layout.implicitHeight - // Override hide function to animate first - function hide() { - // Start hide animation - panelBackground.scaleValue = 0.8 - panelBackground.opacityValue = 0.0 + // Layout content (not vertically anchored so implicitHeight is valid) + ColumnLayout { + id: layout + // Use the same spacing value horizontally and vertically + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: content.cardSpacing - // Hide after animation completes - hideTimer.start() + // Cards (consistent inter-card spacing via ColumnLayout spacing) + ProfileCard {// Layout.topMargin: 0 + // Layout.bottomMargin: 0 + } + WeatherCard {// Layout.topMargin: 0 + // Layout.bottomMargin: 0 } - // Connect to NPanel's dismissed signal to handle external close events - Connections { - target: sidePanel - function onDismissed() { - // Start hide animation - panelBackground.scaleValue = 0.8 - panelBackground.opacityValue = 0.0 + // Middle section: media + stats column + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 0 + Layout.bottomMargin: 0 + spacing: content.cardSpacing - // Hide after animation completes - hideTimer.start() + // Media card + MediaCard { + id: mediaCard + Layout.fillWidth: true + implicitHeight: statsCard.implicitHeight + } + + // System monitors combined in one card + SystemMonitorCard { + id: statsCard } } - // Also handle visibility changes from external sources - onVisibleChanged: { - if (!visible && panelBackground.opacityValue > 0) { - // Start hide animation - panelBackground.scaleValue = 0.8 - panelBackground.opacityValue = 0.0 + // Bottom actions (two grouped rows of round buttons) + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 0 + Layout.bottomMargin: 0 + spacing: content.cardSpacing - // Hide after animation completes - hideTimer.start() - } - } - - // Ensure panel shows itself once created - Component.onCompleted: show() - - // Inline helpers moved to dedicated widgets: NCard and NCircleStat - Rectangle { - id: panelBackground - color: Color.mSurface - radius: Style.radiusL * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - layer.enabled: true - width: 460 * scaling - property real innerMargin: sidePanel.cardSpacing - // Height scales to content plus vertical padding - height: content.implicitHeight + innerMargin * 2 - // Place the panel relative to the bar based on its position - y: Settings.data.bar.position === "top" ? Style.marginS * scaling : undefined - anchors { - bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined - bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling + Style.marginS * scaling : undefined - } - // Center horizontally under the anchorX, clamped to the screen bounds - x: Math.max(Style.marginS * scaling, Math.min(parent.width - width - Style.marginS * scaling, - Math.round(anchorX - width / 2))) - - // Animation properties - property real scaleValue: 0.8 - property real opacityValue: 0.0 - - scale: scaleValue - opacity: opacityValue - - // Animate in when component is completed - Component.onCompleted: { - scaleValue = 1.0 - opacityValue = 1.0 + // Power Profiles switcher + PowerProfilesCard { + spacing: content.cardSpacing } - // Timer to hide panel after animation - Timer { - id: hideTimer - interval: Style.animationSlow - repeat: false - onTriggered: { - sidePanel.visible = false - sidePanel.dismissed() - } - } - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - // Animation behaviors - Behavior on scale { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.OutExpo - } - } - - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutQuad - } - } - - // Content wrapper to ensure childrenRect drives implicit height - Item { - id: content - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: panelBackground.innerMargin - implicitHeight: layout.implicitHeight - - // Layout content (not vertically anchored so implicitHeight is valid) - ColumnLayout { - id: layout - // Use the same spacing value horizontally and vertically - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: sidePanel.cardSpacing - - // Cards (consistent inter-card spacing via ColumnLayout spacing) - ProfileCard { - Layout.topMargin: 0 - Layout.bottomMargin: 0 - } - WeatherCard { - Layout.topMargin: 0 - Layout.bottomMargin: 0 - } - - // Middle section: media + stats column - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 0 - Layout.bottomMargin: 0 - spacing: sidePanel.cardSpacing - - // Media card - MediaCard { - id: mediaCard - Layout.fillWidth: true - implicitHeight: statsCard.implicitHeight - } - - // System monitors combined in one card - SystemMonitorCard { - id: statsCard - } - } - - // Bottom actions (two grouped rows of round buttons) - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 0 - Layout.bottomMargin: 0 - spacing: sidePanel.cardSpacing - - // Power Profiles switcher - PowerProfilesCard {} - - // Utilities buttons - UtilitiesCard {} - } - } + // Utilities buttons + UtilitiesCard { + spacing: content.cardSpacing } } } diff --git a/Services/CavaService.qml b/Services/CavaService.qml index b58e09a..dae2f61 100644 --- a/Services/CavaService.qml +++ b/Services/CavaService.qml @@ -37,7 +37,7 @@ Singleton { Process { id: process stdinEnabled: true - running: (Settings.data.audio.visualizerType !== "none") && PanelService.sidePanel.isLoaded + running: (Settings.data.audio.visualizerType !== "none") && PanelService.sidePanel.active command: ["cava", "-p", "/dev/stdin"] onExited: { stdinEnabled = true diff --git a/Services/PanelService.qml b/Services/PanelService.qml index 8f0ca3a..940e242 100644 --- a/Services/PanelService.qml +++ b/Services/PanelService.qml @@ -5,8 +5,16 @@ import Quickshell Singleton { id: root + // A ref. to the sidePanel, so it's accessible from other services + property var sidePanel: null + // Currently opened panel property var openedPanel: null - property var sidePanel: null + function registerOpen(panel) { + if (openedPanel && openedPanel != panel) { + openedPanel.close() + } + openedPanel = panel + } } diff --git a/Services/ScalingService.qml b/Services/ScalingService.qml index 9679bc5..2a9792d 100644 --- a/Services/ScalingService.qml +++ b/Services/ScalingService.qml @@ -9,7 +9,15 @@ Singleton { // ------------------------------------------- // Manual scaling via Settings function scale(aScreen) { - return scaleByName(aScreen.name) + try { + if (aScreen !== undefined && aScreen.name !== undefined) { + return scaleByName(aScreen.name) + } + } catch (e) { + + //Logger.warn(e) + } + return 1.0 } function scaleByName(aScreenName) { diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml index 322499a..a22ac03 100644 --- a/Widgets/NLoader.qml +++ b/Widgets/NLoader.qml @@ -3,7 +3,7 @@ import QtQuick // Example usage: // NLoader { // content: Component { -// NPanel { +// YourComponent { Loader { id: loader @@ -17,9 +17,6 @@ Loader { asynchronous: true sourceComponent: content - // onLoaded: { - // Logger.log("NLoader", "OnLoaded:", item.toString()); - // } onActiveChanged: { if (active && item && item.show) { item.show() diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 8b8efe5..7398439 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -4,111 +4,180 @@ import Quickshell.Wayland import qs.Commons import qs.Services -PanelWindow { +Loader { id: root + active: false + asynchronous: true + readonly property real scaling: ScalingService.scale(screen) + property ShellScreen screen - property bool showOverlay: Settings.data.general.dimDesktop - property int topMargin: Settings.data.bar.position === "top" ? Style.barHeight * scaling : 0 - property int bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling : 0 - // Show dimming if this panel is opened OR if we're in a transition (to prevent flickering) - property color overlayColor: (showOverlay && (PanelService.openedPanel === root - || isTransitioning)) ? Color.applyOpacity(Color.mShadow, - "AA") : Color.transparent - property bool isTransitioning: false - signal dismissed + property Component panelContent: null + property int rWidth: 1500 + property int rHeight: 400 + property bool rAnchorCentered: false + property bool rAnchorLeft: false + property bool rAnchorRight: false - function hide() { - // Clear the panel service when hiding - if (PanelService.openedPanel === root) { - PanelService.openedPanel = null - } - isTransitioning = false - visible = false - root.dismissed() - } + // Animation properties + readonly property real originalScale: 0.7 + readonly property real originalOpacity: 0.0 + property real scaleValue: originalScale + property real opacityValue: originalOpacity - function show() { - // Ensure only one panel is visible at a time using PanelService as ephemeral storage - try { - if (PanelService.openedPanel && PanelService.openedPanel !== root && PanelService.openedPanel.hide) { - // Mark both panels as transitioning to prevent dimming flicker - isTransitioning = true - PanelService.openedPanel.isTransitioning = true - PanelService.openedPanel.hide() - // Small delay to ensure smooth transition - showTimer.start() - return - } - // No previous panel, show immediately - PanelService.openedPanel = root - visible = true - } catch (e) { + property alias isClosing: hideTimer.running - // ignore + signal opened + signal closed + + + + // ----------------------------------------- + function toggle(aScreen) { + if (!active || isClosing) { + open(aScreen) + } else { + close() } } - implicitWidth: screen.width - implicitHeight: screen.height - color: visible ? overlayColor : Color.transparent - visible: false - WlrLayershell.exclusionMode: ExclusionMode.Ignore - - anchors.top: true - anchors.left: true - anchors.right: true - anchors.bottom: true - margins.top: topMargin - margins.bottom: bottomMargin - - MouseArea { - anchors.fill: parent - onClicked: root.hide() - } - - Behavior on color { - ColorAnimation { - duration: Style.animationSlow - easing.type: Easing.InOutCubic + // ----------------------------------------- + function open(aScreen) { + if (aScreen !== null) { + screen = aScreen } + + // Special case if currently closing/animating + if (isClosing) { + hideTimer.stop() // in case we were closing + scaleValue = 1.0 + opacityValue = 1.0 + } + + PanelService.registerOpen(root) + + active = true + root.opened() } + // ----------------------------------------- + function close() { + scaleValue = originalScale + opacityValue = originalOpacity + hideTimer.start() + } + + // ----------------------------------------- + function closeCompleted() { + root.closed() + active = false + } + + // ----------------------------------------- + // Timer to disable the loader after the close animation is completed Timer { - id: showTimer - interval: 50 // Small delay to ensure smooth transition + id: hideTimer + interval: Style.animationSlow repeat: false onTriggered: { - PanelService.openedPanel = root - isTransitioning = false - visible = true + closeCompleted() } } - Component.onDestruction: { - try { - if (visible && Settings.openPanel === root) - Settings.openPanel = null - } catch (e) { + // ----------------------------------------- + sourceComponent: Component { + PanelWindow { + id: panelWindow - } - } + visible: true - onVisibleChanged: { - try { - if (!visible) { - // Clear panel service when panel becomes invisible - if (PanelService.openedPanel === root) { - PanelService.openedPanel = null + // Dim desktop if required + color: (root.active && !root.isClosing && Settings.data.general.dimDesktop) ? Color.applyOpacity( + Color.mShadow, + "BB") : Color.transparent + + + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "noctalia-panel" + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal } - if (Settings.openPanel === root) { - Settings.openPanel = null - } - isTransitioning = false } - } catch (e) { + anchors.top: true + anchors.left: true + anchors.right: true + anchors.bottom: true + margins.top: Settings.data.bar.position === "top" ? Style.barHeight * scaling : 0 + margins.bottom: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling : 0 + + // Clicking outside of the rectangle to close + MouseArea { + anchors.fill: parent + onClicked: root.close() + } + + Rectangle { + id: panelBackground + color: Color.mSurface + radius: Style.radiusL * scaling + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + layer.enabled: true + width: rWidth + height: rHeight + + anchors { + centerIn: rAnchorCentered ? parent : null + left: !rAnchorCentered && rAnchorLeft ? parent.left : parent.center + right: !rAnchorCentered && rAnchorRight ? parent.right : parent.center + top: !rAnchorCentered && (Settings.data.bar.position === "top") ? parent.top : undefined + bottom: !rAnchorCentered && (Settings.data.bar.position === "bottom") ? parent.bottom : undefined + + // margins + topMargin: !rAnchorCentered && (Settings.data.bar.position === "top") ? Style.marginS * scaling : undefined + bottomMargin: !rAnchorCentered + && (Settings.data.bar.position === "bottom") ? Style.marginS * scaling : undefined + rightMargin: !rAnchorCentered && rAnchorRight ? Style.marginS * scaling : undefined + } + + scale: root.scaleValue + opacity: root.opacityValue + + // Animate in when component is completed + Component.onCompleted: { + root.scaleValue = 1.0 + root.opacityValue = 1.0 + } + + // Prevent closing when clicking in the panel bg + MouseArea { + anchors.fill: parent + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutExpo + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + Loader { + anchors.fill: parent + sourceComponent: root.panelContent + } + } } } }