__init__.py
Python script, ASCII text executable
1""" 2D-Bus notifier applet for the Panorama panel. 3Copyright 2025, roundabout-host.com <vlad@roundabout-host.com> 4 5This program is free software: you can redistribute it and/or modify 6it under the terms of the GNU General Public Licence as published by 7the Free Software Foundation, either version 3 of the Licence, or 8(at your option) any later version. 9 10This program is distributed in the hope that it will be useful, 11but WITHOUT ANY WARRANTY; without even the implied warranty of 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13GNU General Public Licence for more details. 14 15You should have received a copy of the GNU General Public Licence 16along with this program. If not, see <https://www.gnu.org/licenses/>. 17""" 18 19from __future__ import annotations 20 21import panorama_panel 22import locale 23from pathlib import Path 24from pydbus import SessionBus 25 26import gi 27gi.require_version("Gtk", "4.0") 28 29from gi.repository import Gtk, Gdk, GLib 30 31 32module_directory = Path(__file__).resolve().parent 33 34locale.bindtextdomain("panorama-app-menu", module_directory / "locale") 35_ = lambda x: locale.dgettext("panorama-app-menu", x) 36 37 38custom_css = """ 39.notification-button { 40padding: 0; 41} 42 43.panorama-panel-notifier-summary { 44font-weight: 600; 45} 46""" 47 48css_provider = Gtk.CssProvider() 49css_provider.load_from_data(custom_css) 50Gtk.StyleContext.add_provider_for_display( 51Gdk.Display.get_default(), 52css_provider, 53100 54) 55 56 57class 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 85def __init__(self, applet: NotifierApplet): 86self._next_id = 1 87self.applet = applet 88 89def GetCapabilities(self): 90return ["body"] 91 92def GetServerInformation(self): 93# name, vendor, version, spec_version 94return "panorama-panel", "panorama", "0.1", "1.2" 95 96def Notify(self, *args): 97notification_id = self._next_id 98self._next_id += 1 99self.applet.push_notification(*args) 100return notification_id 101 102 103class BriefNotification(Gtk.Box): 104def __init__(self, app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout): 105Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=4) 106if not app_icon: 107app_icon = "preferences-desktop-notifications" 108self.image = Gtk.Image.new_from_icon_name(app_icon) 109self.append(self.image) 110self.append(Gtk.Label.new(summary)) 111self.set_halign(Gtk.Align.CENTER) 112 113 114class DetailedNotification(Gtk.ListBoxRow): 115def __init__(self, app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout): 116Gtk.ListBoxRow.__init__(self) 117if not app_icon: 118app_icon = "preferences-desktop-notifications" 119 120self.box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) 121self.set_child(self.box) 122 123self.icon = Gtk.Image.new_from_icon_name(app_icon) 124self.icon.set_icon_size(Gtk.IconSize.LARGE) 125self.box.append(self.icon) 126 127self.text_area = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) 128 129self.summary = Gtk.Label.new(summary) 130self.summary.set_halign(Gtk.Align.START) 131self.summary.set_xalign(0) 132self.summary.add_css_class("panorama-panel-notifier-summary") 133self.text_area.append(self.summary) 134 135self.body = Gtk.Label.new(body) 136self.body.set_halign(Gtk.Align.START) 137self.body.set_xalign(0) 138self.summary.add_css_class("panorama-panel-notifier-body") 139self.text_area.append(self.body) 140 141# TODO: add actions 142 143self.box.append(self.text_area) 144 145 146class NotifierApplet(panorama_panel.Applet): 147name = _("Notification centre") 148description = _("Get desktop notifications") 149 150def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None): 151super().__init__(orientation=orientation, config=config) 152if config is None: 153config = {} 154 155self.icon_size = config.get("icon_size", 24) 156 157self.button = Gtk.MenuButton() 158self.button.set_has_frame(False) 159self.button.add_css_class("notification-button") 160self.stack = Gtk.Stack() 161self.button.set_child(self.stack) 162self.notification_indicator = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) 163self.status_icon = Gtk.Image.new_from_icon_name("emblem-important") 164self.status_icon.set_pixel_size(self.icon_size) 165self.notification_indicator.append(self.status_icon) 166self.notification_indicator.set_halign(Gtk.Align.CENTER) 167self.notification_count_label = Gtk.Label.new(locale.format_string("%d", 0)) 168self.notification_count = 0 169self.notification_indicator.append(self.notification_count_label) 170self.stack.add_child(self.notification_indicator) 171self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_UP_DOWN) 172self.stack.set_transition_duration(500) 173self.popover = Gtk.Popover() 174self.notification_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE) 175self.popover.set_child(self.notification_list) 176self.button.set_popover(self.popover) 177# TODO: support the other parameters; add a popover with a history 178self.append(self.button) 179 180self.service = NotificationsService(self) 181bus = SessionBus() 182self.publishing = bus.publish("org.freedesktop.Notifications", self.service) 183 184def push_notification(self, app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout): 185new_child = BriefNotification(app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout) 186new_child.image.set_pixel_size(self.icon_size) 187self.stack.add_child(new_child) 188self.stack.set_visible_child(new_child) 189def remove_notification(): 190def remove_child(): 191self.stack.remove(new_child) 192 193if self.stack.get_visible_child() == new_child: 194self.stack.set_visible_child(self.stack.get_visible_child().get_prev_sibling()) 195GLib.timeout_add(self.stack.get_transition_duration(), remove_child) 196else: 197# TODO: split the time 198def readd_timeout(*args): 199if new_child == self.stack.get_visible_child(): 200GLib.timeout_add(self.stack.get_transition_duration(), remove_notification) 201self.stack.connect("notify::visible-child", readd_timeout) 202return False 203if expire_timeout <= 0: 204expire_timeout = 2500 205GLib.timeout_add(expire_timeout, remove_notification) 206 207self.notification_list.append(DetailedNotification(app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout)) 208self.notification_count_label.set_text(locale.format_string('%d', self.notification_count + 1)) 209self.notification_count += 1 210# TODO: notify that it was closed 211 212def get_config(self): 213return { 214"icon_size": self.icon_size, 215} 216 217def shutdown(self): 218self.publishing.unpublish() 219