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 {
screen: root.bar.screen
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
onMenuClosed: menuLoader.active = false
onDismissed: menuLoader.active = false
onRunCommand: cmd => {
runner.command = cmd;
runner.running = true;

View file

@ -1,105 +1,28 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import "." as M
PanelWindow {
M.PopupPanel {
id: menuWindow
required property var screen
required property real anchorX
panelWidth: 180
signal menuClosed
signal runCommand(var cmd)
readonly property bool _isNiri: Quickshell.env("NIRI_SOCKET") !== ""
function _run(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 {
model: [
{
label: "Lock",
icon: "\uF023",
cmd: ["loginctl", "lock-session"],
color: M.Theme.base0D
},
{
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
}
{ label: "Lock", icon: "\uF023", cmd: ["loginctl", "lock-session"], color: M.Theme.base0D },
{ 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 {
@ -108,7 +31,7 @@ PanelWindow {
required property var modelData
required property int index
width: menuCol.width
width: menuWindow.panelWidth
height: 32
Rectangle {
@ -148,6 +71,4 @@ PanelWindow {
}
}
}
}
}
}

View file

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

View file

@ -1,75 +1,15 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "." as M
// Per-icon context menu popup window.
// Covers the screen on the Overlay layer so clicking anywhere outside
// the menu panel dismisses it. Created on demand by Tray.qml delegates.
PanelWindow {
M.PopupPanel {
id: menuWindow
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 _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 {
id: opener
menu: menuWindow._currentHandle
@ -78,7 +18,7 @@ PanelWindow {
// Back button (submenus only)
Item {
visible: menuWindow._handleStack.length > 0
width: menuCol.width
width: menuWindow.panelWidth
height: visible ? 28 : 0
Rectangle {
@ -119,10 +59,9 @@ PanelWindow {
required property QsMenuEntry modelData
width: menuCol.width
width: menuWindow.panelWidth
height: modelData.isSeparator ? 9 : 28
// Separator
Rectangle {
visible: entryItem.modelData.isSeparator
anchors.verticalCenter: parent.verticalCenter
@ -134,7 +73,6 @@ PanelWindow {
color: M.Theme.base03
}
// Hover highlight
Rectangle {
visible: !entryItem.modelData.isSeparator
anchors.fill: parent
@ -144,7 +82,6 @@ PanelWindow {
radius: M.Theme.radius
}
// Icon
Image {
id: entryIcon
visible: !entryItem.modelData.isSeparator && entryItem.modelData.icon !== ""
@ -157,7 +94,6 @@ PanelWindow {
fillMode: Image.PreserveAspectFit
}
// Label
Text {
visible: !entryItem.modelData.isSeparator
anchors.verticalCenter: parent.verticalCenter
@ -172,7 +108,6 @@ PanelWindow {
elide: Text.ElideRight
}
// Submenu chevron
Text {
id: entryChevron
visible: !entryItem.modelData.isSeparator && entryItem.modelData.hasChildren
@ -196,9 +131,7 @@ PanelWindow {
menuWindow._currentHandle = entryItem.modelData;
} else {
entryItem.modelData.triggered();
menuWindow.menuClosed();
}
}
menuWindow.dismiss();
}
}
}

View file

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