Important information: Google announced that, from September 2026, Android devices will require ALL apps to be signed by Google, effectively leading to an iOS situation. Value your right to a computer that does what you want; do not tolerate this monopolistic practice! Contact me if you don't understand why it is bad. Click to learn more.

 __init__.py

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