def show_notify(self, dbus_id, tray, nid, app_name, replaces_nid, app_icon, summary, body, actions, hints, expire_timeout, icon): if not self.dbus_check(dbus_id): return self.may_retry = True try: icon_string = self.get_icon_string(nid, app_icon, icon) log("get_icon_string%s=%s", (nid, app_icon, ellipsizer(icon)), icon_string) try: app_str = self.app_name_format % app_name except TypeError: app_str = app_name or "Xpra" self.last_notification = (dbus_id, tray, nid, app_name, replaces_nid, app_icon, summary, body, expire_timeout, icon) def NotifyReply(notification_id): log("NotifyReply(%s) for nid=%i", notification_id, nid) self.actual_notification_id[nid] = int(notification_id) dbus_hints = self.parse_hints(hints) self.dbusnotify.Notify(app_str, 0, icon_string, summary, body, actions, dbus_hints, expire_timeout, reply_handler=NotifyReply, error_handler=self.NotifyError) except Exception: log.error("Error: dbus notify failed", exc_info=True)
def get_gtk_notifier(self): if self.gtk_notifier is None: try: self.gtk_notifier = GTK_Notifier(self.closed_cb, self.action_cb) except: log("failed to load GTK Notifier fallback", exc_info=True) return self.gtk_notifier
def parse_hints(self, h) -> dict: hints = {} for x in ("action-icons", "category", "desktop-entry", "resident", "transient", "x", "y", "urgency"): v = h.get(x) if v is not None: hints[x] = native_to_dbus(v) image_data = h.get("image-data") if image_data and bytestostr(image_data[0]) == "png": try: from xpra.codecs.pillow.decoder import open_only img_data = image_data[3] img = open_only(img_data, ("png", )) w, h = img.size channels = len(img.mode) rowstride = w * channels has_alpha = img.mode == "RGBA" pixel_data = bytearray(img.tobytes("raw", img.mode)) log.info("pixel_data=%s", type(pixel_data)) args = w, h, rowstride, has_alpha, 8, channels, pixel_data hints["image-data"] = tuple(native_to_dbus(x) for x in args) #hints["image-data"] = args except Exception as e: log("parse_hints(%s) error on image-data=%s", h, image_data, exc_info=True) log.error("Error parsing notification image:") log.error(" %s", e) log("parse_hints(%s)=%s", h, hints) #return dbus.types.Dictionary(hints, signature="sv") return hints
def show_notify(self, dbus_id, tray, nid, app_name, replaces_nid, app_icon, summary, body, actions, hints, expire_timeout, icon): getHWND = getattr(tray, "getHWND", None) if GTK_NOTIFIER or tray is None or getHWND is None or actions: log( "show_notify(..) using gtk fallback, GTK_NOTIFIER=%s, tray=%s, getHWND=%s, actions=%s", GTK_NOTIFIER, tray, getHWND, actions) gtk_notifier = self.get_gtk_notifier() if gtk_notifier: gtk_notifier.show_notify(dbus_id, tray, nid, app_name, replaces_nid, app_icon, summary, body, actions, hints, expire_timeout, icon) self.gtk_notifications.add(nid) return if tray is None: log.warn("Warning: no system tray - cannot show notification!") return hwnd = getHWND() app_id = tray.app_id log("show_notify%s hwnd=%#x, app_id=%i", (dbus_id, tray, nid, app_name, replaces_nid, app_icon, summary, body, actions, hints, expire_timeout, icon), hwnd, app_id) #FIXME: remove handles when notification is closed self.notification_handles[nid] = (hwnd, app_id) do_notify(hwnd, app_id, summary, body, expire_timeout, icon)
def NotifyError(self, dbus_error, *_args): try: if isinstance(dbus_error, dbus.exceptions.DBusException): message = dbus_error.get_dbus_message() dbus_error_name = dbus_error.get_dbus_name() if dbus_error_name != "org.freedesktop.DBus.Error.ServiceUnknown": log.error("unhandled dbus exception: %s, %s", message, dbus_error_name) return False if not self.may_retry: log.error("Error: cannot send notification via dbus,") log.error( " check that you notification service is operating properly" ) return False self.may_retry = False log.info("trying to re-connect to the notification service") #try to connect to the notification again (just once): self.setup_dbusnotify() #and retry: self.show_notify(*self.last_notification) except Exception: log("cannot filter error", exc_info=True) log.error("Error processing notification:") log.error(" %s", dbus_error) return False
def get_x(self, w): x = self.stack.get_origin_x() - w//2 if (x + w) >= self.stack.max_width: #dont overflow on the right x = self.stack.max_width - w if x <= 0: #or on the left x = 0 log("get_x(%s)=%s", w, x) return x
def close_notify(self, nid: int): actual_id = self.actual_notification_id.get(nid) if actual_id is None: log( "close_notify(%i) actual notification not found, already closed?", nid) return log("close_notify(%i) actual id=%s", nid, actual_id) self.do_close(nid, actual_id)
def get_y(self, h): y = self.stack.get_origin_y() if y >= (self.stack.max_height//2): #if near bottom, substract window height y = y - h if (y + h) >= self.stack.max_height: y = self.stack.max_height - h if y<= 0: y = 0 log("get_y(%s)=%s", h, y) return y
def hide_notification(self): """Destroys the notification and tells the stack to move the remaining notification windows""" log("hide_notification()") for timer in ("fade_in_timer", "fade_out_timer", "wait_timer"): v = getattr(self, timer) if v: setattr(self, timer, None) glib.source_remove(v) self.destroy() self.destroy_cb(self)
def close_notify(self, nid): try: self.gtk_notifications.remove(nid) except KeyError: try: hwnd, app_id = self.notification_handles.pop(nid) except KeyError: return log("close_notify(%i) hwnd=%i, app_id=%i", nid, hwnd, app_id) notify(hwnd, app_id, "", "", 0, None) else: self.get_gtk_notifier().close_notify(nid)
def do_close(self, nid: int, actual_id: int): log("do_close_notify(%i)", actual_id) def CloseNotificationReply(): self.actual_notification_id.pop(nid, None) def CloseNotificationError(dbus_error, *_args): log.warn("Error: error closing notification:") log.warn(" %s", dbus_error) self.dbusnotify.CloseNotification(actual_id, reply_handler=CloseNotificationReply, error_handler=CloseNotificationError)
def NotificationClosed(self, actual_id: int, reason): nid = self._find_nid(actual_id) reason_str = { 1: "expired", 2: "dismissed by the user", 3: "closed by a call to CloseNotification", 4: "Undefined/reserved reasons", }.get(int(reason), str(reason)) log("NotificationClosed(%s, %s) nid=%s, reason=%s", actual_id, reason, nid, reason_str) if nid: self.actual_notification_id.pop(nid, None) self.clean_notification(nid) if self.closed_cb: self.closed_cb(nid, int(reason), reason_str)
def __init__(self, closed_cb=None, action_cb=None, size_x=DEFAULT_WIDTH, size_y=DEFAULT_HEIGHT, timeout=5): NotifierBase.__init__(self, closed_cb, action_cb) self.handles_actions = True """ Create a new notification stack. The recommended way to create Popup instances. Parameters: `size_x` : The desired width of the notifications. `size_y` : The desired minimum height of the notifications. If the text is longer it will be expanded to fit. `timeout` : Popup instance will disappear after this timeout if there is no human intervention. This can be overridden temporarily by passing a new timout to the new_popup method. """ self.size_x = size_x self.size_y = size_y self.timeout = timeout """ Other parameters: These will take effect for every popup created after the change. `max_popups` : The maximum number of popups to be shown on the screen at one time. `bg_color` : if None default is used (usually grey). set with a gdk.Color. `fg_color` : if None default is used (usually black). set with a gdk.Color. `show_timeout : if True, a countdown till destruction will be displayed. """ self.max_popups = 5 self.fg_color = DEFAULT_FG_COLOUR self.bg_color = DEFAULT_BG_COLOUR self.show_timeout = False self._notify_stack = [] self._offset = 0 display = display_get_default() screen = display.get_default_screen() n = screen.get_n_monitors() log("screen=%s, monitors=%s", screen, n) if n < 2: self.max_width = screen.get_width() self.max_height = screen.get_height() log("screen dimensions: %dx%d", self.max_width, self.max_height) else: rect = screen.get_monitor_geometry(0) self.max_width = rect.width self.max_height = rect.height log("first monitor dimensions: %dx%d", self.max_width, self.max_height) self.x = self.max_width - 20 #keep away from the edge self.y = self.max_height - 64 #space for a panel log("our reduced dimensions: %dx%d", self.x, self.y)
def setup_dbusnotify(self): self.dbus_session = dbus.SessionBus() FD_NOTIFICATIONS = 'org.freedesktop.Notifications' self.org_fd_notifications = self.dbus_session.get_object( FD_NOTIFICATIONS, '/org/freedesktop/Notifications') self.org_fd_notifications.connect_to_signal("NotificationClosed", self.NotificationClosed) self.org_fd_notifications.connect_to_signal("ActionInvoked", self.ActionInvoked) #connect_to_signal("HelloSignal", hello_signal_handler, dbus_interface="com.example.TestService", arg0="Hello") self.dbusnotify = dbus.Interface(self.org_fd_notifications, FD_NOTIFICATIONS) log("using dbusnotify: %s(%s)", type(self.dbusnotify), FD_NOTIFICATIONS) caps = tuple(str(x) for x in self.dbusnotify.GetCapabilities()) log("capabilities=%s", csv(caps)) self.handles_actions = "actions" in caps log("dbus.get_default_main_loop()=%s", dbus.get_default_main_loop())
def popup_cb_clicked(*args): self.hide_notification() log("popup_cb_clicked%s for action_id=%s, action_text=%s", args, action_id, action_text) self.action_cb(self.nid, action_id)
def __init__(self, stack, nid, title, message, actions, image, timeout=5, show_timeout=False): log("Popup%s", (stack, nid, title, message, actions, image, timeout, show_timeout)) self.stack = stack self.nid = nid super().__init__() self.set_accept_focus(False) self.set_focus_on_map(False) self.set_size_request(stack.size_x, -1) self.set_decorated(False) self.set_deletable(False) self.set_property("skip-pager-hint", True) self.set_property("skip-taskbar-hint", True) self.connect("enter-notify-event", self.on_hover, True) self.connect("leave-notify-event", self.on_hover, False) self.set_opacity(0.2) self.set_keep_above(True) self.destroy_cb = stack.destroy_popup_cb self.popup_closed = stack.popup_closed self.action_cb = stack.popup_action main_box = Gtk.VBox() header_box = Gtk.HBox() self.header = Gtk.Label() self.header.set_padding(3, 3) self.header.set_alignment(0, 0) header_box.pack_start(self.header, True, True, 5) icon = get_icon_pixbuf("close.png") if icon: close_button = Gtk.Image() close_button.set_from_pixbuf(icon) close_button.set_padding(3, 3) close_window = Gtk.EventBox() close_window.set_visible_window(False) close_window.connect("button-press-event", self.user_closed) close_window.add(close_button) close_window.set_size_request(icon.get_width(), icon.get_height()) header_box.pack_end(close_window, False, False, 0) main_box.pack_start(header_box) body_box = Gtk.HBox() self.image = Gtk.Image() self.image.set_size_request(70, 70) self.image.set_alignment(0, 0) body_box.pack_start(self.image, False, False, 5) self.message = Gtk.Label() self.message.set_max_width_chars(80) self.message.set_size_request(stack.size_x - 90, -1) self.message.set_line_wrap(True) self.message.set_alignment(0, 0) self.message.set_padding(5, 10) self.counter = Gtk.Label() self.counter.set_alignment(1, 1) self.counter.set_padding(3, 3) self.timeout = timeout body_box.pack_start(self.message, True, False, 5) body_box.pack_end(self.counter, False, False, 5) main_box.pack_start(body_box, False, False, 5) self.buttons_box = Gtk.HBox(homogeneous=True) alignment = Gtk.Alignment(xalign=1.0, yalign=0.5, xscale=0.0, yscale=0.0) alignment.add(self.buttons_box) main_box.pack_start(alignment) self.add(main_box) if stack.bg_color is not None: self.modify_bg(Gtk.StateType.NORMAL, stack.bg_color) if stack.fg_color is not None: self.message.modify_fg(Gtk.StateType.NORMAL, stack.fg_color) self.header.modify_fg(Gtk.StateType.NORMAL, stack.fg_color) self.counter.modify_fg(Gtk.StateType.NORMAL, stack.fg_color) self.show_timeout = show_timeout self.hover = False self.show_all() self.w = self.get_preferred_width()[0] self.h = self.get_preferred_height()[0] self.move(self.get_x(self.w), self.get_y(self.h)) self.wait_timer = None self.fade_out_timer = None self.fade_in_timer = GLib.timeout_add(100, self.fade_in) #populate the window: self.set_content(title, message, actions, image) #ensure we dont show it in the taskbar: self.realize() self.get_window().set_skip_taskbar_hint(True) self.get_window().set_skip_pager_hint(True) add_close_accel(self, self.user_closed)
def ActionInvoked(self, actual_id: int, action): nid = self._find_nid(actual_id) log("ActionInvoked(%s, %s) nid=%s", actual_id, action, nid) if nid: if self.action_cb: self.action_cb(nid, str(action))
def NotifyReply(notification_id): log("NotifyReply(%s) for nid=%i", notification_id, nid) self.actual_notification_id[nid] = int(notification_id)
def reposition(self, offset, stack): """Move the notification window down, when an older notification is removed""" log("reposition(%s, %s)", offset, stack) new_offset = self.h + offset GLib.idle_add(self.move, self.get_x(self.w), self.get_y(new_offset)) return new_offset
def __init__(self, stack, nid, title, message, actions, image, timeout=5, show_timeout=False): log("Popup%s", (stack, nid, title, message, actions, image, timeout, show_timeout)) self.stack = stack self.nid = nid gtk.Window.__init__(self) self.set_size_request(stack.size_x, -1) self.set_decorated(False) self.set_deletable(False) self.set_property("skip-pager-hint", True) self.set_property("skip-taskbar-hint", True) self.connect("enter-notify-event", self.on_hover, True) self.connect("leave-notify-event", self.on_hover, False) self.set_opacity(0.2) self.set_keep_above(True) self.destroy_cb = stack.destroy_popup_cb self.popup_closed = stack.popup_closed self.action_cb = stack.popup_action main_box = gtk.VBox() header_box = gtk.HBox() self.header = gtk.Label() self.header.set_markup("<b>%s</b>" % title) self.header.set_padding(3, 3) self.header.set_alignment(0, 0) header_box.pack_start(self.header, True, True, 5) icon = get_pixbuf("close.png") if icon: close_button = gtk.Image() close_button.set_from_pixbuf(icon) close_button.set_padding(3, 3) close_window = gtk.EventBox() close_window.set_visible_window(False) close_window.connect("button-press-event", self.user_closed) close_window.add(close_button) close_window.set_size_request(icon.get_width(), icon.get_height()) header_box.pack_end(close_window, False, False) main_box.pack_start(header_box) body_box = gtk.HBox() if image is not None: self.image = gtk.Image() self.image.set_size_request(70, 70) self.image.set_alignment(0, 0) self.image.set_from_pixbuf(image) body_box.pack_start(self.image, False, False, 5) self.message = gtk.Label() self.message.set_max_width_chars(80) self.message.set_size_request(stack.size_x - 90, -1) self.message.set_line_wrap(True) self.message.set_alignment(0, 0) self.message.set_padding(5, 10) self.message.set_text(message) self.counter = gtk.Label() self.counter.set_alignment(1, 1) self.counter.set_padding(3, 3) self.timeout = timeout body_box.pack_start(self.message, True, False, 5) body_box.pack_end(self.counter, False, False, 5) main_box.pack_start(body_box, False, False, 5) if len(actions)>=2: buttons_box = gtk.HBox(True) while len(actions)>=2: action_id, action_text = actions[:2] actions = actions[2:] button = self.action_button(action_id, action_text) buttons_box.add(button) alignment = gtk.Alignment(xalign=1.0, yalign=0.5, xscale=0.0, yscale=0.0) alignment.add(buttons_box) main_box.pack_start(alignment) self.add(main_box) if stack.bg_color is not None: self.modify_bg(STATE_NORMAL, stack.bg_color) if stack.fg_color is not None: self.message.modify_fg(STATE_NORMAL, stack.fg_color) self.header.modify_fg(STATE_NORMAL, stack.fg_color) self.counter.modify_fg(STATE_NORMAL, stack.fg_color) self.show_timeout = show_timeout self.hover = False self.show_all() self.w, self.h = get_preferred_size(self) self.move(self.get_x(self.w), self.get_y(self.h)) self.wait_timer = None self.fade_out_timer = None self.fade_in_timer = glib.timeout_add(100, self.fade_in) #ensure we dont show it in the taskbar: self.realize() self.get_window().set_skip_taskbar_hint(True) self.get_window().set_skip_pager_hint(True) add_close_accel(self, self.user_closed)