more flyout stuff, tray menus
This commit is contained in:
parent
3a548930e2
commit
59458cade3
4 changed files with 401 additions and 1 deletions
|
|
@ -2,6 +2,7 @@ import QtQuick
|
|||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import "." as M
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
|
@ -25,6 +26,20 @@ RowLayout {
|
|||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: {
|
||||
const tip = [iconItem.modelData.tooltipTitle, iconItem.modelData.tooltipDescription]
|
||||
.filter(s => s).join("\n") || iconItem.modelData.title;
|
||||
if (hovered && tip) {
|
||||
M.FlyoutState.text = tip;
|
||||
M.FlyoutState.itemX = iconItem.mapToGlobal(iconItem.width / 2, 0).x;
|
||||
M.FlyoutState.visible = true;
|
||||
} else if (!hovered) {
|
||||
M.FlyoutState.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
|
@ -32,10 +47,26 @@ RowLayout {
|
|||
if (mouse.button === Qt.LeftButton) {
|
||||
iconItem.modelData.activate();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
iconItem.modelData.display(root.bar, mouse.x, mouse.y);
|
||||
if (iconItem.modelData.menu) {
|
||||
menuLoader.active = true;
|
||||
} else {
|
||||
iconItem.modelData.secondaryActivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Per-icon context menu window, created on demand
|
||||
Loader {
|
||||
id: menuLoader
|
||||
active: false
|
||||
sourceComponent: M.TrayMenu {
|
||||
handle: iconItem.modelData.menu
|
||||
screen: root.bar.screen
|
||||
anchorX: iconItem.mapToGlobal(iconItem.width / 2, 0).x
|
||||
onClosed: menuLoader.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
235
modules/TrayMenu.qml
Normal file
235
modules/TrayMenu.qml
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "." as M
|
||||
|
||||
// Per-icon context menu popup window.
|
||||
// Covers the full screen on the Overlay layer so clicking anywhere outside
|
||||
// the menu panel dismisses it. Created on demand by Tray.qml delegates.
|
||||
PanelWindow {
|
||||
id: menuWindow
|
||||
|
||||
required property QsMenuHandle handle
|
||||
required property var screen
|
||||
// Global x of the icon center (from mapToGlobal), used to position the panel
|
||||
required property real anchorX
|
||||
|
||||
signal closed()
|
||||
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
// -1 = ignore exclusive zones so this covers the full screen including the bar
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
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.closed()
|
||||
}
|
||||
|
||||
// Menu panel
|
||||
Item {
|
||||
id: panel
|
||||
|
||||
x: Math.max(0, Math.min(
|
||||
Math.round(menuWindow.anchorX - menuStack.implicitWidth / 2),
|
||||
menuWindow.width - menuStack.implicitWidth
|
||||
))
|
||||
y: M.Theme.barHeight
|
||||
|
||||
implicitWidth: menuStack.implicitWidth
|
||||
implicitHeight: menuStack.implicitHeight
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
// Eat clicks inside the panel so they don't reach the dismiss area above
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: M.Theme.base01
|
||||
opacity: M.Theme.barOpacity
|
||||
radius: M.Theme.radius
|
||||
border.color: M.Theme.base02
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: menuStack
|
||||
|
||||
implicitWidth: currentItem ? currentItem.implicitWidth : 0
|
||||
implicitHeight: currentItem ? currentItem.implicitHeight : 0
|
||||
|
||||
// Push the root page once the stack is ready
|
||||
Component.onCompleted: {
|
||||
menuStack.push(menuPageComp, { handle: menuWindow.handle, isRoot: true }, StackView.Immediate);
|
||||
}
|
||||
|
||||
pushEnter: Transition {}
|
||||
pushExit: Transition {}
|
||||
popEnter: Transition {}
|
||||
popExit: Transition {}
|
||||
}
|
||||
}
|
||||
|
||||
// Reusable menu page component — used for both root and submenus
|
||||
Component {
|
||||
id: menuPageComp
|
||||
|
||||
Column {
|
||||
id: page
|
||||
|
||||
required property QsMenuHandle handle
|
||||
property bool isRoot: false
|
||||
|
||||
implicitWidth: 220
|
||||
topPadding: 4
|
||||
bottomPadding: 4
|
||||
spacing: 2
|
||||
|
||||
QsMenuOpener {
|
||||
id: opener
|
||||
menu: page.handle
|
||||
}
|
||||
|
||||
// Back button (shown only in submenus)
|
||||
Item {
|
||||
visible: !page.isRoot
|
||||
implicitWidth: page.implicitWidth
|
||||
implicitHeight: 28
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 4
|
||||
anchors.rightMargin: 4
|
||||
color: backArea.containsMouse ? M.Theme.base02 : "transparent"
|
||||
radius: M.Theme.radius
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 12
|
||||
text: "‹ " + qsTr("Back")
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: menuStack.pop()
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: opener.children
|
||||
|
||||
delegate: Item {
|
||||
id: entryItem
|
||||
|
||||
required property QsMenuEntry modelData
|
||||
|
||||
implicitWidth: page.implicitWidth
|
||||
implicitHeight: modelData.isSeparator ? 9 : 28
|
||||
|
||||
// Separator line
|
||||
Rectangle {
|
||||
visible: entryItem.modelData.isSeparator
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
height: 1
|
||||
color: M.Theme.base03
|
||||
}
|
||||
|
||||
// Hover highlight
|
||||
Rectangle {
|
||||
visible: !entryItem.modelData.isSeparator
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 4
|
||||
anchors.rightMargin: 4
|
||||
color: rowArea.containsMouse && entryItem.modelData.enabled
|
||||
? M.Theme.base02 : "transparent"
|
||||
radius: M.Theme.radius
|
||||
}
|
||||
|
||||
// Icon
|
||||
Image {
|
||||
id: entryIcon
|
||||
visible: !entryItem.modelData.isSeparator && entryItem.modelData.icon !== ""
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 12
|
||||
width: M.Theme.fontSize
|
||||
height: M.Theme.fontSize
|
||||
source: entryItem.modelData.icon
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
// Label
|
||||
Text {
|
||||
visible: !entryItem.modelData.isSeparator
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: entryIcon.visible ? entryIcon.right : parent.left
|
||||
anchors.leftMargin: entryIcon.visible ? 6 : 12
|
||||
anchors.right: entryChevron.visible ? entryChevron.left : parent.right
|
||||
anchors.rightMargin: entryChevron.visible ? 4 : 12
|
||||
text: entryItem.modelData.text
|
||||
color: entryItem.modelData.enabled ? M.Theme.base05 : M.Theme.base03
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Submenu chevron
|
||||
Text {
|
||||
id: entryChevron
|
||||
visible: !entryItem.modelData.isSeparator && entryItem.modelData.hasChildren
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
text: "›"
|
||||
color: entryItem.modelData.enabled ? M.Theme.base05 : M.Theme.base03
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rowArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: !entryItem.modelData.isSeparator && entryItem.modelData.enabled
|
||||
onClicked: {
|
||||
if (entryItem.modelData.hasChildren) {
|
||||
menuStack.push(menuPageComp, {
|
||||
handle: entryItem.modelData,
|
||||
isRoot: false
|
||||
}, StackView.Immediate);
|
||||
} else {
|
||||
entryItem.modelData.triggered();
|
||||
menuWindow.closed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ WindowTitle 1.0 WindowTitle.qml
|
|||
Clock 1.0 Clock.qml
|
||||
Volume 1.0 Volume.qml
|
||||
Tray 1.0 Tray.qml
|
||||
TrayMenu 1.0 TrayMenu.qml
|
||||
Battery 1.0 Battery.qml
|
||||
Mpris 1.0 Mpris.qml
|
||||
Network 1.0 Network.qml
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue