Tray, fullscreen-only hiding, peeks, Ethernet details, wider maps
- tray.rs: the shell serves org.kde.StatusNotifierWatcher (only when no other watcher runs) and is its host; icons from the theme or from IconPixmap (png.rs, a stdlib png writer); left/middle/right click and scroll; dbusmenu menus drawn in the island's look; tray IPC verbs for testing - edge.rs: the bar hides only on monitors showing a fullscreen window, driven by Hyprland's event socket; peeks of the workspaces (with the disc sliding) and the island (song change, new player), with a pop on the island - prompt.rs: Ethernet details popup (addresses, gateway, DNS, MAC copyable) - map.rs: 1100x560 maps, every other node a ring further in - tools/fake_tray.py: a fake tray app, by bus name or by path
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fake StatusNotifierItem for tray tests.
|
||||
|
||||
usage: fake_tray.py name|path [ICON_NAME]
|
||||
name: owns org.kde.StatusNotifierItem-<pid>-1, item at /StatusNotifierItem, IconName
|
||||
path: registers its object path only (libappindicator style), IconPixmap pixels
|
||||
Every call it receives is printed (Activate, ContextMenu, Scroll, menu Event...).
|
||||
A menu with a checkmark, a separator, a submenu and a disabled entry.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
MODE = sys.argv[1] if len(sys.argv) > 1 else "name"
|
||||
ICON = sys.argv[2] if len(sys.argv) > 2 else "audio-volume-high"
|
||||
PATH = "/StatusNotifierItem" if MODE == "name" else "/org/ayatana/NotificationItem/fake_tray"
|
||||
NAME = f"org.kde.StatusNotifierItem-{os.getpid()}-1"
|
||||
|
||||
ITEM_XML = """<node><interface name="org.kde.StatusNotifierItem">
|
||||
<property name="Category" type="s" access="read"/><property name="Id" type="s" access="read"/>
|
||||
<property name="Title" type="s" access="read"/><property name="Status" type="s" access="read"/>
|
||||
<property name="IconName" type="s" access="read"/><property name="IconPixmap" type="a(iiay)" access="read"/>
|
||||
<property name="IconThemePath" type="s" access="read"/><property name="ItemIsMenu" type="b" access="read"/>
|
||||
<property name="Menu" type="o" access="read"/>
|
||||
<method name="Activate"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
|
||||
<method name="SecondaryActivate"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
|
||||
<method name="ContextMenu"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
|
||||
<method name="Scroll"><arg type="i" direction="in"/><arg type="s" direction="in"/></method>
|
||||
<signal name="NewIcon"/><signal name="NewStatus"><arg type="s"/></signal>
|
||||
</interface></node>"""
|
||||
|
||||
MENU_XML = """<node><interface name="com.canonical.dbusmenu">
|
||||
<method name="GetLayout"><arg type="i" direction="in"/><arg type="i" direction="in"/><arg type="as" direction="in"/>
|
||||
<arg type="u" direction="out"/><arg type="(ia{sv}av)" direction="out"/></method>
|
||||
<method name="AboutToShow"><arg type="i" direction="in"/><arg type="b" direction="out"/></method>
|
||||
<method name="Event"><arg type="i" direction="in"/><arg type="s" direction="in"/><arg type="v" direction="in"/><arg type="u" direction="in"/></method>
|
||||
</interface></node>"""
|
||||
|
||||
TITLE = "Fake Tray " + MODE
|
||||
muted = [True]
|
||||
|
||||
|
||||
def entry(id, props, children=()):
|
||||
return GLib.Variant("(ia{sv}av)", (id, props, [GLib.Variant("v", child) for child in children]))
|
||||
|
||||
|
||||
def menu(parent):
|
||||
sub = [
|
||||
entry(31, {"label": GLib.Variant("s", "Sub _A")}),
|
||||
entry(32, {"label": GLib.Variant("s", "Sub B")}),
|
||||
]
|
||||
top = [
|
||||
entry(1, {"label": GLib.Variant("s", "_Open Window")}),
|
||||
entry(2, {"type": GLib.Variant("s", "separator")}),
|
||||
entry(3, {"label": GLib.Variant("s", "Mute"), "toggle-type": GLib.Variant("s", "checkmark"), "toggle-state": GLib.Variant("i", 1 if muted[0] else 0)}),
|
||||
entry(4, {"label": GLib.Variant("s", "More"), "children-display": GLib.Variant("s", "submenu")}, sub),
|
||||
entry(5, {"label": GLib.Variant("s", "Disabled thing"), "enabled": GLib.Variant("b", False)}),
|
||||
entry(6, {"label": GLib.Variant("s", "Hidden"), "visible": GLib.Variant("b", False)}),
|
||||
entry(7, {"label": GLib.Variant("s", "Quit")}),
|
||||
]
|
||||
children = sub if parent == 4 else top
|
||||
return GLib.Variant("(u(ia{sv}av))", (1, (parent, {"children-display": GLib.Variant("s", "submenu")}, [GLib.Variant("v", child) for child in children])))
|
||||
|
||||
|
||||
def pixmap():
|
||||
size = 24
|
||||
data = bytearray()
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
inside = (x - 11.5) ** 2 + (y - 11.5) ** 2 < 100
|
||||
data += bytes([255 if inside else 0, 60, 200, 120]) if inside else bytes([0, 0, 0, 0])
|
||||
return [(size, size, bytes(data))]
|
||||
|
||||
|
||||
def item_property(conn, sender, path, iface, name):
|
||||
print("get", name, flush=True)
|
||||
values = {
|
||||
"Category": GLib.Variant("s", "ApplicationStatus"),
|
||||
"Id": GLib.Variant("s", "fake_tray_" + MODE),
|
||||
"Title": GLib.Variant("s", TITLE),
|
||||
"Status": GLib.Variant("s", "Active"),
|
||||
"IconName": GLib.Variant("s", ICON if MODE == "name" else ""),
|
||||
"IconPixmap": GLib.Variant("a(iiay)", pixmap() if MODE == "path" else []),
|
||||
"IconThemePath": GLib.Variant("s", ""),
|
||||
"ItemIsMenu": GLib.Variant("b", False),
|
||||
"Menu": GLib.Variant("o", "/MenuBar"),
|
||||
}
|
||||
return values.get(name)
|
||||
|
||||
|
||||
def item_call(conn, sender, path, iface, method, params, invocation):
|
||||
print("item", method, params.unpack(), flush=True)
|
||||
invocation.return_value(None)
|
||||
|
||||
|
||||
def menu_call(conn, sender, path, iface, method, params, invocation):
|
||||
args = params.unpack()
|
||||
print("menu", method, args, flush=True)
|
||||
if method == "GetLayout":
|
||||
invocation.return_value(menu(args[0]))
|
||||
elif method == "AboutToShow":
|
||||
invocation.return_value(GLib.Variant("(b)", (False,)))
|
||||
else:
|
||||
if method == "Event" and args[0] == 3:
|
||||
muted[0] = not muted[0]
|
||||
if method == "Event" and args[0] == 7:
|
||||
GLib.idle_add(loop.quit)
|
||||
invocation.return_value(None)
|
||||
|
||||
|
||||
bus = Gio.bus_get_sync(Gio.BusType.SESSION)
|
||||
bus.register_object(PATH, Gio.DBusNodeInfo.new_for_xml(ITEM_XML).interfaces[0], item_call, item_property, None)
|
||||
bus.register_object("/MenuBar", Gio.DBusNodeInfo.new_for_xml(MENU_XML).interfaces[0], menu_call, None, None)
|
||||
|
||||
|
||||
def register(*_):
|
||||
service = NAME if MODE == "name" else PATH
|
||||
try:
|
||||
bus.call_sync("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher", "org.kde.StatusNotifierWatcher",
|
||||
"RegisterStatusNotifierItem", GLib.Variant("(s)", (service,)), None, 0, 3000, None)
|
||||
print("registered", service, flush=True)
|
||||
except GLib.Error as error:
|
||||
print("no watcher:", error.message, flush=True)
|
||||
|
||||
|
||||
if MODE == "name":
|
||||
Gio.bus_own_name_on_connection(bus, NAME, 0, None, None)
|
||||
|
||||
# like the real libraries: register again whenever a watcher appears
|
||||
Gio.bus_watch_name_on_connection(bus, "org.kde.StatusNotifierWatcher", 0, register, None)
|
||||
|
||||
loop = GLib.MainLoop()
|
||||
loop.run()
|
||||
Reference in New Issue
Block a user