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/plain • 10.25 kiB
Python script, ASCII text executable
        
            
1
"""
2
D-Bus notifier applet for the Panorama panel.
3
Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>
4
5
This program is free software: you can redistribute it and/or modify
6
it under the terms of the GNU General Public Licence as published by
7
the Free Software Foundation, either version 3 of the Licence, or
8
(at your option) any later version.
9
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
GNU General Public Licence for more details.
14
15
You should have received a copy of the GNU General Public Licence
16
along with this program. If not, see <https://www.gnu.org/licenses/>.
17
"""
18
19
from __future__ import annotations
20
21
import pydbus.generic
22
23
import panorama_panel
24
import locale
25
from pathlib import Path
26
from pydbus import SessionBus
27
28
import gi
29
gi.require_version("Gtk", "4.0")
30
31
from gi.repository import Gtk, Gdk, GLib
32
33
34
module_directory = Path(__file__).resolve().parent
35
36
locale.bindtextdomain("panorama-app-menu", module_directory / "locale")
37
_ = lambda x: locale.dgettext("panorama-app-menu", x)
38
39
40
custom_css = """
41
.notification-button {
42
padding: 0;
43
}
44
45
.panorama-panel-notifier-summary {
46
font-weight: 600;
47
}
48
"""
49
50
css_provider = Gtk.CssProvider()
51
css_provider.load_from_data(custom_css)
52
Gtk.StyleContext.add_provider_for_display(
53
Gdk.Display.get_default(),
54
css_provider,
55
100
56
)
57
58
59
class NotificationsService:
60
"""
61
<node>
62
<interface name="org.freedesktop.Notifications">
63
<method name="GetCapabilities">
64
<arg type="as" name="caps" direction="out"/>
65
</method>
66
<method name="GetServerInformation">
67
<arg type="s" name="name" direction="out"/>
68
<arg type="s" name="vendor" direction="out"/>
69
<arg type="s" name="version" direction="out"/>
70
<arg type="s" name="spec_version" direction="out"/>
71
</method>
72
<method name="Notify">
73
<arg type="s" name="app_name" direction="in"/>
74
<arg type="u" name="replaces_id" direction="in"/>
75
<arg type="s" name="app_icon" direction="in"/>
76
<arg type="s" name="summary" direction="in"/>
77
<arg type="s" name="body" direction="in"/>
78
<arg type="as" name="actions" direction="in"/>
79
<arg type="a{sv}" name="hints" direction="in"/>
80
<arg type="i" name="expire_timeout" direction="in"/>
81
<arg type="u" name="id" direction="out"/>
82
</method>
83
<method name="CloseNotification">
84
<arg type="u" name="id" direction="in"/>
85
</method>
86
<signal name="ActionInvoked">
87
<arg type="u" name="id" direction="out"/>
88
<arg type="s" name="action_key" direction="out"/>
89
</signal>
90
</interface>
91
</node>
92
"""
93
94
ActionInvoked = pydbus.generic.signal()
95
96
def __init__(self, applet: NotifierApplet):
97
self._next_id = 1
98
self.applet = applet
99
100
def GetCapabilities(self):
101
return ["body"]
102
103
def GetServerInformation(self):
104
# name, vendor, version, spec_version
105
return "panorama-panel", "panorama", "0.1", "1.2"
106
107
def Notify(self, *args):
108
notification_id = self._next_id
109
self._next_id += 1
110
self.applet.push_notification(*args, notification_id)
111
return notification_id
112
113
def CloseNotification(self, notification_id):
114
if notification_id in self.applet.notifications_by_id:
115
self.applet.notification_list.remove(self.applet.notifications_by_id[notification_id])
116
self.applet.notification_count_label.set_text(locale.format_string('%d', self.applet.notification_count - 1))
117
self.applet.notification_count -= 1
118
del self.applet.notifications_by_id[notification_id]
119
120
121
class BriefNotification(Gtk.Box):
122
def __init__(self, app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout):
123
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
124
if not app_icon:
125
app_icon = "preferences-desktop-notifications"
126
self.image = Gtk.Image.new_from_icon_name(app_icon)
127
self.append(self.image)
128
self.append(Gtk.Label.new(summary))
129
self.set_halign(Gtk.Align.CENTER)
130
131
132
class DetailedNotification(Gtk.ListBoxRow):
133
def __init__(self, applet, app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout, id):
134
Gtk.ListBoxRow.__init__(self)
135
self.applet = applet
136
if not app_icon:
137
app_icon = "preferences-desktop-notifications"
138
139
self.box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
140
self.set_child(self.box)
141
self.id = id
142
143
self.icon = Gtk.Image.new_from_icon_name(app_icon)
144
self.icon.set_icon_size(Gtk.IconSize.LARGE)
145
self.box.append(self.icon)
146
147
self.text_area = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
148
149
self.summary = Gtk.Label.new(summary)
150
self.summary.set_halign(Gtk.Align.START)
151
self.summary.set_xalign(0)
152
self.summary.add_css_class("panorama-panel-notifier-summary")
153
self.text_area.append(self.summary)
154
155
self.body = Gtk.Label.new(body)
156
self.body.set_halign(Gtk.Align.START)
157
self.body.set_xalign(0)
158
self.summary.add_css_class("panorama-panel-notifier-body")
159
self.text_area.append(self.body)
160
self.text_area.set_hexpand(True)
161
162
self.actions = actions
163
164
for name, label in zip(self.actions[0::2], self.actions[1::2]):
165
action_button = Gtk.Button.new_with_label(label)
166
action_button.connect("clicked", lambda button, n=name: self.engage_action(n))
167
self.text_area.append(action_button)
168
169
self.box.append(self.text_area)
170
171
self.close_button = Gtk.Button(has_frame=False, child=Gtk.Image.new_from_icon_name("window-close"), halign=Gtk.Align.END, valign=Gtk.Align.START)
172
self.box.append(self.close_button)
173
self.close_button.connect("clicked", self.delete)
174
175
def delete(self, button):
176
self.get_parent().remove(self)
177
del self.applet.notifications_by_id[self.id]
178
self.applet.notification_count_label.set_text(locale.format_string('%d', self.applet.notification_count - 1))
179
self.applet.notification_count -= 1
180
# TODO: notify closed
181
182
def engage_action(self, name):
183
self.applet.service.ActionInvoked(self.id, name)
184
185
186
class NotifierApplet(panorama_panel.Applet):
187
name = _("Notification centre")
188
description = _("Get desktop notifications")
189
190
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
191
super().__init__(orientation=orientation, config=config)
192
if config is None:
193
config = {}
194
195
self.icon_size = config.get("icon_size", 24)
196
197
self.button = Gtk.MenuButton()
198
self.button.set_has_frame(False)
199
self.button.add_css_class("notification-button")
200
self.stack = Gtk.Stack()
201
self.button.set_child(self.stack)
202
self.notification_indicator = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
203
self.status_icon = Gtk.Image.new_from_icon_name("emblem-important-symbolic")
204
self.status_icon.set_pixel_size(config.get("empty_icon_size", 16))
205
self.notification_indicator.append(self.status_icon)
206
self.notification_indicator.set_halign(Gtk.Align.CENTER)
207
self.notification_count_label = Gtk.Label.new(locale.format_string("%d", 0))
208
self.notification_count = 0
209
self.notification_indicator.append(self.notification_count_label)
210
self.stack.add_child(self.notification_indicator)
211
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_UP_DOWN)
212
self.stack.set_transition_duration(500)
213
self.popover = Gtk.Popover()
214
self.notification_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
215
self.popover.set_child(self.notification_list)
216
self.button.set_popover(self.popover)
217
# TODO: support the other parameters; add a popover with a history
218
self.append(self.button)
219
220
self.notifications_by_id = {}
221
self.service = NotificationsService(self)
222
bus = SessionBus()
223
self.publishing = bus.publish("org.freedesktop.Notifications", self.service)
224
225
def push_notification(self, app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout, notification_id):
226
new_child = BriefNotification(app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout)
227
new_child.image.set_pixel_size(self.icon_size)
228
self.stack.add_child(new_child)
229
self.button.set_has_frame(True)
230
self.stack.set_visible_child(new_child)
231
def remove_notification():
232
def remove_child():
233
self.stack.remove(new_child)
234
235
if self.stack.get_visible_child() == new_child:
236
self.stack.set_visible_child(self.stack.get_visible_child().get_prev_sibling())
237
GLib.timeout_add(self.stack.get_transition_duration(), remove_child)
238
if self.stack.get_visible_child() is self.notification_indicator:
239
self.button.set_has_frame(False)
240
else:
241
# TODO: split the time
242
def readd_timeout(*args):
243
if new_child == self.stack.get_visible_child():
244
GLib.timeout_add(self.stack.get_transition_duration(), remove_notification)
245
self.stack.connect("notify::visible-child", readd_timeout)
246
return False
247
if expire_timeout <= 0:
248
expire_timeout = 2500
249
GLib.timeout_add(expire_timeout, remove_notification)
250
251
notification_info = DetailedNotification(self, app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout, notification_id)
252
self.notifications_by_id[notification_id] = notification_info
253
self.notification_list.append(notification_info)
254
self.notification_count_label.set_text(locale.format_string('%d', self.notification_count + 1))
255
self.notification_count += 1
256
# TODO: notify that it was closed
257
258
def get_config(self):
259
return {
260
"icon_size": self.icon_size,
261
"empty_icon_size": self.status_icon.get_pixel_size(),
262
}
263
264
def shutdown(self, app):
265
self.publishing.unpublish()
266