extract shared PopupPanel for animated tray/power menus

This commit is contained in:
Damocles 2026-04-12 16:24:33 +02:00
parent 77ce83462d
commit b8ec39f2c9
6 changed files with 249 additions and 302 deletions

92
modules/PopupPanel.qml Normal file
View file

@ -0,0 +1,92 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "." as M
// Shared flyout popup window slides down from the bar, dismisses on
// click outside. Created on demand via Loader; animates in on creation,
// animates out then emits dismissed() for the Loader to deactivate.
PanelWindow {
id: root
default property alias content: contentCol.children
required property var screen
required property real anchorX
property real panelWidth: 220
signal dismissed()
visible: true
color: "transparent"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: 0
WlrLayershell.namespace: "nova-popup"
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
Component.onCompleted: showAnim.start()
function dismiss() {
showAnim.stop();
hideAnim.start();
}
// Click outside dismiss
MouseArea {
anchors.fill: parent
onClicked: root.dismiss()
}
Item {
id: panel
x: Math.max(0, Math.min(
Math.round(root.anchorX - contentCol.width / 2),
root.width - contentCol.width
))
y: 0
width: contentCol.width
height: contentCol.height
opacity: 0
// Eat clicks inside the panel
MouseArea {
anchors.fill: parent
}
Rectangle {
anchors.fill: parent
color: M.Theme.base01
opacity: Math.max(M.Theme.barOpacity, 0.85)
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: M.Theme.radius
bottomRightRadius: M.Theme.radius
}
Column {
id: contentCol
width: root.panelWidth
topPadding: 4
bottomPadding: 4
spacing: 2
}
}
ParallelAnimation {
id: showAnim
NumberAnimation { target: panel; property: "opacity"; from: 0; to: 1; duration: 150; easing.type: Easing.OutCubic }
NumberAnimation { target: panel; property: "y"; from: -panel.height; to: 0; duration: 200; easing.type: Easing.OutCubic }
}
ParallelAnimation {
id: hideAnim
NumberAnimation { target: panel; property: "opacity"; to: 0; duration: 150; easing.type: Easing.InCubic }
NumberAnimation { target: panel; property: "y"; to: -panel.height; duration: 150; easing.type: Easing.InCubic }
onFinished: root.dismissed()
}
}

View file

