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 • 7.98 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 panorama_panel
22
import locale
23
from pathlib import Path
24
from pydbus import SessionBus
25
26
import gi
27
gi.require_version("Gtk", "4.0")
28
29
from gi.repository import Gtk, Gdk, GLib
30
31
32
module_directory = Path(__file__).resolve().parent
33
34
locale.bindtextdomain("panorama-app-menu", module_directory / "locale")
35
_ = lambda x: locale.dgettext("panorama-app-menu", x)
36
37
38
custom_css = """
39
.notification-button {
40
padding: 0;
41
}
42
43
.panorama-panel-notifier-summary {
44
font-weight: 600;
45
}
46
"""
47
48
css_provider = Gtk.CssProvider()
49
css_provider.load_from_data(custom_css)
50
Gtk.StyleContext.add_provider_for_display(
51
Gdk.Display.get_default(),
52
css_provider,
53
100
54
)
55
56
57
class NotificationsService:
58
"""
59
<node>
60
<interface name="org.freedesktop.Notifications">
61
<method name="GetCapabilities">
62
<arg type="as" name="caps" direction="out"/>
63
</method>
64
<method name="GetServerInformation">
65
<arg type="s" name="name" direction="out"/>
66
<arg type="s" name="vendor" direction="out"/>
67
<arg type="s" name="version" direction="out"/>
68
<arg type="s" name="spec_version" direction="out"/>
69
</method>
70
<method name="Notify">
71
<arg type="s" name="app_name" direction="in"/>
72
<arg type="u" name="replaces_id" direction="in"/>
73
<arg type="s" name="app_icon" direction="in"/>
74
<arg type="s" name="summary" direction="in"/>
75
<arg type="s" name="body" direction="in"/>
76
<arg type="as" name="actions" direction="in"/>
77
<arg type="a{sv}" name="hints" direction="in"/>
78
<arg type="i" name="expire_timeout" direction="in"/>
79
<arg type="u" name="id" direction="out"/>
80
</method>
81
</interface>
82
</node>
83
"""
84
85
def __init__(self, applet: NotifierApplet):
86
self._next_id = 1
87
self.applet = applet
88
89
def GetCapabilities(self):
90
return ["body"]
91
92
def GetServerInformation(self):
93
# name, vendor, version, spec_version
94
return "panorama-panel", "panorama", "0.1", "1.2"
95
96
def Notify(self, *args):
97
notification_id = self._next_id
98
self._next_id += 1
99
self.applet.push_notification(*args)
100
return notification_id
101
102
103
class BriefNotification(Gtk.Box):
104
def __init__(self, app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout):
105
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
106
if not app_icon:
107
app_icon = "preferences-desktop-notifications"
108
self.image = Gtk.Image.new_from_icon_name(app_icon)
109
self.append(self.image)
110
self.append(Gtk.Label.new(summary))
111
self.set_halign(Gtk.Align.CENTER)
112
113
114
class DetailedNotification(Gtk.ListBoxRow):
115
def __init__(self, app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout):
116
Gtk.ListBoxRow.__init__(self)
117
if not app_icon:
118
app_icon = "preferences-desktop-notifications"
119
120
self.box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
121
self.set_child(self.box)
122
123
self.icon = Gtk.Image.new_from_icon_name(app_icon)
124
self.icon.set_icon_size(Gtk.IconSize.LARGE)
125
self.box.append(self.icon)
126
127
self.text_area = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
128
129
self.summary = Gtk.Label.new(summary)
130
self.summary.set_halign(Gtk.Align.START)
131
self.summary.set_xalign(0)
132
self.summary.add_css_class("panorama-panel-notifier-summary")
133
self.text_area.append(self.summary)
134
135
self.body = Gtk.Label.new(body)
136
self.body.set_halign(Gtk.Align.START)
137
self.body.set_xalign(0)
138
self.summary.add_css_class("panorama-panel-notifier-body")
139
self.text_area.append(self.body)
140
141
# TODO: add actions
142
143
self.box.append(self.text_area)
144
145
146
class NotifierApplet(panorama_panel.Applet):
147
name = _("Notification centre")
148
description = _("Get desktop notifications")
149
150
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
151
super().__init__(orientation=orientation, config=config)
152
if config is None:
153
config = {}
154
155
self.icon_size = config.get("icon_size", 24)
156
157
self.button = Gtk.MenuButton()
158
self.button.set_has_frame(False)
159
self.button.add_css_class("notification-button")
160
self.stack = Gtk.Stack()
161
self.button.set_child(self.stack)
162
self.notification_indicator = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
163
self.status_icon = Gtk.Image.new_from_icon_name("emblem-important")
164
self.status_icon.set_pixel_size(self.icon_size)
165
self.notification_indicator.append(self.status_icon)
166
self.notification_indicator.set_halign(Gtk.Align.CENTER)
167
self.notification_count_label = Gtk.Label.new(locale.format_string("%d", 0))
168
self.notification_count = 0
169
self.notification_indicator.append(self.notification_count_label)
170
self.stack.add_child(self.notification_indicator)
171
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_UP_DOWN)
172
self.stack.set_transition_duration(500)
173
self.popover = Gtk.Popover()
174
self.notification_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
175
self.popover.set_child(self.notification_list)
176
self.button.set_popover(self.popover)
177
# TODO: support the other parameters; add a popover with a history
178
self.append(self.button)
179
180
self.service = NotificationsService(self)
181
bus = SessionBus()
182
self.publishing = bus.publish("org.freedesktop.Notifications", self.service)
183
184
def push_notification(self, app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout):
185
new_child = BriefNotification(app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout)
186
new_child.image.set_pixel_size(self.icon_size)
187
self.stack.add_child(new_child)
188
self.stack.set_visible_child(new_child)
189
def remove_notification():
190
def remove_child():
191
self.stack.remove(new_child)
192
193
if self.stack.get_visible_child() == new_child:
194
self.stack.set_visible_child(self.stack.get_visible_child().get_prev_sibling())
195
GLib.timeout_add(self.stack.get_transition_duration(), remove_child)
196
else:
197
# TODO: split the time
198
def readd_timeout(*args):
199
if new_child == self.stack.get_visible_child():
200
GLib.timeout_add(self.stack.get_transition_duration(), remove_notification)
201
self.stack.connect("notify::visible-child", readd_timeout)
202
return False
203
if expire_timeout <= 0:
204
expire_timeout = 2500
205
GLib.timeout_add(expire_timeout, remove_notification)
206
207
self.notification_list.append(DetailedNotification(app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout))
208
self.notification_count_label.set_text(locale.format_string('%d', self.notification_count + 1))
209
self.notification_count += 1
210
# TODO: notify that it was closed
211
212
def get_config(self):
213
return {
214
"icon_size": self.icon_size,
215
}
216
217
def shutdown(self):
218
self.publishing.unpublish()
219