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.83 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
icon = Gio.ThemedIcon.new("dialog-warning")
141
142
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
143
super().__init__(orientation=orientation, config=config or {})
144
self.items = {}
145
self.item_id = 0
146
147
if config is None:
148
config = {}
149
150
self.icon_size = config.get("icon_size", 24)
151
152
self.connection = Gio.bus_get_sync(Gio.BusType.SESSION)
153
154
# Make the host name unique
155
self.bus_name = f"org.kde.StatusNotifierHost-{os.getpid()}-pan-{uuid.uuid4()}"
156
157
self.service = StatusNotifierService(self)
158
self.service.export(self.connection)
159
Gio.bus_own_name(
160
Gio.BusType.SESSION,
161
self.bus_name,
162
Gio.BusNameOwnerFlags.NONE,
163
None,
164
None,
165
None,
166
)
167
168
try:
169
self.watcher = Gio.DBusProxy.new_sync(
170
self.connection,
171
Gio.DBusProxyFlags.NONE,
172
None,
173
"org.kde.StatusNotifierWatcher",
174
"/StatusNotifierWatcher",
175
"org.kde.StatusNotifierWatcher",
176
None,
177
)
178
except Exception as e:
179
print("Could not connect to StatusNotifierWatcher:", e)
180
self.watcher = None
181
182
if self.watcher:
183
try:
184
self.watcher.call_sync(
185
"RegisterStatusNotifierHost",
186
GLib.Variant("(s)", (self.bus_name,)),
187
Gio.DBusCallFlags.NONE,
188
-1,
189
None,
190
)
191
except Exception as e:
192
print("Failed to register host:", e)
193
194
# Watch for new items
195
self.connection.signal_subscribe(
196
None,
197
"org.kde.StatusNotifierWatcher",
198
"StatusNotifierItemRegistered",
199
"/StatusNotifierWatcher",
200
None,
201
Gio.DBusSignalFlags.NONE,
202
self.on_item_registered_signal,
203
)
204
# Watch for vanishing items
205
self.connection.signal_subscribe(
206
None,
207
"org.kde.StatusNotifierWatcher",
208
"StatusNotifierItemUnregistered",
209
"/StatusNotifierWatcher",
210
None,
211
Gio.DBusSignalFlags.NONE,
212
self.on_item_unregistered_signal,
213
)
214
# TODO: watch for things such as icon changes.
215
216
def on_item_registered_signal(self, conn, sender_name, obj_path, iface_name, signal_name, params):
217
(service,) = params.unpack()
218
self.on_item_registered(service)
219
220
def on_item_unregistered_signal(self, conn, sender_name, obj_path, iface_name, signal_name, params):
221
(service,) = params.unpack()
222
self.on_item_unregistered(service)
223
224
def on_item_registered(self, service):
225
icon = StatusIcon(self, service)
226
self.append(icon)
227
self.items[service] = icon
228
229
def on_item_unregistered(self, service):
230
if service in self.items:
231
self.remove(self.items[service])
232
del self.items[service]
233
234
def get_config(self):
235
return {
236
"icon_size": self.icon_size,
237
}
238