@ -27,7 +27,7 @@ M.BarIcon {
sourceComponent: M.PowerMenu { sourceComponent: M.PowerMenu {
screen: root.bar.screen screen: root.bar.screen
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
onMenuClosed: menuLoader.active = false onDismissed: menuLoader.active = false
onRunCommand: cmd => { onRunCommand: cmd => {
runner.command = cmd; runner.command = cmd;
runner.running = true; runner.running = true;

View file

@ -1,105 +1,28 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import "." as M import "." as M
PanelWindow { M.PopupPanel {
id: menuWindow id: menuWindow
required property var screen panelWidth: 180
required property real anchorX
signal menuClosed
signal runCommand(var cmd) signal runCommand(var cmd)
readonly property bool _isNiri: Quickshell.env("NIRI_SOCKET") !== "" readonly property bool _isNiri: Quickshell.env("NIRI_SOCKET") !== ""
function _run(cmd) { function _run(cmd) {
runCommand(cmd); runCommand(cmd);
menuClosed(); dismiss();
} }
visible: true
color: "transparent"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: 0
WlrLayershell.namespace: "nova-powermenu"
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
MouseArea {
anchors.fill: parent
onClicked: menuWindow.menuClosed()
}
Item {
id: panel
x: Math.max(0, Math.min(Math.round(menuWindow.anchorX - menuCol.width / 2), menuWindow.width - menuCol.width))
y: 0
width: menuCol.width
height: menuCol.height
MouseArea {
anchors.fill: parent
}
Rectangle {
anchors.fill: parent
color: M.Theme.base01
opacity: Math.max(M.Theme.barOpacity, 0.85)
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: M.Theme.radius
bottomRightRadius: M.Theme.radius
}
Column {
id: menuCol
width: 180
topPadding: 4
bottomPadding: 4
spacing: 2
Repeater { Repeater {
model: [ model: [
{ { label: "Lock", icon: "\uF023", cmd: ["loginctl", "lock-session"], color: M.Theme.base0D },
label: "Lock", { label: "Suspend", icon: "\uF186", cmd: ["systemctl", "suspend"], color: M.Theme.base0E },
icon: "\uF023", { label: "Logout", icon: "\uF2F5", cmd: menuWindow._isNiri ? ["niri", "msg", "action", "quit"] : ["loginctl", "terminate-user", ""], color: M.Theme.base0A },
cmd: ["loginctl", "lock-session"], { label: "Reboot", icon: "\uF021", cmd: ["systemctl", "reboot"], color: M.Theme.base09 },
color: M.Theme.base0D { label: "Shutdown", icon: "\uF011", cmd: ["systemctl", "poweroff"], color: M.Theme.base08 }
},
{
label: "Suspend",
icon: "\uF186",
cmd: ["systemctl", "suspend"],
color: M.Theme.base0E
},
{
label: "Logout",
icon: "\uF2F5",
cmd: menuWindow._isNiri ? ["niri", "msg", "action", "quit"] : ["loginctl", "terminate-user", ""],
color: M.Theme.base0A
},
{
label: "Reboot",
icon: "\uF021",
cmd: ["systemctl", "reboot"],
color: M.Theme.base09
},
{
label: "Shutdown",
icon: "\uF011",
cmd: ["systemctl", "poweroff"],
color: M.Theme.base08
}
] ]
delegate: Item { delegate: Item {
@ -108,7 +31,7 @@ PanelWindow {
required property var modelData required property var modelData
required property int index required property int index
width: menuCol.width width: menuWindow.panelWidth
height: 32 height: 32
Rectangle { Rectangle {
@ -148,6 +71,4 @@ PanelWindow {
} }
} }
} }
}
}
} }

View file

@ -69,7 +69,7 @@ RowLayout {
handle: iconItem.modelData.menu handle: iconItem.modelData.menu
screen: root.bar.screen screen: root.bar.screen
anchorX: iconItem.mapToGlobal(iconItem.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) anchorX: iconItem.mapToGlobal(iconItem.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
onMenuClosed: { onDismissed: {
menuLoader.active = false; menuLoader.active = false;
root._activeMenu = null; root._activeMenu = null;
} }

View file

@ -1,75 +1,15 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland
import "." as M import "." as M
// Per-icon context menu popup window. M.PopupPanel {
// Covers the screen on the Overlay layer so clicking anywhere outside
// the menu panel dismisses it. Created on demand by Tray.qml delegates.
PanelWindow {
id: menuWindow id: menuWindow
required property var handle required property var handle
required property var screen
required property real anchorX
signal menuClosed
// Current menu level swapped when entering/leaving submenus
property var _currentHandle: handle property var _currentHandle: handle
property var _handleStack: [] property var _handleStack: []
visible: true
color: "transparent"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: 0
WlrLayershell.namespace: "nova-traymenu"
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
// Click outside the menu panel dismiss
MouseArea {
anchors.fill: parent
onClicked: menuWindow.menuClosed()
}
// Menu panel
Item {
id: panel
x: Math.max(0, Math.min(Math.round(menuWindow.anchorX - menuCol.width / 2), menuWindow.width - menuCol.width))
y: 0
width: menuCol.width
height: menuCol.height
// Eat clicks inside the panel
MouseArea {
anchors.fill: parent
}
Rectangle {
anchors.fill: parent
color: M.Theme.base01
opacity: Math.max(M.Theme.barOpacity, 0.85)
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: M.Theme.radius
bottomRightRadius: M.Theme.radius
}
Column {
id: menuCol
width: 220
topPadding: 4
bottomPadding: 4
spacing: 2
QsMenuOpener { QsMenuOpener {
id: opener id: opener
menu: menuWindow._currentHandle menu: menuWindow._currentHandle
@ -78,7 +18,7 @@ PanelWindow {
// Back button (submenus only) // Back button (submenus only)
Item { Item {
visible: menuWindow._handleStack.length > 0 visible: menuWindow._handleStack.length > 0
width: menuCol.width width: menuWindow.panelWidth
height: visible ? 28 : 0 height: visible ? 28 : 0
Rectangle { Rectangle {
@ -119,10 +59,9 @@ PanelWindow {
required property QsMenuEntry modelData required property QsMenuEntry modelData
width: menuCol.width width: menuWindow.panelWidth
height: modelData.isSeparator ? 9 : 28 height: modelData.isSeparator ? 9 : 28
// Separator
Rectangle { Rectangle {
visible: entryItem.modelData.isSeparator visible: entryItem.modelData.isSeparator
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -134,7 +73,6 @@ PanelWindow {
color: M.Theme.base03 color: M.Theme.base03
} }
// Hover highlight
Rectangle { Rectangle {
visible: !entryItem.modelData.isSeparator visible: !entryItem.modelData.isSeparator
anchors.fill: parent anchors.fill: parent
@ -144,7 +82,6 @@ PanelWindow {
radius: M.Theme.radius radius: M.Theme.radius
} }
// Icon
Image { Image {
id: entryIcon id: entryIcon
visible: !entryItem.modelData.isSeparator && entryItem.modelData.icon !== "" visible: !entryItem.modelData.isSeparator && entryItem.modelData.icon !== ""
@ -157,7 +94,6 @@ PanelWindow {
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
} }
// Label
Text { Text {
visible: !entryItem.modelData.isSeparator visible: !entryItem.modelData.isSeparator
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -172,7 +108,6 @@ PanelWindow {
elide: Text.ElideRight elide: Text.ElideRight
} }
// Submenu chevron
Text { Text {
id: entryChevron id: entryChevron
visible: !entryItem.modelData.isSeparator && entryItem.modelData.hasChildren visible: !entryItem.modelData.isSeparator && entryItem.modelData.hasChildren
@ -196,9 +131,7 @@ PanelWindow {
menuWindow._currentHandle = entryItem.modelData; menuWindow._currentHandle = entryItem.modelData;
} else { } else {
entryItem.modelData.triggered(); entryItem.modelData.triggered();
menuWindow.menuClosed(); menuWindow.dismiss();
}
}
} }
} }
} }

View file

@ -13,6 +13,7 @@ Clock 1.0 Clock.qml
Volume 1.0 Volume.qml Volume 1.0 Volume.qml
Tray 1.0 Tray.qml Tray 1.0 Tray.qml
TrayMenu 1.0 TrayMenu.qml TrayMenu 1.0 TrayMenu.qml
PopupPanel 1.0 PopupPanel.qml
PowerMenu 1.0 PowerMenu.qml PowerMenu 1.0 PowerMenu.qml
ScreenCorners 1.0 ScreenCorners.qml ScreenCorners 1.0 ScreenCorners.qml
Osd 1.0 Osd.qml Osd 1.0 Osd.qml