By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 __init__.py

View raw Download
text/x-script.python • 6.71 kiB
Python script, ASCII text executable
        
            
1
#!/usr/bin/env python3
2
"""
3
D-Bus app indicator/system tray for the Panorama panel.
4
Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>
5
6
This program is free software: you can redistribute it and/or modify
7
it under the terms of the GNU General Public Licence as published by
8
the Free Software Foundation, either version 3 of the Licence, or
9
(at your option) any later version.
10
11
This program is distributed in the hope that it will be useful,
12
but WITHOUT ANY WARRANTY; without even the implied warranty of
13
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
GNU General Public Licence for more details.
15
16
You should have received a copy of the GNU General Public Licence
17
along with this program. If not, see <https://www.gnu.org/licenses/>.
18
"""
19
20
from __future__ import annotations
21
import os
22
import traceback
23
import uuid
24
import locale
25
from pathlib import Path
26
27
import gi
28
from ctypes import CDLL
29
CDLL("libdbusmenu-glib.so")
30
gi.require_version("Dbusmenu", "0.4")
31
32
import panorama_panel
33
from gi.repository import Gtk, GLib, Gio
34
35
from . import dbusmenu
36
37
module_directory = Path(__file__).resolve().parent
38
locale.bindtextdomain("panorama-indicator-applet", module_directory / "locale")
39
_ = lambda x: locale.dgettext("panorama-indicator-applet", x)
40
41
42
class StatusNotifierService:
43
"""
44
<node>
45
<interface name="org.kde.StatusNotifierHost">
46
</interface>
47
</node>
48
"""
49
50
def __init__(self, applet: IndicatorApplet):
51
self.applet = applet
52
self.xml = """
53
<node>
54
<interface name="org.kde.StatusNotifierHost">
55
</interface>
56
</node>
57
"""
58
self.node_info = Gio.DBusNodeInfo.new_for_xml(self.xml)
59
self.iface_info = self.node_info.interfaces[0]
60
61
def export(self, connection: Gio.DBusConnection):
62
connection.register_object(
63
"/StatusNotifierHost",
64
self.iface_info,
65
self.handle_method_call,
66
None,
67
None,
68
)
69
70
def handle_method_call(self, conn, sender, obj_path, iface_name, method, params, invocation):
71
# It has no methods
72
invocation.return_value(None)
73
74
75
class StatusIcon(Gtk.MenuButton):
76
def __init__(self, applet: IndicatorApplet, service: str):
77
super().__init__()
78
self.applet = applet
79
self.service = service
80
label = Gtk.Label(label=service)
81
self.set_child(label)
82
print(self.service)
83
self.popover_menu = Gtk.PopoverMenu(flags=Gtk.PopoverMenuFlags.NESTED)
84
self.set_popover(self.popover_menu)
85
86
connection, _0, path = self.service.rpartition(":")
87
self.menu = None
88
89
try:
90
proxy = Gio.DBusProxy.new_sync(
91
self.applet.connection,
92
Gio.DBusProxyFlags.NONE,
93
None,
94
connection,
95
path,
96
"org.kde.StatusNotifierItem",
97
None
98
)
99
self.applet.item_id += 1
100
icon_name = proxy.get_cached_property("IconName")
101
if icon_name:
102
icon_name = icon_name.unpack()
103
self.icon = Gtk.Image.new_from_icon_name(icon_name)
104
self.set_child(self.icon)
105
menu = proxy.get_cached_property("Menu")
106
if menu:
107
self.menu = dbusmenu.CanonicalMenuModel()
108
self.menu.connect_dbus(connection, menu.get_string(), f"ac{self.applet.item_id}")
109
self.insert_action_group(f"ac{self.applet.item_id}", self.menu.actions)
110
self.popover_menu.set_menu_model(self.menu.menu)
111
except Exception as e:
112
print(f"Could not get properties from {service}: {e}")
113
traceback.print_exc()
114
115
116
class IndicatorApplet(panorama_panel.Applet):
117
name = _("Indicators")
118
description = _("Show application status icons")
119
120
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
121
super().__init__(orientation=orientation, config=config or {})
122
self.items = {}
123
self.item_id = 0
124
self.connection = Gio.bus_get_sync(Gio.BusType.SESSION)
125
126
# Make the host name unique
127
self.bus_name = f"org.kde.StatusNotifierHost-{os.getpid()}-pan-{uuid.uuid4()}"
128
129
self.service = StatusNotifierService(self)
130
self.service.export(self.connection)
131
Gio.bus_own_name(
132
Gio.BusType.SESSION,
133
self.bus_name,
134
Gio.BusNameOwnerFlags.NONE,
135
None,
136
None,
137
None,
138
)
139
140
try:
141
self.watcher = Gio.DBusProxy.new_sync(
142
self.connection,
143
Gio.DBusProxyFlags.NONE,
144
None,
145
"org.kde.StatusNotifierWatcher",
146
"/StatusNotifierWatcher",
147
"org.kde.StatusNotifierWatcher",
148
None,
149
)
150
except Exception as e:
151
print("Could not connect to StatusNotifierWatcher:", e)
152
self.watcher = None
153
154
if self.watcher:
155
try:
156
self.watcher.call_sync(
157
"RegisterStatusNotifierHost",
158
GLib.Variant("(s)", (self.bus_name,)),
159
Gio.DBusCallFlags.NONE,
160
-1,
161
None,
162
)
163
except Exception as e:
164
print("Failed to register host:", e)
165
166
# Watch for new items
167
self.connection.signal_subscribe(
168
None,
169
"org.kde.StatusNotifierWatcher",
170
"StatusNotifierItemRegistered",
171
"/StatusNotifierWatcher",
172
None,
173
Gio.DBusSignalFlags.NONE,
174
self.on_item_registered_signal,
175
)
176
# Watch for vanishing items
177
self.connection.signal_subscribe(
178
None,
179
"org.kde.StatusNotifierWatcher",
180
"StatusNotifierItemUnregistered",
181
"/StatusNotifierWatcher",
182
None,
183
Gio.DBusSignalFlags.NONE,
184
self.on_item_unregistered_signal,
185
)
186
# TODO: watch for things such as icon changes.
187
188
def on_item_registered_signal(self, conn, sender_name, obj_path, iface_name, signal_name, params):
189
(service,) = params.unpack()
190
self.on_item_registered(service)
191
192
def on_item_unregistered_signal(self, conn, sender_name, obj_path, iface_name, signal_name, params):
193
(service,) = params.unpack()
194
self.on_item_unregistered(service)
195
196
def on_item_registered(self, service):
197
icon = StatusIcon(self, service)
198
self.append(icon)
199
self.items[service] = icon
200
201
def on_item_unregistered(self, service):
202
if service in self.items:
203
self.remove(self.items[service])
204
del self.items[service]
205