From a99e3fa56ba50a4aa93791c430213eddbc32d8d5 Mon Sep 17 00:00:00 2001 From: Mikhail Novosyolov Date: Mon, 15 Jun 2026 15:24:49 +0300 Subject: [PATCH] Modernize tray icon: use AyatanaAppIndicator3 (SNI) - Replace deprecated Gtk.StatusIcon (XEmbed) with AyatanaAppIndicator3 (StatusNotifierItem protocol), shown natively in KDE Plasma and other modern desktops. The old XEmbed-based icon was not displayed in KDE Plasma 6, which dropped support for legacy system tray icons. - Use icon names from system theme: printer, printer-printing, printer-error, printer-warning - Support left-click (Activate signal, libayatana-appindicator >= 0.6.0) and tooltip (set_tooltip_title) with graceful fallback on older versions - Lazy import of AyatanaAppIndicator3 (only when applet mode is used) - Enable autostart in KDE (remove from NotShowIn in print-applet.desktop.in) Co-authored-by: Z.AI GLM --- jobviewer.py | 157 +++++++++++++++----------------- print-applet.desktop.in | 2 +- system-config-printer-applet.in | 18 +++- 3 files changed, 89 insertions(+), 88 deletions(-) diff --git a/jobviewer.py b/jobviewer.py index 6c9e1e57e..63f670c50 100644 --- a/jobviewer.py +++ b/jobviewer.py @@ -394,7 +394,6 @@ def __init__(self, bus=None, loop=None, self.specific_dests = specific_dests notify_caps = Notify.get_server_caps () self.notify_has_actions = "actions" in notify_caps - self.notify_has_persistence = "persistence" in notify_caps self.jobs = {} self.jobiters = {} @@ -587,22 +586,8 @@ def load_icon(theme, icon): theme = Gtk.IconTheme.get_default () self.icon_jobs = load_icon (theme, ICON) self.icon_jobs_processing = load_icon (theme, "printer-printing") - self.icon_no_jobs = self.icon_jobs.copy () - self.icon_no_jobs.fill (0) - self.icon_jobs.composite (self.icon_no_jobs, - 0, 0, - self.icon_no_jobs.get_width(), - self.icon_no_jobs.get_height(), - 0, 0, - 1.0, 1.0, - GdkPixbuf.InterpType.BILINEAR, - 127) - if self.applet and not self.notify_has_persistence: - self.statusicon = Gtk.StatusIcon () - self.statusicon.set_from_pixbuf (self.icon_no_jobs) - self.statusicon.connect ('activate', self.toggle_window_display) - self.statusicon.connect ('popup-menu', self.on_icon_popupmenu) - self.statusicon.set_visible (False) + if self.applet: + self._setup_tray_icon () # D-Bus if bus is None: @@ -704,11 +689,46 @@ def cleanup (self): for op in self.ops: op.destroy () - if self.applet and not self.notify_has_persistence: - self.statusicon.set_visible (False) + if self.applet: + self._set_tray_visible (False) self.emit ('finished') + def _setup_tray_icon (self): + """Set up the system tray icon using AyatanaAppIndicator3 + (StatusNotifierItem protocol).""" + gi.require_version('AyatanaAppIndicator3', '0.1') + from gi.repository import AyatanaAppIndicator3 as AppIndicator + self._AppIndicator = AppIndicator + self.statusicon_popupmenu.show_all () + self.indicator = AppIndicator.Indicator.new ( + "system-config-printer-applet", + "printer", + AppIndicator.IndicatorCategory.APPLICATION_STATUS) + self.indicator.set_title (_("Print Queue Applet")) + self.indicator.set_icon_full ("printer", _("Print queue")) + self.indicator.set_menu (self.statusicon_popupmenu) + # Middle-click activates the first menu item ("Hide"). + items = self.statusicon_popupmenu.get_children () + if items: + self.indicator.set_secondary_activate_target (items[0]) + # Left-click toggles the jobs window. Requires libayatana-appindicator + # >= 0.6.0 (Activate D-Bus method support); on older versions the + # signal is silently ignored and the panel falls back to showing + # the context menu. + self.indicator.connect ("activate", self._on_indicator_activate) + self.indicator.set_status (AppIndicator.IndicatorStatus.PASSIVE) + + def _on_indicator_activate (self, indicator, x, y): + self.toggle_window_display () + + def _set_tray_visible (self, visible): + AppIndicator = self._AppIndicator + if visible: + self.indicator.set_status (AppIndicator.IndicatorStatus.ACTIVE) + else: + self.indicator.set_status (AppIndicator.IndicatorStatus.PASSIVE) + def set_process_pending (self, whether): self.process_pending_events = whether @@ -733,35 +753,19 @@ def job_attributes_on_delete_event(self, widget, event=None): def show_IPP_Error(self, exception, message): return errordialogs.show_IPP_Error (exception, message, self.JobsWindow) - def toggle_window_display(self, icon, force_show=False): + def toggle_window_display(self, icon=None, force_show=False): visible = getattr (self.JobsWindow, 'visible', None) if force_show: visible = False - if self.notify_has_persistence: - if visible: - self.JobsWindow.hide () - else: - self.JobsWindow.show () + if visible: + self.JobsWindow.set_visible (False) else: - if visible: - w = self.JobsWindow.get_window() - aw = self.JobsAttributesWindow.get_window() - (loc, s, area, o) = self.statusicon.get_geometry () - - if loc: - w.set_skip_taskbar_hint (True) - if aw is not None: - aw.set_skip_taskbar_hint (True) - self.JobsWindow.iconify () - else: - self.JobsWindow.set_visible (False) - else: - self.JobsWindow.present () - self.JobsWindow.set_skip_taskbar_hint (False) - aw = self.JobsAttributesWindow.get_window() - if aw is not None: - aw.set_skip_taskbar_hint (False) + self.JobsWindow.present () + self.JobsWindow.set_skip_taskbar_hint (False) + aw = self.JobsAttributesWindow.get_window() + if aw is not None: + aw.set_skip_taskbar_hint (False) self.JobsWindow.visible = not visible @@ -1223,16 +1227,9 @@ def set_statusicon_visibility (self): debugprint ("num_jobs: %d" % num_jobs) debugprint ("num_jobs_when_hidden: %d" % self.num_jobs_when_hidden) - if self.notify_has_persistence: - return - - # Don't handle tooltips during the mainloop recursion at the - # end of this function as it seems to cause havoc (bug #664044, - # bug #739745). - self.statusicon.set_has_tooltip (False) + show = open_notifications > 0 or num_jobs > self.num_jobs_when_hidden - self.statusicon.set_visible (open_notifications > 0 or - num_jobs > self.num_jobs_when_hidden) + self._set_tray_visible (show) # Let the icon show/hide itself before continuing. while self.process_pending_events and Gtk.events_pending (): @@ -1349,9 +1346,6 @@ def show_treeview_popup_menu (self, treeview, event, event_button): self.job_context_menu.popup (None, None, None, None, event_button, event.get_time ()) - def on_icon_popupmenu(self, icon, button, time): - self.statusicon_popupmenu.popup (None, None, None, None, button, time) - def on_icon_hide_activate(self, menuitem): self.num_jobs_when_hidden = len (self.jobs.keys ()) self.set_statusicon_visibility () @@ -1726,30 +1720,6 @@ def add_state_reason_emblem (self, pixbuf, printer=None): return pixbuf - def get_icon_pixbuf (self, have_jobs=None): - if not self.applet: - return - - if have_jobs is None: - have_jobs = len (self.jobs.keys ()) > 0 - - if have_jobs: - pixbuf = self.icon_jobs - for jobid, jobdata in self.jobs.items (): - jstate = jobdata.get ('job-state', cups.IPP_JOB_PENDING) - if jstate == cups.IPP_JOB_PROCESSING: - pixbuf = self.icon_jobs_processing - break - else: - pixbuf = self.icon_no_jobs - - try: - pixbuf = self.add_state_reason_emblem (pixbuf) - except: - nonfatalException () - - return pixbuf - def set_statusicon_tooltip (self, tooltip=None): if not self.applet: return @@ -1763,9 +1733,11 @@ def set_statusicon_tooltip (self, tooltip=None): else: tooltip = _("%d documents queued") % num_jobs - self.statusicon.set_tooltip_markup (tooltip) + # set_tooltip_title requires libayatana-appindicator >= 0.6.0 + if hasattr(self.indicator, 'set_tooltip_title'): + self.indicator.set_tooltip_title (tooltip) - def update_status (self, have_jobs=None): + def update_status (self): # Found out which printer state reasons apply to our active jobs. upset_printers = set() for printer, reasons in self.printer_state_reasons.items (): @@ -1799,13 +1771,13 @@ def update_status (self, have_jobs=None): Gdk.threads_enter () self.statusbar.pop (0) + processing = 0 if self.worst_reason is not None: (title, tooltip) = self.worst_reason.get_description () self.statusbar.push (0, tooltip) else: tooltip = None status_message = "" - processing = 0 pending = 0 for jobid in self.active_jobs: try: @@ -1820,9 +1792,22 @@ def update_status (self, have_jobs=None): status_message = _("processing / pending: %d / %d") % (processing, pending) self.statusbar.push(0, status_message) - if self.applet and not self.notify_has_persistence: - pixbuf = self.get_icon_pixbuf (have_jobs=have_jobs) - self.statusicon.set_from_pixbuf (pixbuf) + if self.applet: + # Tray icon reflects the most severe current condition: + # error > warning > processing > idle. + if self.worst_reason is not None: + level = self.worst_reason.get_level () + if level >= StateReason.ERROR: + icon_name = "printer-error" + elif level >= StateReason.WARNING: + icon_name = "printer-warning" + else: + icon_name = "printer" + elif processing > 0: + icon_name = "printer-printing" + else: + icon_name = "printer" + self.indicator.set_icon_full (icon_name, _("Print queue")) self.set_statusicon_visibility () self.set_statusicon_tooltip (tooltip=tooltip) @@ -1997,7 +1982,7 @@ def job_added (self, mon, jobid, eventname, event, jobdata): elif jobid in self.active_jobs: self.active_jobs.remove (jobid) - self.update_status (have_jobs=True) + self.update_status () if self.applet: if not self.job_is_active (jobdata): return diff --git a/print-applet.desktop.in b/print-applet.desktop.in index 1bc003f39..46d493823 100644 --- a/print-applet.desktop.in +++ b/print-applet.desktop.in @@ -5,6 +5,6 @@ Exec=system-config-printer-applet Terminal=false Type=Application Icon=printer -NotShowIn=KDE;GNOME;Cinnamon; +NotShowIn=GNOME;Cinnamon; StartupNotify=false X-GNOME-Autostart-Delay=30 diff --git a/system-config-printer-applet.in b/system-config-printer-applet.in index 22b5f7581..69d40cf65 100755 --- a/system-config-printer-applet.in +++ b/system-config-printer-applet.in @@ -1,3 +1,19 @@ #!/bin/sh prefix=@prefix@ -exec @datarootdir@/@PACKAGE@/applet.py "$@" + +force=no +args="" +for arg in "$@"; do + case "$arg" in + --force) force=yes ;; + *) args="$args $arg" ;; + esac +done + +if [ "$force" = "no" ] && command -v kde-print-queue >/dev/null 2>&1; then + echo "system-config-printer-applet: kde-print-queue found — plasma-print-manager is likely installed." >&2 + echo "The tray applet would duplicate its functionality. Use --force to override." >&2 + exit 0 +fi + +exec @datarootdir@/@PACKAGE@/applet.py $args