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 • 7.78 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__(has_frame=False)
78
self.applet = applet
79
self.service = service
80
self.icon = Gtk.Image(icon_name="image-missing", pixel_size=self.applet.icon_size)
81
self.set_child(self.icon)
82
self.popover_menu = Gtk.PopoverMenu(flags=Gtk.PopoverMenuFlags.NESTED, has_arrow=False, halign=Gtk.Align.START)
83
self.set_popover(self.popover_menu)
84
85
connection, _0, path = self.service.rpartition(":")
86
self.menu = None
87
88
try:
89
self.proxy = Gio.DBusProxy.new_sync(
90
self.applet.connection,
91
Gio.DBusProxyFlags.NONE,
92
None,
93
connection,
94
path,
95
"org.kde.StatusNotifierItem",
96
None
97
)
98
self.applet.item_id += 1
99
icon_name = self.proxy.get_cached_property("IconName")
100
if icon_name:
101
icon_name = icon_name.unpack()
102
self.icon.set_from_icon_name(icon_name)
103
menu = self.proxy.get_cached_property("Menu")
104
if menu:
105
self.menu = dbusmenu.CanonicalMenuModel()
106
self.menu.connect("menu_changed", lambda x: panorama_panel.add_icons_to_menu(self.popover_menu, Gtk.IconSize.NORMAL))
107
self.menu.connect_dbus(connection, menu.get_string(), f"ac{self.applet.item_id}")
108
self.insert_action_group(f"ac{self.applet.item_id}", self.menu.actions)
109
self.popover_menu.set_menu_model(self.menu.menu)
110
111
# TODO: NewTitle etc.
112
self.applet.connection.signal_subscribe(
113
connection,
114
"org.kde.StatusNotifierItem",
115
"NewIcon",
116
path,
117
None,
118
Gio.DBusSignalFlags.NONE,
119
self.on_icon_change,
120
)
121
except Exception as e:
122
print(f"Could not get properties from {service}: {e}")
123
traceback.print_exc()
124
125
def on_icon_change(self, *args):
126
icon_name = self.proxy.call_sync(
127
"org.freedesktop.DBus.Properties.Get",
128
GLib.Variant("(ss)", ("org.kde.StatusNotifierItem", "IconName")),
129
Gio.DBusCallFlags.NONE,
130
-1,
131
None
132
)
133
if icon_name:
134
self.icon.set_from_icon_name(icon_name.unpack()[0])
135
136
137
class IndicatorApplet(panorama_panel.Applet):
138
name = _("Indicators")
139
description = _("Show application status icons")
140
141
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
142
super().__init__(orientation=orientation, config=config or {})
143
self.items = {}
144
self.item_id = 0
145
146
if config is None:
147
config = {}
148
149
self.icon_size = config.get("icon_size", 24)
150
151
self.connection = Gio.bus_get_sync(Gio.BusType.SESSION)
152
153
# Make the host name unique
154
self.bus_name = f"org.kde.StatusNotifierHost-{os.getpid()}-pan-{uuid.uuid4()}"
155
156
self.service = StatusNotifierService(self)
157
self.service.export(self.connection)
158
Gio.bus_own_name(
159
Gio.BusType.SESSION,
160
self.bus_name,
161
Gio.BusNameOwnerFlags.NONE,
162
None,
163
None,
164
None,
165
)
166
167
try:
168
self.watcher = Gio.DBusProxy.new_sync(
169
self.connection,
170
Gio.DBusProxyFlags.NONE,
171
None,
172
"org.kde.StatusNotifierWatcher",
173
"/StatusNotifierWatcher",
174
"org.kde.StatusNotifierWatcher",
175
None,
176
)
177
except Exception as e:
178
print("Could not connect to StatusNotifierWatcher:", e)
179
self.watcher = None
180
181
if self.watcher:
182
try:
183
self.watcher.call_sync(
184
"RegisterStatusNotifierHost",
185
GLib.Variant("(s)", (self.bus_name,)),
186
Gio.DBusCallFlags.NONE,
187
-1,
188
None,
189
)
190
except Exception as e:
191
print("Failed to register host:", e)
192
193
# Watch for new items
194
self.connection.signal_subscribe(
195
None,
196
"org.kde.StatusNotifierWatcher",
197
"StatusNotifierItemRegistered",
198
"/StatusNotifierWatcher",
199
None,
200
Gio.DBusSignalFlags.NONE,
201
self.on_item_registered_signal,
202
)
203
# Watch for vanishing items
204
self.connection.signal_subscribe(
205
None,
206
"org.kde.StatusNotifierWatcher",
207
"StatusNotifierItemUnregistered",
208
"/StatusNotifierWatcher",
209
None,
210
Gio.DBusSignalFlags.NONE,
211
self.on_item_unregistered_signal,
212
)
213
# TODO: watch for things such as icon changes.
214
215
def on_item_registered_signal(self, conn, sender_name, obj_path, iface_name, signal_name, params):
216
(service,) = params.unpack()
217
self.on_item_registered(service)
218
219
def on_item_unregistered_signal(self, conn, sender_name, obj_path, iface_name, signal_name, params):
220
(service,) = params.unpack()
221
self.on_item_unregistered(service)
222
223
def on_item_registered(self, service):
224
icon = StatusIcon(self, service)
225
self.append(icon)
226
self.items[service] = icon
227
228
def on_item_unregistered(self, service):
229
if service in self.items:
230
self.remove(self.items[service])
231
del self.items[service]
232
233
def get_config(self):
234
return {
235
"icon_size": self.icon_size,
236
}
237