feat: tray item menus

This commit is contained in:
2 * r + 2 * t 2025-06-06 22:38:15 +10:00
parent c3a91e7554
commit 17fb60f216
6 changed files with 278 additions and 1 deletions

View file

@ -12,6 +12,7 @@ Singleton {
component Sizes: QtObject { component Sizes: QtObject {
property int innerHeight: 30 property int innerHeight: 30
property int windowPreviewSize: 400 property int windowPreviewSize: 400
property int trayMenuWidth: 300
} }
component Workspaces: QtObject { component Workspaces: QtObject {

View file

@ -15,16 +15,31 @@ StyledRect {
const spacing = Appearance.spacing.small; const spacing = Appearance.spacing.small;
const aw = activeWindow.child; const aw = activeWindow.child;
const awy = activeWindow.y + aw.y; const awy = activeWindow.y + aw.y;
const ty = tray.y;
const th = tray.implicitHeight;
const trayItems = tray.items;
const n = statusIconsInner.network; const n = statusIconsInner.network;
const ny = statusIcons.y + statusIconsInner.y + n.y - spacing / 2; const ny = statusIcons.y + statusIconsInner.y + n.y - spacing / 2;
const bls = statusIcons.y + statusIconsInner.y + statusIconsInner.bs - spacing / 2; const bls = statusIcons.y + statusIconsInner.y + statusIconsInner.bs - spacing / 2;
const ble = statusIcons.y + statusIconsInner.y + statusIconsInner.be + spacing / 2; const ble = statusIcons.y + statusIconsInner.y + statusIconsInner.be + spacing / 2;
const b = statusIconsInner.battery; const b = statusIconsInner.battery;
const by = statusIcons.y + statusIconsInner.y + b.y - spacing / 2; const by = statusIcons.y + statusIconsInner.y + b.y - spacing / 2;
if (y >= awy && y <= awy + aw.implicitHeight) { if (y >= awy && y <= awy + aw.implicitHeight) {
Popouts.currentName = "activewindow"; Popouts.currentName = "activewindow";
Popouts.currentCenter = Qt.binding(() => activeWindow.y + aw.y + aw.implicitHeight / 2); Popouts.currentCenter = Qt.binding(() => activeWindow.y + aw.y + aw.implicitHeight / 2);
Popouts.hasCurrent = true; Popouts.hasCurrent = true;
} else if (y > ty && y < ty + th) {
const index = Math.floor(((y - ty) / th) * trayItems.count);
const item = trayItems.itemAt(index);
Popouts.currentName = `traymenu${index}`;
Popouts.currentCenter = Qt.binding(() => tray.y + item.y + item.implicitHeight / 2);
Popouts.hasCurrent = true;
} else if (y >= ny && y <= ny + n.implicitHeight + spacing) { } else if (y >= ny && y <= ny + n.implicitHeight + spacing) {
Popouts.currentName = "network"; Popouts.currentName = "network";
Popouts.currentCenter = Qt.binding(() => statusIcons.y + statusIconsInner.y + n.y + n.implicitHeight / 2); Popouts.currentCenter = Qt.binding(() => statusIcons.y + statusIconsInner.y + n.y + n.implicitHeight / 2);

View file

@ -5,6 +5,8 @@ import QtQuick
Item { Item {
id: root id: root
readonly property Repeater items: items
clip: true clip: true
visible: width > 0 && height > 0 // To avoid warnings about being visible with no size visible: width > 0 && height > 0 // To avoid warnings about being visible with no size
@ -28,6 +30,8 @@ Item {
} }
Repeater { Repeater {
id: items
model: SystemTray.items model: SystemTray.items
TrayItem {} TrayItem {}

View file

@ -1,6 +1,9 @@
pragma ComponentBehavior: Bound
import "root:/services" import "root:/services"
import "root:/config" import "root:/config"
import Quickshell import Quickshell
import Quickshell.Services.SystemTray
import QtQuick import QtQuick
Item { Item {
@ -40,6 +43,24 @@ Item {
name: "battery" name: "battery"
source: "Battery.qml" source: "Battery.qml"
} }
Repeater {
model: ScriptModel {
values: [...SystemTray.items.values]
}
Popout {
id: trayMenu
required property SystemTrayItem modelData
required property int index
name: `traymenu${index}`
sourceComponent: TrayMenu {
trayItem: trayMenu.modelData.menu
}
}
}
} }
Behavior on implicitWidth { Behavior on implicitWidth {
@ -65,6 +86,7 @@ Item {
opacity: 0 opacity: 0
scale: 0.8 scale: 0.8
active: false
asynchronous: true asynchronous: true
states: State { states: State {

View file

@ -0,0 +1,235 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
StackView {
id: root
required property QsMenuHandle trayItem
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
initialItem: SubMenu {
handle: root.trayItem
}
pushEnter: Anim {}
pushExit: Anim {}
popEnter: Anim {}
popExit: Anim {}
component Anim: Transition {
NumberAnimation {
duration: 0 // Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
component SubMenu: Column {
id: menu
required property QsMenuHandle handle
property bool isSubMenu
property bool shown
padding: Appearance.padding.smaller
spacing: Appearance.spacing.small
opacity: shown ? 1 : 0
scale: shown ? 1 : 0.8
Component.onCompleted: shown = true
StackView.onActivating: shown = true
StackView.onDeactivating: shown = false
StackView.onRemoved: destroy()
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on scale {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
QsMenuOpener {
id: menuOpener
menu: menu.handle
}
Repeater {
model: menuOpener.children
StyledRect {
id: item
required property QsMenuEntry modelData
implicitWidth: BarConfig.sizes.trayMenuWidth
implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight
radius: Appearance.rounding.full
color: modelData.isSeparator ? Colours.palette.m3outlineVariant : "transparent"
Loader {
id: children
anchors.left: parent.left
anchors.right: parent.right
active: !item.modelData.isSeparator
asynchronous: true
sourceComponent: Item {
implicitHeight: label.implicitHeight
StateLayer {
anchors.margins: -Appearance.padding.small / 2
anchors.leftMargin: -Appearance.padding.smaller
anchors.rightMargin: -Appearance.padding.smaller
radius: item.radius
disabled: !item.modelData.enabled
function onClicked(): void {
const entry = item.modelData;
if (entry.hasChildren)
root.push(subMenuComp.createObject(null, {
handle: entry,
isSubMenu: true
}));
else
item.modelData.triggered();
}
}
Loader {
id: icon
anchors.left: parent.left
active: item.modelData.icon !== ""
asynchronous: true
sourceComponent: IconImage {
implicitSize: label.implicitHeight
source: item.modelData.icon
}
}
StyledText {
id: label
anchors.left: icon.right
anchors.leftMargin: icon.active ? Appearance.spacing.smaller : 0
text: labelMetrics.elidedText
color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline
}
TextMetrics {
id: labelMetrics
text: item.modelData.text
font.pointSize: label.font.pointSize
font.family: label.font.family
elide: Text.ElideRight
elideWidth: BarConfig.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Appearance.spacing.normal : 0)
}
Loader {
id: expand
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
active: item.modelData.hasChildren
asynchronous: true
sourceComponent: MaterialIcon {
text: "chevron_right"
color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline
}
}
}
}
}
}
Loader {
active: menu.isSubMenu
asynchronous: true
sourceComponent: Item {
implicitWidth: back.implicitWidth
implicitHeight: back.implicitHeight + Appearance.spacing.small / 2
Item {
anchors.bottom: parent.bottom
implicitWidth: back.implicitWidth
implicitHeight: back.implicitHeight
StyledRect {
anchors.fill: parent
anchors.margins: -Appearance.padding.small / 2
anchors.leftMargin: -Appearance.padding.smaller
anchors.rightMargin: -Appearance.padding.smaller * 2
radius: Appearance.rounding.full
color: Colours.palette.m3secondaryContainer
StateLayer {
radius: parent.radius
function onClicked(): void {
root.pop();
}
}
}
Row {
id: back
anchors.verticalCenter: parent.verticalCenter
MaterialIcon {
anchors.verticalCenter: parent.verticalCenter
text: "chevron_left"
color: Colours.palette.m3onSecondaryContainer
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Back")
color: Colours.palette.m3onSecondaryContainer
}
}
}
}
}
}
Component {
id: subMenuComp
SubMenu {}
}
}

View file

@ -1,6 +1,6 @@
#!/bin/fish #!/bin/fish
set -l dbus 'quickshell.dbus.properties.warning = false' # System tray dbus property errors set -l dbus 'quickshell.dbus.properties.warning = false;quickshell.dbus.dbusmenu.warning = false' # System tray dbus property errors
set -l notifs 'quickshell.service.notifications.warning = false' # Notification server warnings on reload set -l notifs 'quickshell.service.notifications.warning = false' # Notification server warnings on reload
set -l sni 'quickshell.service.sni.host.warning = false' # StatusNotifierItem warnings on reload set -l sni 'quickshell.service.sni.host.warning = false' # StatusNotifierItem warnings on reload
set -l process 'QProcess: Destroyed while process' # Long running processes on reload set -l process 'QProcess: Destroyed while process' # Long running processes on reload