Fix tray DBus menu activation
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Services.SystemTray
|
import Quickshell.Services.SystemTray
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
@@ -10,6 +11,9 @@ BarWidget {
|
|||||||
|
|
||||||
property bool expanded: false
|
property bool expanded: false
|
||||||
property bool managePopupOpen: false
|
property bool managePopupOpen: false
|
||||||
|
property bool trayMenuOpen: false
|
||||||
|
property var activeTrayItem: null
|
||||||
|
property var activeTrayAnchor: null
|
||||||
readonly property color foreground: bar ? bar.foreground : Color.foreground
|
readonly property color foreground: bar ? bar.foreground : Color.foreground
|
||||||
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
|
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
|
||||||
readonly property var pinnedIds: Array.isArray(settings.pinned) ? settings.pinned : []
|
readonly property var pinnedIds: Array.isArray(settings.pinned) ? settings.pinned : []
|
||||||
@@ -19,8 +23,8 @@ BarWidget {
|
|||||||
readonly property var allItems: bucket("all")
|
readonly property var allItems: bucket("all")
|
||||||
readonly property int drawerCount: drawerItems.length
|
readonly property int drawerCount: drawerItems.length
|
||||||
readonly property int trayItemExtent: Style.space(16)
|
readonly property int trayItemExtent: Style.space(16)
|
||||||
readonly property int trayItemGap: Style.space(17)
|
readonly property int trayItemGap: Style.space(9)
|
||||||
readonly property int trayJoinGap: Style.space(6)
|
readonly property int trayJoinGap: Style.space(4)
|
||||||
readonly property int drawerExtent: drawerCount > 0 ? drawerCount * trayItemExtent + (drawerCount - 1) * trayItemGap : 0
|
readonly property int drawerExtent: drawerCount > 0 ? drawerCount * trayItemExtent + (drawerCount - 1) * trayItemGap : 0
|
||||||
// Match Waybar's group/tray-expander drawer transition-duration.
|
// Match Waybar's group/tray-expander drawer transition-duration.
|
||||||
readonly property int animationDuration: 600
|
readonly property int animationDuration: 600
|
||||||
@@ -29,6 +33,19 @@ BarWidget {
|
|||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
managePopupOpen = false
|
managePopupOpen = false
|
||||||
|
trayMenuOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTrayMenu(item, anchorItem, mouse) {
|
||||||
|
if (!item || !item.menu) {
|
||||||
|
var point = anchorItem.QsWindow.contentItem.mapFromItem(anchorItem, mouse.x, mouse.y)
|
||||||
|
item.display(anchorItem.QsWindow.window, point.x, point.y)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTrayItem = item
|
||||||
|
activeTrayAnchor = anchorItem
|
||||||
|
trayMenuOpen = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function trayIconSource(icon) {
|
function trayIconSource(icon) {
|
||||||
@@ -405,6 +422,136 @@ BarWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QsMenuOpener {
|
||||||
|
id: trayMenuOpener
|
||||||
|
menu: root.activeTrayItem ? root.activeTrayItem.menu : null
|
||||||
|
}
|
||||||
|
|
||||||
|
PopupCard {
|
||||||
|
id: trayMenuPopup
|
||||||
|
anchorItem: root.activeTrayAnchor || root
|
||||||
|
owner: root
|
||||||
|
bar: root.bar
|
||||||
|
open: root.trayMenuOpen
|
||||||
|
padding: Style.space(8)
|
||||||
|
borderColor: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.45)
|
||||||
|
contentWidth: trayMenuPopup.fittedContentWidth(Style.space(232))
|
||||||
|
contentHeight: trayMenuPopup.fittedContentHeight(trayMenuColumn.implicitHeight, Style.space(420))
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: trayMenuColumn
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: trayMenuOpener.children
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: menuRow
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
readonly property string rowText: String(modelData.text || "")
|
||||||
|
readonly property string activeTitle: root.activeTrayItem ? String(root.activeTrayItem.title || root.activeTrayItem.id || "") : ""
|
||||||
|
readonly property bool rootTitleEntry: index === 0 && modelData.hasChildren && rowText.toLowerCase() === activeTitle.toLowerCase()
|
||||||
|
readonly property bool leadingSeparator: modelData.isSeparator && index <= 1
|
||||||
|
readonly property bool hiddenRow: rootTitleEntry || leadingSeparator
|
||||||
|
|
||||||
|
visible: !hiddenRow
|
||||||
|
width: trayMenuColumn.width
|
||||||
|
implicitHeight: hiddenRow ? 0 : (modelData.isSeparator ? Style.space(11) : Style.space(30))
|
||||||
|
opacity: modelData.enabled ? 1.0 : 0.45
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: menuRow.modelData.isSeparator
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Style.space(10)
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Style.space(10)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
height: 1
|
||||||
|
color: Color.popups.border
|
||||||
|
opacity: 0.45
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: !menuRow.modelData.isSeparator
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Math.max(2, Style.cornerRadius)
|
||||||
|
color: rowMouse.containsMouse && menuRow.modelData.enabled ? Style.hoverFillFor(root.foreground, root.foreground) : "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: !menuRow.modelData.isSeparator && menuRow.modelData.buttonType !== QsMenuButtonType.None
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
width: Style.space(22)
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: menuRow.modelData.checkState === Qt.Checked ? "\uf00c" : ""
|
||||||
|
color: root.foreground
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: menuIcon
|
||||||
|
visible: !menuRow.modelData.isSeparator && String(menuRow.modelData.icon || "") !== ""
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Style.space(24)
|
||||||
|
implicitSize: Style.space(16)
|
||||||
|
width: Style.space(16)
|
||||||
|
height: Style.space(16)
|
||||||
|
source: menuRow.modelData.icon
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: !menuRow.modelData.isSeparator
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: menuIcon.visible ? Style.space(46) : Style.space(28)
|
||||||
|
anchors.right: submenuGlyph.left
|
||||||
|
anchors.rightMargin: Style.space(8)
|
||||||
|
text: menuRow.rowText
|
||||||
|
color: root.foreground
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: submenuGlyph
|
||||||
|
visible: !menuRow.modelData.isSeparator && menuRow.modelData.hasChildren
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Style.space(10)
|
||||||
|
text: "\u203a"
|
||||||
|
color: root.foreground
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: rowMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: !menuRow.modelData.isSeparator && menuRow.modelData.enabled
|
||||||
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: {
|
||||||
|
if (menuRow.modelData.hasChildren) {
|
||||||
|
var point = menuRow.QsWindow.contentItem.mapFromItem(menuRow, menuRow.width, menuRow.height / 2)
|
||||||
|
menuRow.modelData.display(menuRow.QsWindow.window, point.x, point.y)
|
||||||
|
} else {
|
||||||
|
menuRow.modelData.triggered()
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
component TrayItem: Item {
|
component TrayItem: Item {
|
||||||
id: trayItemRoot
|
id: trayItemRoot
|
||||||
|
|
||||||
@@ -414,6 +561,10 @@ BarWidget {
|
|||||||
implicitWidth: visible ? root.trayItemExtent : 0
|
implicitWidth: visible ? root.trayItemExtent : 0
|
||||||
implicitHeight: visible ? root.trayItemExtent : 0
|
implicitHeight: visible ? root.trayItemExtent : 0
|
||||||
|
|
||||||
|
function displayMenu(mouse) {
|
||||||
|
root.openTrayMenu(trayItemRoot.modelData, trayItemRoot, mouse)
|
||||||
|
}
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
implicitSize: Style.space(12)
|
implicitSize: Style.space(12)
|
||||||
@@ -430,12 +581,19 @@ BarWidget {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onEntered: if (root.bar) root.bar.showTooltip(trayItemRoot, root.trayTooltip(modelData))
|
onEntered: if (root.bar) root.bar.showTooltip(trayItemRoot, root.trayTooltip(modelData))
|
||||||
onExited: if (root.bar) root.bar.hideTooltip(trayItemRoot)
|
onExited: if (root.bar) root.bar.hideTooltip(trayItemRoot)
|
||||||
|
onPressed: function(mouse) {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
trayItemRoot.displayMenu(mouse)
|
||||||
|
mouse.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
onClicked: function(mouse) {
|
onClicked: function(mouse) {
|
||||||
if (mouse.button === Qt.RightButton && trayItemRoot.modelData.hasMenu) {
|
if (mouse.button === Qt.RightButton) {
|
||||||
var point = trayItemRoot.QsWindow.contentItem.mapFromItem(trayItemRoot, mouse.x, mouse.y)
|
mouse.accepted = true
|
||||||
trayItemRoot.modelData.display(trayItemRoot.QsWindow.window, point.x, point.y)
|
|
||||||
} else if (mouse.button === Qt.MiddleButton) {
|
} else if (mouse.button === Qt.MiddleButton) {
|
||||||
trayItemRoot.modelData.secondaryActivate()
|
trayItemRoot.modelData.secondaryActivate()
|
||||||
|
} else if (trayItemRoot.modelData.onlyMenu) {
|
||||||
|
trayItemRoot.displayMenu(mouse)
|
||||||
} else {
|
} else {
|
||||||
trayItemRoot.modelData.activate()
|
trayItemRoot.modelData.activate()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import dbus
|
||||||
|
import dbus.service
|
||||||
|
from dbus.mainloop.glib import DBusGMainLoop
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
|
||||||
|
ITEM_PATH = "/StatusNotifierItem"
|
||||||
|
MENU_PATH = "/StatusNotifierItem/Menu"
|
||||||
|
|
||||||
|
|
||||||
|
def variant(value):
|
||||||
|
return dbus.Variant(value)
|
||||||
|
|
||||||
|
|
||||||
|
class StatusNotifierItem(dbus.service.Object):
|
||||||
|
def __init__(self, bus):
|
||||||
|
super().__init__(bus, ITEM_PATH)
|
||||||
|
|
||||||
|
@dbus.service.method("org.freedesktop.DBus.Properties", in_signature="ss", out_signature="v")
|
||||||
|
def Get(self, interface, prop):
|
||||||
|
return variant(self.GetAll(interface)[prop])
|
||||||
|
|
||||||
|
@dbus.service.method("org.freedesktop.DBus.Properties", in_signature="s", out_signature="a{sv}")
|
||||||
|
def GetAll(self, interface):
|
||||||
|
if interface != "org.kde.StatusNotifierItem":
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return dbus.Dictionary({
|
||||||
|
"Category": "ApplicationStatus",
|
||||||
|
"Id": "omarchy-test-tray",
|
||||||
|
"Title": "omarchy-test-tray",
|
||||||
|
"Status": "Active",
|
||||||
|
"WindowId": dbus.Int32(0),
|
||||||
|
"IconName": "dialog-information",
|
||||||
|
"IconThemePath": "",
|
||||||
|
"Menu": dbus.ObjectPath(MENU_PATH),
|
||||||
|
"ItemIsMenu": dbus.Boolean(False),
|
||||||
|
"ToolTip": dbus.Struct((
|
||||||
|
"",
|
||||||
|
dbus.Array([], signature="(iiay)"),
|
||||||
|
"omarchy-test-tray",
|
||||||
|
"",
|
||||||
|
), signature=None),
|
||||||
|
}, signature="sv")
|
||||||
|
|
||||||
|
@dbus.service.method("org.freedesktop.DBus.Properties", in_signature="ssv")
|
||||||
|
def Set(self, interface, prop, value):
|
||||||
|
return
|
||||||
|
|
||||||
|
@dbus.service.method("org.kde.StatusNotifierItem", in_signature="ii")
|
||||||
|
def ContextMenu(self, x, y):
|
||||||
|
return
|
||||||
|
|
||||||
|
@dbus.service.method("org.kde.StatusNotifierItem", in_signature="ii")
|
||||||
|
def Activate(self, x, y):
|
||||||
|
return
|
||||||
|
|
||||||
|
@dbus.service.method("org.kde.StatusNotifierItem", in_signature="ii")
|
||||||
|
def SecondaryActivate(self, x, y):
|
||||||
|
return
|
||||||
|
|
||||||
|
@dbus.service.method("org.kde.StatusNotifierItem", in_signature="is")
|
||||||
|
def Scroll(self, delta, orientation):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class DBusMenu(dbus.service.Object):
|
||||||
|
def __init__(self, bus, event_path):
|
||||||
|
super().__init__(bus, MENU_PATH)
|
||||||
|
self.event_path = event_path
|
||||||
|
|
||||||
|
@dbus.service.method("org.freedesktop.DBus.Properties", in_signature="ss", out_signature="v")
|
||||||
|
def Get(self, interface, prop):
|
||||||
|
return variant(self.GetAll(interface)[prop])
|
||||||
|
|
||||||
|
@dbus.service.method("org.freedesktop.DBus.Properties", in_signature="s", out_signature="a{sv}")
|
||||||
|
def GetAll(self, interface):
|
||||||
|
if interface != "com.canonical.dbusmenu":
|
||||||
|
return {}
|
||||||
|
return dbus.Dictionary({
|
||||||
|
"Version": dbus.UInt32(3),
|
||||||
|
"TextDirection": "ltr",
|
||||||
|
"Status": "normal",
|
||||||
|
"IconThemePath": dbus.Array([], signature="s"),
|
||||||
|
}, signature="sv")
|
||||||
|
|
||||||
|
@dbus.service.method("org.freedesktop.DBus.Properties", in_signature="ssv")
|
||||||
|
def Set(self, interface, prop, value):
|
||||||
|
return
|
||||||
|
|
||||||
|
@dbus.service.method("com.canonical.dbusmenu", in_signature="i", out_signature="b")
|
||||||
|
def AboutToShow(self, item_id):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@dbus.service.method("com.canonical.dbusmenu", in_signature="ai", out_signature="aiai")
|
||||||
|
def AboutToShowGroup(self, item_ids):
|
||||||
|
return ([], [])
|
||||||
|
|
||||||
|
@dbus.service.method("com.canonical.dbusmenu", in_signature="iias", out_signature="u(ia{sv}av)")
|
||||||
|
def GetLayout(self, parent_id, recursion_depth, property_names):
|
||||||
|
print("GetLayout", int(parent_id), int(recursion_depth), list(property_names), flush=True)
|
||||||
|
child = (
|
||||||
|
dbus.Int32(2),
|
||||||
|
dbus.Dictionary({"label": "Sign in", "enabled": True}, signature="sv"),
|
||||||
|
dbus.Array([], signature="v"),
|
||||||
|
)
|
||||||
|
root = (
|
||||||
|
dbus.Int32(0),
|
||||||
|
dbus.Dictionary({"children-display": "submenu"}, signature="sv"),
|
||||||
|
dbus.Array([dbus.Struct(child, signature=None, variant_level=1)], signature="v"),
|
||||||
|
)
|
||||||
|
return (dbus.UInt32(1), dbus.Struct(root, signature=None))
|
||||||
|
|
||||||
|
@dbus.service.method("com.canonical.dbusmenu", in_signature="aias", out_signature="a(ia{sv})")
|
||||||
|
def GetGroupProperties(self, item_ids, property_names):
|
||||||
|
return []
|
||||||
|
|
||||||
|
@dbus.service.method("com.canonical.dbusmenu", in_signature="is", out_signature="v")
|
||||||
|
def GetProperty(self, item_id, name):
|
||||||
|
return variant("")
|
||||||
|
|
||||||
|
@dbus.service.method("com.canonical.dbusmenu", in_signature="isvu")
|
||||||
|
def Event(self, item_id, event_id, data, timestamp):
|
||||||
|
print("Event", int(item_id), str(event_id), flush=True)
|
||||||
|
if int(item_id) == 2 and str(event_id) == "clicked":
|
||||||
|
with open(self.event_path, "w", encoding="utf-8") as handle:
|
||||||
|
handle.write("clicked\n")
|
||||||
|
|
||||||
|
@dbus.service.method("com.canonical.dbusmenu", in_signature="a(isvu)", out_signature="ai")
|
||||||
|
def EventGroup(self, events):
|
||||||
|
for item_id, event_id, data, timestamp in events:
|
||||||
|
self.Event(item_id, event_id, data, timestamp)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def register_with_watcher(bus):
|
||||||
|
watcher = dbus.Interface(
|
||||||
|
bus.get_object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"),
|
||||||
|
"org.kde.StatusNotifierWatcher",
|
||||||
|
)
|
||||||
|
watcher.RegisterStatusNotifierItem(ITEM_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
return run()
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
event_path = os.environ["OMARCHY_TRAY_MENU_EVENT_RESULT"]
|
||||||
|
ready_path = os.environ["OMARCHY_TRAY_MENU_READY"]
|
||||||
|
|
||||||
|
DBusGMainLoop(set_as_default=True)
|
||||||
|
bus = dbus.SessionBus()
|
||||||
|
dbus.service.BusName("org.omarchy.TestStatusNotifier", bus)
|
||||||
|
|
||||||
|
StatusNotifierItem(bus)
|
||||||
|
DBusMenu(bus, event_path)
|
||||||
|
|
||||||
|
for _ in range(50):
|
||||||
|
try:
|
||||||
|
register_with_watcher(bus)
|
||||||
|
with open(ready_path, "w", encoding="utf-8") as handle:
|
||||||
|
handle.write("ready\n")
|
||||||
|
break
|
||||||
|
except dbus.DBusException:
|
||||||
|
time.sleep(0.1)
|
||||||
|
else:
|
||||||
|
print("timed out registering StatusNotifierItem", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
GLib.MainLoop().run()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.SystemTray
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string resultPath: Quickshell.env("OMARCHY_QML_TEST_RESULT")
|
||||||
|
property var trayItem: null
|
||||||
|
property int attempts: 0
|
||||||
|
property string lastState: ""
|
||||||
|
|
||||||
|
function shellQuote(value) {
|
||||||
|
return "'" + String(value).replace(/'/g, "'\\''") + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeResult(ok, message) {
|
||||||
|
var payload = JSON.stringify({ ok: ok, message: String(message || "") })
|
||||||
|
if (resultPath) {
|
||||||
|
Quickshell.execDetached(["bash", "-lc", "printf '%s' " + shellQuote(payload) + " > " + shellQuote(resultPath)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTrayItem() {
|
||||||
|
var values = SystemTray.items.values
|
||||||
|
var ids = []
|
||||||
|
for (var i = 0; i < values.length; i++) {
|
||||||
|
ids.push(String(values[i].id || ""))
|
||||||
|
if (String(values[i].id || "") === "omarchy-test-tray") return values[i]
|
||||||
|
}
|
||||||
|
lastState = "items=" + ids.join(",")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerSignIn() {
|
||||||
|
var rows = menuOpener.children.values
|
||||||
|
var labels = []
|
||||||
|
for (var i = 0; i < rows.length; i++) {
|
||||||
|
var row = rows[i]
|
||||||
|
labels.push(String(row.text || ""))
|
||||||
|
if (!row.isSeparator && String(row.text || "") === "Sign in") {
|
||||||
|
row.triggered()
|
||||||
|
writeResult(true, "triggered Sign in")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastState = "item=" + String(root.trayItem ? root.trayItem.id : "") + " rows=" + labels.join(",")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
QsMenuOpener {
|
||||||
|
id: menuOpener
|
||||||
|
menu: root.trayItem ? root.trayItem.menu : null
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 100
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
root.attempts++
|
||||||
|
|
||||||
|
if (!root.trayItem) root.trayItem = root.findTrayItem()
|
||||||
|
if (root.trayItem && root.triggerSignIn()) {
|
||||||
|
stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.attempts >= 50) {
|
||||||
|
root.writeResult(false, "timed out waiting for tray menu Sign in row; " + root.lastState)
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||||
|
|
||||||
|
TMPDIR=""
|
||||||
|
QS_PID=""
|
||||||
|
MOCK_PID=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n $MOCK_PID ]] && kill -0 "$MOCK_PID" 2>/dev/null; then
|
||||||
|
kill "$MOCK_PID" 2>/dev/null || true
|
||||||
|
wait "$MOCK_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [[ -n $QS_PID ]] && kill -0 "$QS_PID" 2>/dev/null; then
|
||||||
|
kill "$QS_PID" 2>/dev/null || true
|
||||||
|
wait "$QS_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
[[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
if ! command -v quickshell >/dev/null 2>&1; then
|
||||||
|
pass "quickshell not installed; skipping tray menu activation test"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_command jq
|
||||||
|
require_command python
|
||||||
|
|
||||||
|
python - <<'PY' || {
|
||||||
|
import dbus
|
||||||
|
import gi
|
||||||
|
PY
|
||||||
|
pass "python DBus bindings unavailable; skipping tray menu activation test"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
config_dir="$TMPDIR/tray-menu-activation"
|
||||||
|
result="$TMPDIR/result.json"
|
||||||
|
event_result="$TMPDIR/event"
|
||||||
|
ready="$TMPDIR/ready"
|
||||||
|
qs_log="$TMPDIR/quickshell.log"
|
||||||
|
mock_log="$TMPDIR/mock-sni.log"
|
||||||
|
mkdir -p "$config_dir" "$TMPDIR/home"
|
||||||
|
cp "$SHELL_TEST_DIR/fixtures/tray-menu-activation/shell.qml" "$config_dir/shell.qml"
|
||||||
|
|
||||||
|
OMARCHY_PATH="$ROOT" \
|
||||||
|
OMARCHY_QML_TEST_RESULT="$result" \
|
||||||
|
HOME="$TMPDIR/home" \
|
||||||
|
quickshell -p "$config_dir" --no-color >"$qs_log" 2>&1 &
|
||||||
|
QS_PID=$!
|
||||||
|
|
||||||
|
OMARCHY_TRAY_MENU_EVENT_RESULT="$event_result" \
|
||||||
|
OMARCHY_TRAY_MENU_READY="$ready" \
|
||||||
|
python "$SHELL_TEST_DIR/fixtures/tray-menu-activation/mock-sni.py" >"$mock_log" 2>&1 &
|
||||||
|
MOCK_PID=$!
|
||||||
|
|
||||||
|
for _ in {1..80}; do
|
||||||
|
[[ -s $event_result ]] && break
|
||||||
|
|
||||||
|
if ! kill -0 "$QS_PID" 2>/dev/null; then
|
||||||
|
sed -n '1,160p' "$qs_log" >&2
|
||||||
|
fail "tray menu activation quickshell exited before triggering menu item"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! kill -0 "$MOCK_PID" 2>/dev/null; then
|
||||||
|
sed -n '1,160p' "$mock_log" >&2
|
||||||
|
fail "mock StatusNotifierItem exited before receiving menu event"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -s $event_result ]]; then
|
||||||
|
[[ -s $result ]] && jq . "$result" >&2
|
||||||
|
printf 'Quickshell log:\n' >&2
|
||||||
|
sed -n '1,160p' "$qs_log" >&2
|
||||||
|
printf 'Mock SNI log:\n' >&2
|
||||||
|
sed -n '1,160p' "$mock_log" >&2
|
||||||
|
fail "tray menu sends DBusMenu clicked event"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pass "tray menu sends DBusMenu clicked event"
|
||||||
Reference in New Issue
Block a user