add system tray module
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
|||||||
/.idea
|
/.idea
|
||||||
/venv
|
/venv
|
||||||
/nwg-panel.egg-info/
|
/nwg-panel.egg-info/
|
||||||
|
/nwg_panel.egg-info/
|
||||||
/build/
|
/build/
|
||||||
/dist/
|
/dist/
|
||||||
|
__pycache__
|
@@ -13,6 +13,7 @@ taskbars_list = []
|
|||||||
scratchpads_list = []
|
scratchpads_list = []
|
||||||
workspaces_list = []
|
workspaces_list = []
|
||||||
controls_list = []
|
controls_list = []
|
||||||
|
tray_list = []
|
||||||
config_dir = ""
|
config_dir = ""
|
||||||
dwl_data_file = None
|
dwl_data_file = None
|
||||||
dwl_instances = []
|
dwl_instances = []
|
||||||
|
@@ -50,6 +50,7 @@ from nwg_panel.modules.menu_start import MenuStart
|
|||||||
dir_name = os.path.dirname(__file__)
|
dir_name = os.path.dirname(__file__)
|
||||||
|
|
||||||
from nwg_panel import common
|
from nwg_panel import common
|
||||||
|
from nwg_panel.modules import sni_system_tray
|
||||||
|
|
||||||
sway = os.getenv('SWAYSOCK') is not None
|
sway = os.getenv('SWAYSOCK') is not None
|
||||||
if sway:
|
if sway:
|
||||||
@@ -72,6 +73,7 @@ def signal_handler(sig, frame):
|
|||||||
desc = {2: "SIGINT", 15: "SIGTERM", 10: "SIGUSR1"}
|
desc = {2: "SIGINT", 15: "SIGTERM", 10: "SIGUSR1"}
|
||||||
if sig == 2 or sig == 15:
|
if sig == 2 or sig == 15:
|
||||||
print("Terminated with {}".format(desc[sig]))
|
print("Terminated with {}".format(desc[sig]))
|
||||||
|
sni_system_tray.deinit_tray()
|
||||||
Gtk.main_quit()
|
Gtk.main_quit()
|
||||||
elif sig == sig_dwl:
|
elif sig == sig_dwl:
|
||||||
refresh_dwl()
|
refresh_dwl()
|
||||||
@@ -218,6 +220,14 @@ def instantiate_content(panel, container, content_list, icons_path=""):
|
|||||||
else:
|
else:
|
||||||
print("{} data file not found".format(common.dwl_data_file))
|
print("{} data file not found".format(common.dwl_data_file))
|
||||||
|
|
||||||
|
if item == "tray":
|
||||||
|
tray_settings = {}
|
||||||
|
if "tray-settings" in panel:
|
||||||
|
tray_settings = panel["tray-settings"]
|
||||||
|
tray = sni_system_tray.Tray(tray_settings, icons_path)
|
||||||
|
common.tray_list.append(tray)
|
||||||
|
container.pack_start(tray, False, False, panel["items-padding"])
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
common.config_dir = get_config_dir()
|
common.config_dir = get_config_dir()
|
||||||
@@ -540,6 +550,9 @@ def main():
|
|||||||
common.outputs_num = len(common.outputs)
|
common.outputs_num = len(common.outputs)
|
||||||
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 200, check_tree)
|
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 200, check_tree)
|
||||||
|
|
||||||
|
if len(common.tray_list) > 0:
|
||||||
|
sni_system_tray.init_tray(common.tray_list)
|
||||||
|
|
||||||
Gtk.main()
|
Gtk.main()
|
||||||
|
|
||||||
|
|
||||||
|
20
nwg_panel/modules/sni_system_tray/__init__.py
Normal file
20
nwg_panel/modules/sni_system_tray/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import typing
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from . import host, watcher
|
||||||
|
from .tray import Tray
|
||||||
|
|
||||||
|
|
||||||
|
def init_tray(trays: typing.List[Tray]):
|
||||||
|
host_thread = Thread(target=host.init, args=[0, trays])
|
||||||
|
host_thread.daemon = True
|
||||||
|
host_thread.start()
|
||||||
|
|
||||||
|
watcher_thread = Thread(target=watcher.init)
|
||||||
|
watcher_thread.daemon = True
|
||||||
|
watcher_thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
def deinit_tray():
|
||||||
|
host.deinit()
|
||||||
|
watcher.deinit()
|
128
nwg_panel/modules/sni_system_tray/host.py
Normal file
128
nwg_panel/modules/sni_system_tray/host.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import typing
|
||||||
|
import os
|
||||||
|
|
||||||
|
from dasbus.connection import SessionMessageBus
|
||||||
|
from dasbus.loop import EventLoop
|
||||||
|
from dasbus.client.observer import DBusObserver
|
||||||
|
from dasbus.client.proxy import disconnect_proxy
|
||||||
|
|
||||||
|
from .watcher import WATCHER_SERVICE_NAME, WATCHER_OBJECT_PATH
|
||||||
|
from .tray import Tray
|
||||||
|
from .item import StatusNotifierItem
|
||||||
|
|
||||||
|
HOST_SERVICE_NAME_TEMPLATE = "org.kde.StatusNotifierHost-{}-{}"
|
||||||
|
HOST_OBJECT_PATH_TEMPLATE = "/StatusNotifierHost/{}"
|
||||||
|
|
||||||
|
dasbus_event_loop: typing.Union[EventLoop, None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_service_name_and_object_path(service: str) -> (str, str):
|
||||||
|
index = service.find("/")
|
||||||
|
if index != len(service):
|
||||||
|
return service[0:index], service[index:]
|
||||||
|
return service, "/StatusNotifierItem"
|
||||||
|
|
||||||
|
|
||||||
|
class StatusNotifierHostInterface(object):
|
||||||
|
def __init__(self, host_id, trays: typing.List[Tray]):
|
||||||
|
self.host_id = host_id
|
||||||
|
self.trays = trays
|
||||||
|
|
||||||
|
self._statusNotifierItems = []
|
||||||
|
self.watcher_proxy = None
|
||||||
|
self.session_bus = SessionMessageBus()
|
||||||
|
|
||||||
|
self.host_service_name = HOST_SERVICE_NAME_TEMPLATE.format(os.getpid(), self.host_id)
|
||||||
|
self.host_object_path = HOST_OBJECT_PATH_TEMPLATE.format(self.host_id)
|
||||||
|
self.session_bus.register_service(self.host_service_name)
|
||||||
|
|
||||||
|
self.watcher_service_observer = DBusObserver(
|
||||||
|
message_bus=self.session_bus,
|
||||||
|
service_name=WATCHER_SERVICE_NAME
|
||||||
|
)
|
||||||
|
self.watcher_service_observer.service_available.connect(
|
||||||
|
self.watcher_available_handler
|
||||||
|
)
|
||||||
|
self.watcher_service_observer.service_unavailable.connect(
|
||||||
|
self.watcher_unavailable_handler
|
||||||
|
)
|
||||||
|
self.watcher_service_observer.connect_once_available()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self.watcher_proxy is not None:
|
||||||
|
disconnect_proxy(self.watcher_proxy)
|
||||||
|
self.watcher_service_observer.disconnect()
|
||||||
|
self.session_bus.disconnect()
|
||||||
|
|
||||||
|
def watcher_available_handler(self, _observer):
|
||||||
|
print("StatusNotifierHostInterface -> watcher_available_handler")
|
||||||
|
self.watcher_proxy = self.session_bus.get_proxy(WATCHER_SERVICE_NAME, WATCHER_OBJECT_PATH)
|
||||||
|
self.watcher_proxy.StatusNotifierItemRegistered.connect(self.item_registered_handler)
|
||||||
|
self.watcher_proxy.StatusNotifierItemUnregistered.connect(self.item_unregistered_handler)
|
||||||
|
self.watcher_proxy.RegisterStatusNotifierHost(self.host_object_path, callback=lambda _: None)
|
||||||
|
|
||||||
|
def watcher_unavailable_handler(self, _observer):
|
||||||
|
print("StatusNotifierHostInterface -> watcher_unavailable_handler")
|
||||||
|
self._statusNotifierItems.clear()
|
||||||
|
disconnect_proxy(self.watcher_proxy)
|
||||||
|
self.watcher_proxy = None
|
||||||
|
|
||||||
|
def item_registered_handler(self, full_service_service):
|
||||||
|
print(
|
||||||
|
"StatusNotifierHostInterface -> item_registered_handler\n full_service_name: {}".format(
|
||||||
|
full_service_service
|
||||||
|
)
|
||||||
|
)
|
||||||
|
service_name, object_path = get_service_name_and_object_path(full_service_service)
|
||||||
|
if self.find_item(service_name, object_path) is None:
|
||||||
|
item = StatusNotifierItem(service_name, object_path)
|
||||||
|
item.set_on_loaded_callback(self.item_loaded_handler)
|
||||||
|
item.set_on_updated_callback(self.item_updated_handler)
|
||||||
|
self._statusNotifierItems.append(item)
|
||||||
|
|
||||||
|
def item_unregistered_handler(self, full_service_service):
|
||||||
|
print(
|
||||||
|
"StatusNotifierHostInterface -> item_unregistered_handler\n full_service_name: {}".format(
|
||||||
|
full_service_service
|
||||||
|
)
|
||||||
|
)
|
||||||
|
service_name, object_path = get_service_name_and_object_path(full_service_service)
|
||||||
|
item = self.find_item(service_name, object_path)
|
||||||
|
if item is not None:
|
||||||
|
self._statusNotifierItems.remove(item)
|
||||||
|
for tray in self.trays:
|
||||||
|
tray.remove_item(item)
|
||||||
|
|
||||||
|
def find_item(self, service_name, object_path) -> typing.Union[StatusNotifierItem, None]:
|
||||||
|
for item in self._statusNotifierItems:
|
||||||
|
if item.service_name == service_name and item.object_path == object_path:
|
||||||
|
return item
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def item_loaded_handler(self, item):
|
||||||
|
for tray in self.trays:
|
||||||
|
tray.add_item(item)
|
||||||
|
|
||||||
|
def item_updated_handler(self, item, changed_properties):
|
||||||
|
for tray in self.trays:
|
||||||
|
tray.update_item(item, changed_properties)
|
||||||
|
|
||||||
|
|
||||||
|
def init(host_id, trays: typing.List[Tray]):
|
||||||
|
_status_notifier_host_interface = StatusNotifierHostInterface(host_id, trays)
|
||||||
|
|
||||||
|
global dasbus_event_loop
|
||||||
|
if dasbus_event_loop is None:
|
||||||
|
print("host.init(): running dasbus.EventLoop")
|
||||||
|
dasbus_event_loop = EventLoop()
|
||||||
|
dasbus_event_loop.run()
|
||||||
|
|
||||||
|
|
||||||
|
def deinit():
|
||||||
|
global dasbus_event_loop
|
||||||
|
if dasbus_event_loop is not None:
|
||||||
|
print("host.deinit(): quitting dasbus.EventLoop")
|
||||||
|
dasbus_event_loop.quit()
|
||||||
|
if dasbus_event_loop is not None:
|
||||||
|
dasbus_event_loop = None
|
96
nwg_panel/modules/sni_system_tray/item.py
Normal file
96
nwg_panel/modules/sni_system_tray/item.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
from dasbus.connection import SessionMessageBus
|
||||||
|
from dasbus.client.observer import DBusObserver
|
||||||
|
from dasbus.client.proxy import disconnect_proxy
|
||||||
|
|
||||||
|
PROPERTIES = [
|
||||||
|
"Id",
|
||||||
|
"Category",
|
||||||
|
"Title",
|
||||||
|
"Status",
|
||||||
|
"WindowId",
|
||||||
|
"IconName",
|
||||||
|
"IconPixmap",
|
||||||
|
"OverlayIconName",
|
||||||
|
"OverlayIconPixmap",
|
||||||
|
"AttentionIconName",
|
||||||
|
"AttentionIconPixmap",
|
||||||
|
"AttentionMovieName",
|
||||||
|
"ToolTip",
|
||||||
|
"IconThemePath",
|
||||||
|
"ItemIsMenu",
|
||||||
|
"Menu"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class StatusNotifierItem(object):
|
||||||
|
def __init__(self, service_name, object_path):
|
||||||
|
self.service_name = service_name
|
||||||
|
self.object_path = object_path
|
||||||
|
self.on_loaded_callback = None
|
||||||
|
self.on_updated_callback = None
|
||||||
|
self.session_bus = SessionMessageBus()
|
||||||
|
self.properties = {
|
||||||
|
"ItemIsMenu": True
|
||||||
|
}
|
||||||
|
self.item_proxy = None
|
||||||
|
|
||||||
|
self.item_observer = DBusObserver(
|
||||||
|
message_bus=self.session_bus,
|
||||||
|
service_name=self.service_name
|
||||||
|
)
|
||||||
|
self.item_observer.service_available.connect(
|
||||||
|
self.item_available_handler
|
||||||
|
)
|
||||||
|
self.item_observer.service_unavailable.connect(
|
||||||
|
self.item_unavailable_handler
|
||||||
|
)
|
||||||
|
self.item_observer.connect_once_available()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self.item_proxy is not None:
|
||||||
|
disconnect_proxy(self.item_proxy)
|
||||||
|
self.item_observer.disconnect()
|
||||||
|
self.session_bus.disconnect()
|
||||||
|
|
||||||
|
def item_available_handler(self, _observer):
|
||||||
|
self.item_proxy = self.session_bus.get_proxy(self.service_name, self.object_path)
|
||||||
|
self.item_proxy.PropertiesChanged.connect(
|
||||||
|
lambda _if, changed_properties, _invalid: self.change_handler(list(changed_properties))
|
||||||
|
)
|
||||||
|
self.item_proxy.NewTitle.connect(
|
||||||
|
lambda _title: self.change_handler(["Title"])
|
||||||
|
)
|
||||||
|
self.item_proxy.NewIcon.connect(
|
||||||
|
lambda _icon_name, _icon_pixmap: self.change_handler(["IconName", "IconPixmap"])
|
||||||
|
)
|
||||||
|
self.item_proxy.NewAttentionIcon.connect(
|
||||||
|
lambda _icon_name, _icon_pixmap: self.change_handler(["AttentionIconName", "AttentionIconPixmap"])
|
||||||
|
)
|
||||||
|
self.item_proxy.NewIconThemePath.connect(
|
||||||
|
lambda _icon_theme_path: self.change_handler(["IconThemePath"])
|
||||||
|
)
|
||||||
|
self.item_proxy.NewStatus.connect(
|
||||||
|
lambda _status: self.change_handler(["Status"])
|
||||||
|
)
|
||||||
|
for name in PROPERTIES:
|
||||||
|
if hasattr(self.item_proxy, name):
|
||||||
|
self.properties[name] = getattr(self.item_proxy, name)
|
||||||
|
if self.on_loaded_callback is not None:
|
||||||
|
self.on_loaded_callback(self)
|
||||||
|
|
||||||
|
def item_unavailable_handler(self, _observer):
|
||||||
|
disconnect_proxy(self.item_proxy)
|
||||||
|
self.item_proxy = None
|
||||||
|
|
||||||
|
def change_handler(self, changed_properties: list[str]):
|
||||||
|
if len(changed_properties) > 0:
|
||||||
|
for name, value in changed_properties:
|
||||||
|
self.properties[name] = value
|
||||||
|
if self.on_updated_callback is not None:
|
||||||
|
self.on_updated_callback(self, changed_properties)
|
||||||
|
|
||||||
|
def set_on_loaded_callback(self, callback):
|
||||||
|
self.on_loaded_callback = callback
|
||||||
|
|
||||||
|
def set_on_updated_callback(self, callback):
|
||||||
|
self.on_updated_callback = callback
|
135
nwg_panel/modules/sni_system_tray/tray.py
Normal file
135
nwg_panel/modules/sni_system_tray/tray.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk, GLib, GdkPixbuf
|
||||||
|
|
||||||
|
from nwg_panel.tools import check_key, get_config_dir
|
||||||
|
from .item import StatusNotifierItem
|
||||||
|
|
||||||
|
|
||||||
|
def load_icon(image, icon_name, icon_size, icons_path=""):
|
||||||
|
icon_theme = Gtk.IconTheme.get_default()
|
||||||
|
search_path = icon_theme.get_search_path()
|
||||||
|
if icons_path:
|
||||||
|
search_path.append(icons_path)
|
||||||
|
icon_theme.set_search_path(search_path)
|
||||||
|
if icon_theme.has_icon(icon_name):
|
||||||
|
pixbuf = icon_theme.load_icon(icon_name, icon_size, Gtk.IconLookupFlags.FORCE_SIZE)
|
||||||
|
elif icon_theme.has_icon(icon_name.lower()):
|
||||||
|
pixbuf = icon_theme.load_icon(icon_name.lower(), icon_size, Gtk.IconLookupFlags.FORCE_SIZE)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
pixbuf = icon_theme.load_icon(icon_name, icon_size, Gtk.IconLookupFlags.FORCE_SIZE)
|
||||||
|
except GLib.GError:
|
||||||
|
print(
|
||||||
|
"tray.update_icon -> icon not found\n icon_name: {}\n search_path: {}".format(
|
||||||
|
icon_name,
|
||||||
|
search_path
|
||||||
|
),
|
||||||
|
file=sys.stderr
|
||||||
|
)
|
||||||
|
path = os.path.join(get_config_dir(), "icons_light/icon-missing.svg")
|
||||||
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(path, icon_size, icon_size)
|
||||||
|
# TODO: if image height is different to icon_size, resize to match, while maintaining
|
||||||
|
# aspect ratio. Width can be ignored.
|
||||||
|
image.set_from_pixbuf(pixbuf)
|
||||||
|
|
||||||
|
|
||||||
|
def update_icon(image, item, icon_size, icon_path):
|
||||||
|
if "IconThemePath" in item.properties:
|
||||||
|
icon_path = item.properties["IconThemePath"]
|
||||||
|
load_icon(image, item.properties["IconName"], icon_size, icon_path)
|
||||||
|
|
||||||
|
|
||||||
|
def update_status(event_box, item):
|
||||||
|
if "Status" in item.properties:
|
||||||
|
status = item.properties["Status"].lower()
|
||||||
|
event_box.set_visible(status != "passive")
|
||||||
|
event_box_style = event_box.get_style_context()
|
||||||
|
for class_name in event_box_style.list_classes():
|
||||||
|
event_box_style.remove_class(class_name)
|
||||||
|
if status == "needsattention":
|
||||||
|
event_box_style.add_class("needs-attention")
|
||||||
|
event_box_style.add_class(status)
|
||||||
|
|
||||||
|
|
||||||
|
class Tray(Gtk.EventBox):
|
||||||
|
def __init__(self, settings, icons_path=""):
|
||||||
|
self.settings = settings
|
||||||
|
self.icons_path = icons_path
|
||||||
|
Gtk.EventBox.__init__(self)
|
||||||
|
|
||||||
|
check_key(settings, "icon-size", 16)
|
||||||
|
check_key(settings, "root-css-name", "tray")
|
||||||
|
|
||||||
|
self.set_property("name", settings["root-css-name"])
|
||||||
|
|
||||||
|
self.icon_size = settings["icon-size"]
|
||||||
|
|
||||||
|
self.box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
|
||||||
|
self.add(self.box)
|
||||||
|
|
||||||
|
self.items = {}
|
||||||
|
|
||||||
|
def add_item(self, item: StatusNotifierItem):
|
||||||
|
print("Tray -> add_item: {}".format(item.properties))
|
||||||
|
full_service_name = "{}{}".format(item.service_name, item.object_path)
|
||||||
|
if full_service_name not in self.items:
|
||||||
|
event_box = Gtk.EventBox()
|
||||||
|
image = Gtk.Image()
|
||||||
|
|
||||||
|
if "IconPixmap" in item.properties:
|
||||||
|
# TODO: handle loading pixbuf from dbus
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
update_icon(image, item, self.icon_size, self.icons_path)
|
||||||
|
|
||||||
|
if "Tooltip" in item.properties:
|
||||||
|
# TODO: handle tooltip variant type
|
||||||
|
pass
|
||||||
|
|
||||||
|
if "Title" in item.properties:
|
||||||
|
image.set_tooltip_markup(item.properties["Title"])
|
||||||
|
|
||||||
|
update_status(event_box, item)
|
||||||
|
|
||||||
|
event_box.add(image)
|
||||||
|
self.box.pack_start(event_box, False, False, 6)
|
||||||
|
self.box.show_all()
|
||||||
|
|
||||||
|
self.items[full_service_name] = {
|
||||||
|
"event_box": event_box,
|
||||||
|
"image": image,
|
||||||
|
"item": item
|
||||||
|
}
|
||||||
|
|
||||||
|
def update_item(self, item: StatusNotifierItem, changed_properties: list[str]):
|
||||||
|
full_service_name = "{}{}".format(item.service_name, item.object_path)
|
||||||
|
event_box = self.items[full_service_name]["event_box"]
|
||||||
|
image = self.items[full_service_name]["image"]
|
||||||
|
|
||||||
|
if "IconPixmap" in changed_properties:
|
||||||
|
# TODO: handle loading pixbuf from dbus
|
||||||
|
pass
|
||||||
|
elif "IconThemePath" in changed_properties or "IconName" in changed_properties:
|
||||||
|
update_icon(image, item, self.icon_size, self.icons_path)
|
||||||
|
|
||||||
|
if "Tooltip" in changed_properties:
|
||||||
|
# handle tooltip variant type
|
||||||
|
pass
|
||||||
|
|
||||||
|
if "Title" in changed_properties:
|
||||||
|
image.set_tooltip_markup(item.properties["Title"])
|
||||||
|
|
||||||
|
update_status(event_box, item)
|
||||||
|
|
||||||
|
event_box.show_all()
|
||||||
|
|
||||||
|
def remove_item(self, item: StatusNotifierItem):
|
||||||
|
full_service_name = "{}{}".format(item.service_name, item.object_path)
|
||||||
|
self.box.remove(self.items[full_service_name]["event_box"])
|
||||||
|
self.items.pop(full_service_name)
|
||||||
|
self.box.show_all()
|
230
nwg_panel/modules/sni_system_tray/watcher.py
Normal file
230
nwg_panel/modules/sni_system_tray/watcher.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
from dasbus.connection import SessionMessageBus
|
||||||
|
from dasbus.loop import EventLoop
|
||||||
|
from dasbus.signal import Signal
|
||||||
|
from dasbus.client.observer import DBusObserver
|
||||||
|
from dasbus.server.interface import accepts_additional_arguments
|
||||||
|
import dasbus.typing
|
||||||
|
|
||||||
|
WATCHER_SERVICE_NAME = "org.kde.StatusNotifierWatcher"
|
||||||
|
WATCHER_OBJECT_PATH = "/StatusNotifierWatcher"
|
||||||
|
|
||||||
|
dasbus_event_loop: typing.Union[EventLoop, None] = None
|
||||||
|
|
||||||
|
|
||||||
|
class StatusNotifierWatcherInterface(object):
|
||||||
|
__dbus_xml__ = """
|
||||||
|
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||||
|
<node>
|
||||||
|
<interface name="org.kde.StatusNotifierWatcher">
|
||||||
|
<annotation name="org.gtk.GDBus.C.Name" value="Watcher" />
|
||||||
|
|
||||||
|
<method name="RegisterStatusNotifierItem">
|
||||||
|
<annotation name="org.gtk.GDBus.C.Name" value="RegisterItem" />
|
||||||
|
<arg name="service" type="s" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="RegisterStatusNotifierHost">
|
||||||
|
<annotation name="org.gtk.GDBus.C.Name" value="RegisterHost" />
|
||||||
|
<arg name="service" type="s" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<property name="RegisteredStatusNotifierItems" type="as" access="read">
|
||||||
|
<annotation name="org.gtk.GDBus.C.Name" value="RegisteredItems" />
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QStringList"/>
|
||||||
|
</property>
|
||||||
|
|
||||||
|
<property name="IsStatusNotifierHostRegistered" type="b" access="read">
|
||||||
|
<annotation name="org.gtk.GDBus.C.Name" value="IsHostRegistered" />
|
||||||
|
</property>
|
||||||
|
|
||||||
|
<property name="ProtocolVersion" type="i" access="read"/>
|
||||||
|
|
||||||
|
<signal name="StatusNotifierItemRegistered">
|
||||||
|
<annotation name="org.gtk.GDBus.C.Name" value="ItemRegistered" />
|
||||||
|
<arg type="s" direction="out" name="service" />
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
<signal name="StatusNotifierItemUnregistered">
|
||||||
|
<annotation name="org.gtk.GDBus.C.Name" value="ItemUnregistered" />
|
||||||
|
<arg type="s" direction="out" name="service" />
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
<signal name="StatusNotifierHostRegistered">
|
||||||
|
<annotation name="org.gtk.GDBus.C.Name" value="HostRegistered" />
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
<signal name="StatusNotifierHostUnregistered">
|
||||||
|
<annotation name="org.gtk.GDBus.C.Name" value="HostUnregistered" />
|
||||||
|
</signal>
|
||||||
|
|
||||||
|
</interface>
|
||||||
|
</node>
|
||||||
|
"""
|
||||||
|
|
||||||
|
PropertiesChanged = Signal()
|
||||||
|
StatusNotifierItemRegistered = Signal()
|
||||||
|
StatusNotifierItemUnregistered = Signal()
|
||||||
|
StatusNotifierHostRegistered = Signal()
|
||||||
|
StatusNotifierHostUnregistered = Signal()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._statusNotifierItems = []
|
||||||
|
self._statusNotifierHosts = []
|
||||||
|
self._isStatusNotifierHostRegistered = False
|
||||||
|
self._protocolVersion = 0
|
||||||
|
self.session_bus = SessionMessageBus()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.session_bus.disconnect()
|
||||||
|
|
||||||
|
@accepts_additional_arguments
|
||||||
|
def RegisterStatusNotifierItem(self, service, call_info):
|
||||||
|
print(
|
||||||
|
"StatusNotifierWatcher -> RegisterStatusNotifierItem\n service: {}\n sender: {}".format(
|
||||||
|
service,
|
||||||
|
call_info["sender"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# libappindicator sends object path, use sender name and object path
|
||||||
|
if service[0] == "/":
|
||||||
|
full_service_name = "{}{}".format(call_info["sender"], service)
|
||||||
|
|
||||||
|
# xembedsniproxy sends item name, use the item from the argument
|
||||||
|
elif service[0] == ":":
|
||||||
|
full_service_name = "{}{}".format(service, "/StatusNotifierItem")
|
||||||
|
|
||||||
|
else:
|
||||||
|
full_service_name = "{}{}".format(call_info["sender"], "/StatusNotifierItem")
|
||||||
|
|
||||||
|
if full_service_name not in self._statusNotifierItems:
|
||||||
|
item_service_observer = DBusObserver(
|
||||||
|
message_bus=self.session_bus,
|
||||||
|
service_name=call_info["sender"]
|
||||||
|
)
|
||||||
|
item_service_observer.service_available.connect(
|
||||||
|
lambda _observer: self.item_available_handler(full_service_name)
|
||||||
|
)
|
||||||
|
item_service_observer.service_unavailable.connect(
|
||||||
|
lambda _observer: self.item_unavailable_handler(full_service_name)
|
||||||
|
)
|
||||||
|
item_service_observer.connect_once_available()
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
(
|
||||||
|
"StatusNotifierWatcher -> RegisterStatusNotifierItem: item already registered\n"
|
||||||
|
" full_service_name: {}"
|
||||||
|
).format(full_service_name, service)
|
||||||
|
)
|
||||||
|
|
||||||
|
@accepts_additional_arguments
|
||||||
|
def RegisterStatusNotifierHost(self, service, call_info):
|
||||||
|
print("StatusNotifierWatcher -> RegisterStatusNotifierHost: {}".format(service))
|
||||||
|
if call_info["sender"] not in self._statusNotifierHosts:
|
||||||
|
host_service_observer = DBusObserver(
|
||||||
|
message_bus=self.session_bus,
|
||||||
|
service_name=call_info["sender"]
|
||||||
|
)
|
||||||
|
host_service_observer.service_available.connect(
|
||||||
|
self.host_available_handler
|
||||||
|
)
|
||||||
|
host_service_observer.service_unavailable.connect(
|
||||||
|
self.host_unavailable_handler
|
||||||
|
)
|
||||||
|
host_service_observer.connect_once_available()
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
"StatusNotifierWatcher -> RegisterStatusNotifierHost: host already registered\n service: {}\n sender: {})".format(
|
||||||
|
service,
|
||||||
|
call_info["sender"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def RegisteredStatusNotifierItems(self) -> list:
|
||||||
|
print("StatusNotifierWatcher -> RegisteredStatusNotifierItems")
|
||||||
|
return self._statusNotifierItems
|
||||||
|
|
||||||
|
@property
|
||||||
|
def IsStatusNotifierHostRegistered(self) -> bool:
|
||||||
|
print(
|
||||||
|
"StatusNotifierWatcher -> IsStatusNotifierHostRegistered: {}".format(
|
||||||
|
str(len(self._statusNotifierHosts) > 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return len(self._statusNotifierHosts) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ProtocolVersion(self) -> int:
|
||||||
|
print("StatusNotifierWatcher -> ProtocolVersion: ".format(str(self._protocolVersion)))
|
||||||
|
return self._protocolVersion
|
||||||
|
|
||||||
|
def item_available_handler(self, full_service_name):
|
||||||
|
print(
|
||||||
|
"StatusNotifierWatcher -> item_available_handler\n full_service_name: {}".format(
|
||||||
|
full_service_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._statusNotifierItems.append(full_service_name)
|
||||||
|
self.StatusNotifierItemRegistered.emit(full_service_name)
|
||||||
|
self.PropertiesChanged.emit(WATCHER_SERVICE_NAME, {
|
||||||
|
"RegisteredStatusNotifierItems": dasbus.typing.get_variant(
|
||||||
|
dasbus.typing.List[dasbus.typing.Str],
|
||||||
|
self._statusNotifierItems
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
def item_unavailable_handler(self, full_service_name):
|
||||||
|
print(
|
||||||
|
"StatusNotifierWatcher -> item_unavailable_handler\n full_service_name: {}".format(
|
||||||
|
full_service_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if full_service_name in set(self._statusNotifierItems):
|
||||||
|
self._statusNotifierItems.remove(full_service_name)
|
||||||
|
self.StatusNotifierItemUnregistered.emit(full_service_name)
|
||||||
|
self.PropertiesChanged.emit(WATCHER_SERVICE_NAME, {
|
||||||
|
"RegisteredStatusNotifierItems": dasbus.typing.get_variant(
|
||||||
|
dasbus.typing.List[dasbus.typing.Str],
|
||||||
|
self._statusNotifierItems
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
def host_available_handler(self, observer):
|
||||||
|
self._statusNotifierHosts.append(observer.service_name)
|
||||||
|
self.StatusNotifierHostRegistered.emit()
|
||||||
|
self.PropertiesChanged.emit(WATCHER_SERVICE_NAME, {
|
||||||
|
"IsStatusNotifierHostRegistered": dasbus.typing.get_variant(dasbus.typing.Bool, True)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
def host_unavailable_handler(self, observer):
|
||||||
|
self._statusNotifierHosts.remove(observer.service_name)
|
||||||
|
self.StatusNotifierHostUnregistered.emit()
|
||||||
|
if len(self._statusNotifierHosts) == 0:
|
||||||
|
self.PropertiesChanged.emit(WATCHER_SERVICE_NAME, {
|
||||||
|
"IsStatusNotifierHostRegistered": dasbus.typing.get_variant(dasbus.typing.Bool, False)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
def init():
|
||||||
|
session_bus = SessionMessageBus()
|
||||||
|
session_bus.publish_object(WATCHER_OBJECT_PATH, StatusNotifierWatcherInterface())
|
||||||
|
session_bus.register_service(WATCHER_SERVICE_NAME)
|
||||||
|
print("watcher.init(): published {}{} on dbus.".format(WATCHER_SERVICE_NAME, WATCHER_OBJECT_PATH))
|
||||||
|
|
||||||
|
global dasbus_event_loop
|
||||||
|
if dasbus_event_loop is None:
|
||||||
|
print("watcher.init(): running dasbus.EventLoop")
|
||||||
|
dasbus_event_loop = EventLoop()
|
||||||
|
dasbus_event_loop.run()
|
||||||
|
|
||||||
|
|
||||||
|
def deinit():
|
||||||
|
global dasbus_event_loop
|
||||||
|
if dasbus_event_loop is not None:
|
||||||
|
print("watcher.deinit(): quitting dasbus.EventLoop")
|
||||||
|
dasbus_event_loop.quit()
|
||||||
|
if dasbus_event_loop is not None:
|
||||||
|
dasbus_event_loop = None
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
PyGObject~=3.42.0
|
||||||
|
psutil~=5.9.0
|
||||||
|
i3ipc~=2.2.1
|
||||||
|
setuptools~=57.0.0
|
||||||
|
dasbus~=1.6
|
Reference in New Issue
Block a user