#!/usr/bin/env python3 """Fake StatusNotifierItem for tray tests. usage: fake_tray.py name|path [ICON_NAME] name: owns org.kde.StatusNotifierItem--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 = """ """ MENU_XML = """ """ 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()