Fix TrayMenu crash after display wake. Add checks if screen exists, else set scaling to 1.0

TrayMenu: Replace PopupPanel with NPanel (for better loading & to
prevent QS crash)
Overview, Background etc: add screen checks, if it doesnt exist set
scaling to 1.0
This commit is contained in:
Ly-sec 2025-08-31 08:55:20 +02:00
parent 714f6c058f
commit 51f1923e22
9 changed files with 119 additions and 137 deletions

View file

@ -12,7 +12,7 @@ Variants {
required property ShellScreen modelData required property ShellScreen modelData
active: Settings.isLoaded active: Settings.isLoaded && modelData
sourceComponent: PanelWindow { sourceComponent: PanelWindow {
id: root id: root
@ -38,7 +38,7 @@ Variants {
property real stripesAngle: 0 property real stripesAngle: 0
// External state management // External state management
property string servicedWallpaper: WallpaperService.getWallpaper(modelData.name) property string servicedWallpaper: modelData ? WallpaperService.getWallpaper(modelData.name) : ""
property string futureWallpaper: "" property string futureWallpaper: ""
onServicedWallpaperChanged: { onServicedWallpaperChanged: {
// Set wallpaper immediately on startup // Set wallpaper immediately on startup

View file

@ -12,12 +12,14 @@ Variants {
delegate: Loader { delegate: Loader {
required property ShellScreen modelData required property ShellScreen modelData
active: Settings.isLoaded && CompositorService.isNiri active: Settings.isLoaded && CompositorService.isNiri && modelData
sourceComponent: PanelWindow { sourceComponent: PanelWindow {
Component.onCompleted: { Component.onCompleted: {
if (modelData) {
Logger.log("Overview", "Loading Overview component for Niri on", modelData.name) Logger.log("Overview", "Loading Overview component for Niri on", modelData.name)
} }
}
color: Color.transparent color: Color.transparent
screen: modelData screen: modelData
@ -36,7 +38,7 @@ Variants {
id: bgImage id: bgImage
anchors.fill: parent anchors.fill: parent
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
source: WallpaperService.getWallpaper(modelData.name) source: modelData ? WallpaperService.getWallpaper(modelData.name) : ""
smooth: true smooth: true
mipmap: false mipmap: false
cache: false cache: false

View file

@ -16,13 +16,13 @@ Variants {
id: root id: root
required property ShellScreen modelData 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 || (Settings.data.bar.monitors.length === 0)) : false
sourceComponent: PanelWindow { sourceComponent: PanelWindow {
screen: modelData screen: modelData || null
WlrLayershell.namespace: "noctalia-bar" WlrLayershell.namespace: "noctalia-bar"
@ -65,7 +65,7 @@ Variants {
delegate: NWidgetLoader { delegate: NWidgetLoader {
widgetName: modelData widgetName: modelData
widgetProps: { widgetProps: {
"screen": screen "screen": root.modelData || null
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@ -87,7 +87,7 @@ Variants {
delegate: NWidgetLoader { delegate: NWidgetLoader {
widgetName: modelData widgetName: modelData
widgetProps: { widgetProps: {
"screen": screen "screen": root.modelData || null
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@ -110,7 +110,7 @@ Variants {
delegate: NWidgetLoader { delegate: NWidgetLoader {
widgetName: modelData widgetName: modelData
widgetProps: { widgetProps: {
"screen": screen "screen": root.modelData || null
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }

View file

@ -6,26 +6,23 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
PopupWindow { NPanel {
id: root id: root
objectName: "trayMenu"
panelWidth: 180 * scaling
panelHeight: 220 * scaling
panelAnchorRight: true
property QsMenuHandle menu property QsMenuHandle menu
property var anchorItem: null property var anchorItem: null
property real anchorX property real anchorX
property real anchorY property real anchorY
property bool isSubMenu: false 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) { function showAt(item, x, y) {
if (!item) { if (!item) {
@ -33,27 +30,18 @@ PopupWindow {
return 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 anchorItem = item
anchorX = x anchorX = x
anchorY = y anchorY = y
visible = true // Use NPanel's open method instead of PopupWindow's visible
forceActiveFocus() open(screen)
// Force update after showing.
Qt.callLater(() => {
root.anchor.updateAnchor()
})
} }
function hideMenu() { function hideMenu() {
visible = false close()
// Clean up all submenus recursively // Clean up all submenus recursively
for (var i = 0; i < columnLayout.children.length; i++) { for (var i = 0; i < columnLayout.children.length; i++) {
@ -66,16 +54,18 @@ PopupWindow {
} }
} }
panelContent: Rectangle {
color: Color.transparent
anchors.fill: parent
anchors.margins: Style.marginS * scaling
// Full-sized, transparent MouseArea to track the mouse. // Full-sized, transparent MouseArea to track the mouse.
MouseArea { MouseArea {
id: rootMouseArea id: rootMouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
} onEntered: root.isHovered = true
onExited: root.isHovered = false
Item {
anchors.fill: parent
Keys.onEscapePressed: root.hideMenu()
} }
QsMenuOpener { QsMenuOpener {
@ -83,12 +73,18 @@ PopupWindow {
menu: root.menu menu: root.menu
} }
Rectangle { Component.onCompleted: {
anchors.fill: parent if (menu && opener.children && opener.children.values.length === 0) {
color: Color.mSurface // Menu not ready, try again later
border.color: Color.mOutline Qt.callLater(() => {
border.width: Math.max(1, Style.borderS * scaling) if (opener.children && opener.children.values.length > 0) {
radius: Style.radiusM * scaling // 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 { Flickable {
@ -206,28 +202,17 @@ PopupWindow {
entry.subMenu.destroy() entry.subMenu.destroy()
} }
// Need a slight overlap so that menu don't close when moving the mouse to a submenu // Create submenu using the same TrayMenu component
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
entry.subMenu = Qt.createComponent("TrayMenu.qml").createObject(root, { entry.subMenu = Qt.createComponent("TrayMenu.qml").createObject(root, {
"menu": modelData, "menu": modelData,
"anchorItem": entry, "anchorItem": entry,
"anchorX": anchorX, "anchorX": entry.width,
"anchorY": 0, "anchorY": 0,
"isSubMenu": true "isSubMenu": true
}) })
if (entry.subMenu) { if (entry.subMenu) {
entry.subMenu.showAt(entry, anchorX, 0) entry.subMenu.open(screen)
} }
} }
} }
@ -255,3 +240,4 @@ PopupWindow {
} }
} }
} }
}

View file

@ -14,7 +14,7 @@ Rectangle {
id: root id: root
property ShellScreen screen property ShellScreen screen
property real scaling: ScalingService.scale(screen) property real scaling: screen ? ScalingService.scale(screen) : 1.0
readonly property real itemSize: 24 * scaling readonly property real itemSize: 24 * scaling
visible: SystemTray.items.values.length > 0 visible: SystemTray.items.values.length > 0
@ -80,35 +80,42 @@ Rectangle {
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
// Close any open menu first // Close any open menu first
trayPanel.close() var trayMenuPanel = PanelService.getPanel("trayMenu")
if (trayMenuPanel && trayMenuPanel.active) {
trayMenuPanel.close()
}
if (!modelData.onlyMenu) { if (!modelData.onlyMenu) {
modelData.activate() modelData.activate()
} }
} else if (mouse.button === Qt.MiddleButton) { } else if (mouse.button === Qt.MiddleButton) {
// Close any open menu first // Close any open menu first
trayPanel.close() var trayMenuPanel = PanelService.getPanel("trayMenu")
if (trayMenuPanel && trayMenuPanel.active) {
trayMenuPanel.close()
}
modelData.secondaryActivate && modelData.secondaryActivate() modelData.secondaryActivate && modelData.secondaryActivate()
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
trayTooltip.hide() trayTooltip.hide()
// Close the menu if it was visible // Don't open menu if screen is invalid
if (trayPanel && trayPanel.visible) { if (!screen || !screen.name) {
trayPanel.close() Logger.warn("Tray", "Cannot open tray menu: invalid screen object")
return return
} }
if (modelData.hasMenu && modelData.menu && trayMenu.item) { if (modelData.hasMenu && modelData.menu) {
trayPanel.open() // Get the tray menu panel from PanelService
var trayMenuPanel = PanelService.getPanel("trayMenu")
// Anchor the menu to the tray icon item (parent) and position it below the icon if (trayMenuPanel) {
const menuX = (width / 2) - (trayMenu.item.width / 2) trayMenuPanel.menu = modelData.menu
const menuY = (Style.barHeight * scaling) trayMenuPanel.toggle(screen, this)
trayMenu.item.menu = modelData.menu
trayMenu.item.showAt(parent, menuX, menuY)
} else { } else {
Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set") Logger.warn("Tray", "Tray menu panel not found")
}
} else {
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"
}
}
} }

View file

@ -93,7 +93,7 @@ Loader {
id: lockBgImage id: lockBgImage
anchors.fill: parent anchors.fill: parent
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
source: WallpaperService.getWallpaper(screen.name) source: screen ? WallpaperService.getWallpaper(screen.name) : ""
cache: true cache: true
smooth: true smooth: true
mipmap: false mipmap: false

View file

@ -29,7 +29,7 @@ ColumnLayout {
NImageRounded { NImageRounded {
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginXS * scaling anchors.margins: Style.marginXS * scaling
imagePath: WallpaperService.getWallpaper(screen.name) imagePath: screen ? WallpaperService.getWallpaper(screen.name) : ""
fallbackIcon: "image" fallbackIcon: "image"
imageRadius: Style.radiusM * scaling imageRadius: Style.radiusM * scaling
} }
@ -74,7 +74,7 @@ ColumnLayout {
} }
} }
property list<string> wallpapersList: WallpaperService.getWallpapersList(screen.name) property list<string> wallpapersList: screen ? WallpaperService.getWallpapersList(screen.name) : []
NToggle { NToggle {
label: "Assign selection to all monitors" label: "Assign selection to all monitors"
@ -115,7 +115,7 @@ ColumnLayout {
id: wallpaperItem id: wallpaperItem
property string wallpaperPath: modelData 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 width: wallpaperGridView.itemSize
height: Math.floor(wallpaperGridView.itemSize * 0.67) height: Math.floor(wallpaperGridView.itemSize * 0.67)
@ -183,7 +183,7 @@ ColumnLayout {
onPressed: { onPressed: {
if (Settings.data.wallpaper.setWallpaperOnAllMonitors) { if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(undefined, wallpaperPath) WallpaperService.changeWallpaper(undefined, wallpaperPath)
} else { } else if (screen) {
WallpaperService.changeWallpaper(screen.name, wallpaperPath) WallpaperService.changeWallpaper(screen.name, wallpaperPath)
} }
} }

View file

@ -11,7 +11,7 @@ Loader {
asynchronous: true asynchronous: true
property ShellScreen screen 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 Component panelContent: null
property int panelWidth: 1500 property int panelWidth: 1500
@ -50,6 +50,12 @@ Loader {
// ----------------------------------------- // -----------------------------------------
function toggle(aScreen, buttonItem) { 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) { if (!active || isClosing) {
open(aScreen, buttonItem) open(aScreen, buttonItem)
} else { } else {
@ -59,6 +65,12 @@ Loader {
// ----------------------------------------- // -----------------------------------------
function open(aScreen, buttonItem) { 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) { if (aScreen !== null) {
screen = aScreen screen = aScreen
} }

View file

@ -16,6 +16,7 @@ import qs.Commons
import qs.Modules.Launcher import qs.Modules.Launcher
import qs.Modules.Background import qs.Modules.Background
import qs.Modules.Bar import qs.Modules.Bar
import qs.Modules.Bar.Extras
import qs.Modules.BluetoothPanel import qs.Modules.BluetoothPanel
import qs.Modules.Calendar import qs.Modules.Calendar
import qs.Modules.Dock import qs.Modules.Dock
@ -99,6 +100,11 @@ ShellRoot {
objectName: "archUpdaterPanel" objectName: "archUpdaterPanel"
} }
TrayMenu {
id: trayMenuPanel
objectName: "trayMenu"
}
Component.onCompleted: { Component.onCompleted: {
// Save a ref. to our lockScreen so we can access it easily // Save a ref. to our lockScreen so we can access it easily
PanelService.lockScreen = lockScreen PanelService.lockScreen = lockScreen