diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index fdca504..d8589cf 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -12,7 +12,7 @@ Variants { required property ShellScreen modelData - active: Settings.isLoaded + active: Settings.isLoaded && modelData sourceComponent: PanelWindow { id: root @@ -38,7 +38,7 @@ Variants { property real stripesAngle: 0 // External state management - property string servicedWallpaper: WallpaperService.getWallpaper(modelData.name) + property string servicedWallpaper: modelData ? WallpaperService.getWallpaper(modelData.name) : "" property string futureWallpaper: "" onServicedWallpaperChanged: { // Set wallpaper immediately on startup diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index f976efe..a11ed79 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -12,11 +12,13 @@ Variants { delegate: Loader { required property ShellScreen modelData - active: Settings.isLoaded && CompositorService.isNiri + active: Settings.isLoaded && CompositorService.isNiri && modelData sourceComponent: PanelWindow { Component.onCompleted: { - Logger.log("Overview", "Loading Overview component for Niri on", modelData.name) + if (modelData) { + Logger.log("Overview", "Loading Overview component for Niri on", modelData.name) + } } color: Color.transparent @@ -36,7 +38,7 @@ Variants { id: bgImage anchors.fill: parent fillMode: Image.PreserveAspectCrop - source: WallpaperService.getWallpaper(modelData.name) + source: modelData ? WallpaperService.getWallpaper(modelData.name) : "" smooth: true mipmap: false cache: false diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index fe74419..168c26f 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -16,13 +16,13 @@ Variants { id: root required property ShellScreen modelData - readonly property real scaling: ScalingService.scale(modelData) + readonly property real scaling: modelData ? ScalingService.scale(modelData) : 1.0 - active: Settings.isLoaded && modelData ? (Settings.data.bar.monitors.includes(modelData.name) + active: Settings.isLoaded && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false sourceComponent: PanelWindow { - screen: modelData + screen: modelData || null WlrLayershell.namespace: "noctalia-bar" @@ -65,7 +65,7 @@ Variants { delegate: NWidgetLoader { widgetName: modelData widgetProps: { - "screen": screen + "screen": root.modelData || null } anchors.verticalCenter: parent.verticalCenter } @@ -87,7 +87,7 @@ Variants { delegate: NWidgetLoader { widgetName: modelData widgetProps: { - "screen": screen + "screen": root.modelData || null } anchors.verticalCenter: parent.verticalCenter } @@ -110,7 +110,7 @@ Variants { delegate: NWidgetLoader { widgetName: modelData widgetProps: { - "screen": screen + "screen": root.modelData || null } anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Bar/Extras/TrayMenu.qml b/Modules/Bar/Extras/TrayMenu.qml index ce57d5e..54d836c 100644 --- a/Modules/Bar/Extras/TrayMenu.qml +++ b/Modules/Bar/Extras/TrayMenu.qml @@ -6,26 +6,23 @@ import qs.Commons import qs.Services import qs.Widgets -PopupWindow { +NPanel { id: root + + objectName: "trayMenu" + + panelWidth: 180 * scaling + panelHeight: 220 * scaling + panelAnchorRight: true + property QsMenuHandle menu property var anchorItem: null property real anchorX property real anchorY property bool isSubMenu: false - property bool isHovered: rootMouseArea.containsMouse + property bool isHovered: false - readonly property int menuWidth: 180 - implicitWidth: menuWidth * scaling - - // Use the content height of the Flickable for implicit height - implicitHeight: Math.min(Screen.height * 0.9, flickable.contentHeight + (Style.marginS * 2 * scaling)) - visible: false - color: Color.transparent - anchor.item: anchorItem - anchor.rect.x: anchorX - anchor.rect.y: anchorY - (isSubMenu ? 0 : 4) function showAt(item, x, y) { if (!item) { @@ -33,27 +30,18 @@ PopupWindow { return } - if (!opener.children || opener.children.values.length === 0) { - //Logger.warn("TrayMenu", "Menu not ready, delaying show") - Qt.callLater(() => showAt(item, x, y)) - return - } - anchorItem = item anchorX = x anchorY = y - visible = true - forceActiveFocus() + // Use NPanel's open method instead of PopupWindow's visible + open(screen) + - // Force update after showing. - Qt.callLater(() => { - root.anchor.updateAnchor() - }) } function hideMenu() { - visible = false + close() // Clean up all submenus recursively for (var i = 0; i < columnLayout.children.length; i++) { @@ -66,38 +54,46 @@ PopupWindow { } } - // Full-sized, transparent MouseArea to track the mouse. - MouseArea { - id: rootMouseArea - anchors.fill: parent - hoverEnabled: true - } - - Item { - anchors.fill: parent - Keys.onEscapePressed: root.hideMenu() - } - - QsMenuOpener { - id: opener - menu: root.menu - } - - Rectangle { - anchors.fill: parent - color: Color.mSurface - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - radius: Style.radiusM * scaling - } - - Flickable { - id: flickable + panelContent: Rectangle { + color: Color.transparent anchors.fill: parent anchors.margins: Style.marginS * scaling - contentHeight: columnLayout.implicitHeight - interactive: true - clip: true + + // Full-sized, transparent MouseArea to track the mouse. + MouseArea { + id: rootMouseArea + anchors.fill: parent + hoverEnabled: true + onEntered: root.isHovered = true + onExited: root.isHovered = false + } + + QsMenuOpener { + id: opener + menu: root.menu + } + + Component.onCompleted: { + if (menu && opener.children && opener.children.values.length === 0) { + // Menu not ready, try again later + Qt.callLater(() => { + if (opener.children && opener.children.values.length > 0) { + // Menu is now ready + root.menuItemCount = opener.children.values.length + } + }) + } else if (opener.children && opener.children.values.length > 0) { + root.menuItemCount = opener.children.values.length + } + } + + Flickable { + id: flickable + anchors.fill: parent + anchors.margins: Style.marginS * scaling + contentHeight: columnLayout.implicitHeight + interactive: true + clip: true // Use a ColumnLayout to handle menu item arrangement ColumnLayout { @@ -206,28 +202,17 @@ PopupWindow { entry.subMenu.destroy() } - // Need a slight overlap so that menu don't close when moving the mouse to a submenu - const submenuWidth = menuWidth * scaling // Assuming a similar width as the parent - const overlap = 4 * scaling // A small overlap to bridge the mouse path - - // Check if there's enough space on the right - const globalPos = entry.mapToGlobal(0, 0) - const openLeft = (globalPos.x + entry.width + submenuWidth > Screen.width) - - // Position with overlap - const anchorX = openLeft ? -submenuWidth + overlap : entry.width - overlap - - // Create submenu + // Create submenu using the same TrayMenu component entry.subMenu = Qt.createComponent("TrayMenu.qml").createObject(root, { - "menu": modelData, - "anchorItem": entry, - "anchorX": anchorX, - "anchorY": 0, - "isSubMenu": true - }) + "menu": modelData, + "anchorItem": entry, + "anchorX": entry.width, + "anchorY": 0, + "isSubMenu": true + }) if (entry.subMenu) { - entry.subMenu.showAt(entry, anchorX, 0) + entry.subMenu.open(screen) } } } @@ -254,4 +239,5 @@ PopupWindow { } } } + } } diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 3d97c85..32fb4a7 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -14,7 +14,7 @@ Rectangle { id: root property ShellScreen screen - property real scaling: ScalingService.scale(screen) + property real scaling: screen ? ScalingService.scale(screen) : 1.0 readonly property real itemSize: 24 * scaling visible: SystemTray.items.values.length > 0 @@ -80,35 +80,42 @@ Rectangle { if (mouse.button === Qt.LeftButton) { // Close any open menu first - trayPanel.close() + var trayMenuPanel = PanelService.getPanel("trayMenu") + if (trayMenuPanel && trayMenuPanel.active) { + trayMenuPanel.close() + } if (!modelData.onlyMenu) { modelData.activate() } } else if (mouse.button === Qt.MiddleButton) { // Close any open menu first - trayPanel.close() + var trayMenuPanel = PanelService.getPanel("trayMenu") + if (trayMenuPanel && trayMenuPanel.active) { + trayMenuPanel.close() + } modelData.secondaryActivate && modelData.secondaryActivate() } else if (mouse.button === Qt.RightButton) { trayTooltip.hide() - // Close the menu if it was visible - if (trayPanel && trayPanel.visible) { - trayPanel.close() + // Don't open menu if screen is invalid + if (!screen || !screen.name) { + Logger.warn("Tray", "Cannot open tray menu: invalid screen object") return } - if (modelData.hasMenu && modelData.menu && trayMenu.item) { - trayPanel.open() - - // Anchor the menu to the tray icon item (parent) and position it below the icon - const menuX = (width / 2) - (trayMenu.item.width / 2) - const menuY = (Style.barHeight * scaling) - trayMenu.item.menu = modelData.menu - trayMenu.item.showAt(parent, menuX, menuY) + if (modelData.hasMenu && modelData.menu) { + // Get the tray menu panel from PanelService + var trayMenuPanel = PanelService.getPanel("trayMenu") + if (trayMenuPanel) { + trayMenuPanel.menu = modelData.menu + trayMenuPanel.toggle(screen, this) + } else { + Logger.warn("Tray", "Tray menu panel not found") + } } else { - Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set") + Logger.log("Tray", "No menu available for", modelData.id) } } } @@ -126,36 +133,5 @@ Rectangle { } } - PanelWindow { - id: trayPanel - anchors.top: true - anchors.left: true - anchors.right: true - anchors.bottom: true - visible: false - color: Color.transparent - screen: screen - function open() { - visible = true - - PanelService.willOpenPanel(trayPanel) - } - - function close() { - visible = false - trayMenu.item.hideMenu() - } - - // Clicking outside of the rectangle to close - MouseArea { - anchors.fill: parent - onClicked: trayPanel.close() - } - - Loader { - id: trayMenu - source: "../Extras/TrayMenu.qml" - } - } } diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 131f406..c0d5ae0 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -93,7 +93,7 @@ Loader { id: lockBgImage anchors.fill: parent fillMode: Image.PreserveAspectCrop - source: WallpaperService.getWallpaper(screen.name) + source: screen ? WallpaperService.getWallpaper(screen.name) : "" cache: true smooth: true mipmap: false diff --git a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml index fcd9e50..8edb7d0 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml @@ -29,7 +29,7 @@ ColumnLayout { NImageRounded { anchors.fill: parent anchors.margins: Style.marginXS * scaling - imagePath: WallpaperService.getWallpaper(screen.name) + imagePath: screen ? WallpaperService.getWallpaper(screen.name) : "" fallbackIcon: "image" imageRadius: Style.radiusM * scaling } @@ -74,7 +74,7 @@ ColumnLayout { } } - property list wallpapersList: WallpaperService.getWallpapersList(screen.name) + property list wallpapersList: screen ? WallpaperService.getWallpapersList(screen.name) : [] NToggle { label: "Assign selection to all monitors" @@ -115,7 +115,7 @@ ColumnLayout { id: wallpaperItem property string wallpaperPath: modelData - property bool isSelected: wallpaperPath === WallpaperService.getWallpaper(screen.name) + property bool isSelected: screen ? (wallpaperPath === WallpaperService.getWallpaper(screen.name)) : false width: wallpaperGridView.itemSize height: Math.floor(wallpaperGridView.itemSize * 0.67) @@ -183,7 +183,7 @@ ColumnLayout { onPressed: { if (Settings.data.wallpaper.setWallpaperOnAllMonitors) { WallpaperService.changeWallpaper(undefined, wallpaperPath) - } else { + } else if (screen) { WallpaperService.changeWallpaper(screen.name, wallpaperPath) } } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 659ad5b..41bf245 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -11,7 +11,7 @@ Loader { asynchronous: true property ShellScreen screen - readonly property real scaling: ScalingService.scale(screen) + readonly property real scaling: screen ? ScalingService.scale(screen) : 1.0 property Component panelContent: null property int panelWidth: 1500 @@ -50,6 +50,12 @@ Loader { // ----------------------------------------- function toggle(aScreen, buttonItem) { + // Don't toggle if screen is null or invalid + if (!aScreen || !aScreen.name) { + Logger.warn("NPanel", "Cannot toggle panel: invalid screen object") + return + } + if (!active || isClosing) { open(aScreen, buttonItem) } else { @@ -59,6 +65,12 @@ Loader { // ----------------------------------------- function open(aScreen, buttonItem) { + // Don't open if screen is null or invalid + if (!aScreen || !aScreen.name) { + Logger.warn("NPanel", "Cannot open panel: invalid screen object") + return + } + if (aScreen !== null) { screen = aScreen } diff --git a/shell.qml b/shell.qml index 4b7da7c..884e390 100644 --- a/shell.qml +++ b/shell.qml @@ -16,6 +16,7 @@ import qs.Commons import qs.Modules.Launcher import qs.Modules.Background import qs.Modules.Bar +import qs.Modules.Bar.Extras import qs.Modules.BluetoothPanel import qs.Modules.Calendar import qs.Modules.Dock @@ -99,6 +100,11 @@ ShellRoot { objectName: "archUpdaterPanel" } + TrayMenu { + id: trayMenuPanel + objectName: "trayMenu" + } + Component.onCompleted: { // Save a ref. to our lockScreen so we can access it easily PanelService.lockScreen = lockScreen