%PDF- %PDF-
Direktori : /usr/share/system-config-printer/ |
Current File : //usr/share/system-config-printer/jobviewer.py |
## Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015 Red Hat, Inc. ## Authors: ## Tim Waugh <twaugh@redhat.com> ## Jiri Popelka <jpopelka@redhat.com> ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import asyncconn import authconn import cups import dbus import dbus.glib import dbus.service import threading import gi gi.require_version('Notify', '0.7') from gi.repository import Notify from gi.repository import GLib from gi.repository import GObject from gi.repository import Gdk from gi.repository import GdkPixbuf from gi.repository import Gtk from gui import GtkGUI import monitor import os, shutil from gi.repository import Pango import pwd import smburi import subprocess import sys import time import urllib.parse from xml.sax import saxutils from debug import * import config import statereason import errordialogs from functools import reduce cups.require("1.9.47") try: gi.require_version('Secret', '1') from gi.repository import Secret USE_SECRET=True except ValueError: USE_SECRET=False import gettext gettext.install(domain=config.PACKAGE, localedir=config.localedir) from statereason import StateReason pkgdata = config.pkgdatadir ICON="printer" ICON_SIZE=22 SEARCHING_ICON="document-print-preview" # We need to call Notify.init before we can check the server for caps Notify.init('System Config Printer Notification') if USE_SECRET: NETWORK_PASSWORD = Secret.Schema.new("org.system.config.printer.store", Secret.SchemaFlags.NONE, { "user": Secret.SchemaAttributeType.STRING, "domain": Secret.SchemaAttributeType.STRING, "object": Secret.SchemaAttributeType.STRING, "protocol": Secret.SchemaAttributeType.STRING, "port": Secret.SchemaAttributeType.INTEGER, "server": Secret.SchemaAttributeType.STRING, "authtype": Secret.SchemaAttributeType.STRING, "uri": Secret.SchemaAttributeType.STRING, } ) class ServiceGet: service = Secret.Service() def __init__(self): self.service = Secret.Service.get_sync(0, None) def get_service(self): return self.service class ItemSearch: items = list() def __init__(self, service, attrs): self.items = Secret.Service.search_sync(service, NETWORK_PASSWORD, attrs, Secret.SearchFlags.LOAD_SECRETS, None) def get_items(self): return self.items class PasswordStore: def __init__(self, attrs, name, secret): Secret.password_store(NETWORK_PASSWORD, attrs, Secret.COLLECTION_DEFAULT, name, secret, None, self.on_password_stored) def on_password_stored(self, source, result, unused): Secret.password_store_finish(result) class PrinterURIIndex: def __init__ (self, names=None): self.printer = {} if names is None: names = [] self.names = names self._collect_names () def _collect_names (self, connection=None): if not self.names: return if not connection: try: c = cups.Connection () except RuntimeError: return for name in self.names: self.add_printer (name, connection=c) self.names = [] def add_printer (self, printer, connection=None): try: self._map_printer (name=printer, connection=connection) except KeyError: return def update_from_attrs (self, printer, attrs): uris = [] if 'printer-uri-supported' in attrs: uri_supported = attrs['printer-uri-supported'] if type (uri_supported) != list: uri_supported = [uri_supported] uris.extend (uri_supported) if 'notify-printer-uri' in attrs: uris.append (attrs['notify-printer-uri']) if 'printer-more-info' in attrs: uris.append (attrs['printer-more-info']) for uri in uris: self.printer[uri] = printer def remove_printer (self, printer): # Remove references to this printer in the URI map. self._collect_names () uris = list(self.printer.keys ()) for uri in uris: if self.printer[uri] == printer: del self.printer[uri] def lookup (self, uri, connection=None): self._collect_names () try: return self.printer[uri] except KeyError: return self._map_printer (uri=uri, connection=connection) def all_printer_names (self): self._collect_names () return set (self.printer.values ()) def lookup_cached_by_name (self, name): self._collect_names () for uri, printer in self.printer.items (): if printer == name: return uri raise KeyError def _map_printer (self, uri=None, name=None, connection=None): try: if connection is None: connection = cups.Connection () r = ['printer-name', 'printer-uri-supported', 'printer-more-info'] if uri is not None: attrs = connection.getPrinterAttributes (uri=uri, requested_attributes=r) else: attrs = connection.getPrinterAttributes (name, requested_attributes=r) except RuntimeError: # cups.Connection() failed raise KeyError except cups.IPPError: # URI not known. raise KeyError name = attrs['printer-name'] self.update_from_attrs (name, attrs) if uri is not None: self.printer[uri] = name return name class CancelJobsOperation(GObject.GObject): __gsignals__ = { 'destroy': (GObject.SignalFlags.RUN_LAST, None, ()), 'job-deleted': (GObject.SignalFlags.RUN_LAST, None, (int,)), 'ipp-error': (GObject.SignalFlags.RUN_LAST, None, (int, GObject.TYPE_PYOBJECT)), 'finished': (GObject.SignalFlags.RUN_LAST, None, ()) } def __init__ (self, parent, host, port, encryption, jobids, purge_job): GObject.GObject.__init__ (self) self.jobids = list (jobids) self.purge_job = purge_job self.host = host self.port = port self.encryption = encryption if purge_job: if len(self.jobids) > 1: dialog_title = _("Delete Jobs") dialog_label = _("Do you really want to delete these jobs?") else: dialog_title = _("Delete Job") dialog_label = _("Do you really want to delete this job?") else: if len(self.jobids) > 1: dialog_title = _("Cancel Jobs") dialog_label = _("Do you really want to cancel these jobs?") else: dialog_title = _("Cancel Job") dialog_label = _("Do you really want to cancel this job?") dialog = Gtk.Dialog (title=dialog_title, transient_for=parent, modal=True, destroy_with_parent=True) dialog.add_buttons (_("Keep Printing"), Gtk.ResponseType.NO, dialog_title, Gtk.ResponseType.YES) dialog.set_default_response (Gtk.ResponseType.NO) dialog.set_border_width (6) dialog.set_resizable (False) hbox = Gtk.HBox.new (False, 12) image = Gtk.Image () image.set_from_stock (Gtk.STOCK_DIALOG_QUESTION, Gtk.IconSize.DIALOG) image.set_alignment (0.0, 0.0) hbox.pack_start (image, False, False, 0) label = Gtk.Label(label=dialog_label) label.set_line_wrap (True) label.set_alignment (0.0, 0.0) hbox.pack_start (label, False, False, 0) dialog.vbox.pack_start (hbox, False, False, 0) dialog.connect ("response", self.on_job_cancel_prompt_response) dialog.connect ("delete-event", self.on_job_cancel_prompt_delete) dialog.show_all () self.dialog = dialog self.connection = None debugprint ("+%s" % self) def __del__ (self): debugprint ("-%s" % self) def do_destroy (self): if self.connection: self.connection.destroy () self.connection = None if self.dialog: self.dialog.destroy () self.dialog = None debugprint ("DESTROY: %s" % self) def destroy (self): self.emit ('destroy') def on_job_cancel_prompt_delete (self, dialog, event): self.on_job_cancel_prompt_response (dialog, Gtk.ResponseType.NO) def on_job_cancel_prompt_response (self, dialog, response): dialog.destroy () self.dialog = None if response != Gtk.ResponseType.YES: self.emit ('finished') return if len(self.jobids) == 0: self.emit ('finished') return asyncconn.Connection (host=self.host, port=self.port, encryption=self.encryption, reply_handler=self._connected, error_handler=self._connect_failed) def _connect_failed (self, connection, exc): debugprint ("CancelJobsOperation._connect_failed %s:%s" % (connection, repr (exc))) def _connected (self, connection, result): self.connection = connection if self.purge_job: operation = _("deleting job") else: operation = _("canceling job") self.connection._begin_operation (operation) self.connection.cancelJob (self.jobids[0], self.purge_job, reply_handler=self.cancelJob_finish, error_handler=self.cancelJob_error) def cancelJob_error (self, connection, exc): debugprint ("cancelJob_error %s:%s" % (connection, repr (exc))) if type (exc) == cups.IPPError: (e, m) = exc.args if (e != cups.IPP_NOT_POSSIBLE and e != cups.IPP_NOT_FOUND): self.emit ('ipp-error', self.jobids[0], exc) self.cancelJob_finish(connection, None) else: self.connection._end_operation () self.connection.destroy () self.connection = None self.emit ('ipp-error', self.jobids[0], exc) # Give up. self.emit ('finished') return def cancelJob_finish (self, connection, result): debugprint ("cancelJob_finish %s:%s" % (connection, repr (result))) self.emit ('job-deleted', self.jobids[0]) del self.jobids[0] if not self.jobids: # Last job canceled. self.connection._end_operation () self.connection.destroy () self.connection = None self.emit ('finished') return else: # there are other jobs to cancel/delete connection.cancelJob (self.jobids[0], self.purge_job, reply_handler=self.cancelJob_finish, error_handler=self.cancelJob_error) class JobViewer (GtkGUI): required_job_attributes = set(['job-k-octets', 'job-name', 'job-originating-user-name', 'job-printer-uri', 'job-state', 'time-at-creation', 'auth-info-required', 'job-preserved']) __gsignals__ = { 'finished': (GObject.SignalFlags.RUN_LAST, None, ()) } def __init__(self, bus=None, loop=None, applet=False, suppress_icon_hide=False, my_jobs=True, specific_dests=None, parent=None): GObject.GObject.__init__ (self) self.loop = loop self.applet = applet self.suppress_icon_hide = suppress_icon_hide self.my_jobs = my_jobs 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 = {} self.jobids = [] self.jobs_attrs = {} # dict of jobid->(GtkListStore, page_index) self.active_jobs = set() # of job IDs self.stopped_job_prompts = set() # of job IDs self.printer_state_reasons = {} self.num_jobs_when_hidden = 0 self.connecting_to_device = {} # dict of printer->time first seen self.state_reason_notifications = {} self.auth_info_dialogs = {} # by job ID self.job_creation_times_timer = None self.new_printer_notifications = {} self.completed_job_notifications = {} self.authenticated_jobs = set() # of job IDs self.ops = [] self.getWidgets ({"JobsWindow": ["JobsWindow", "treeview", "statusbar", "toolbar"], "statusicon_popupmenu": ["statusicon_popupmenu"]}, domain=config.PACKAGE) job_action_group = Gtk.ActionGroup (name="JobActionGroup") job_action_group.add_actions ([ ("cancel-job", Gtk.STOCK_CANCEL, _("_Cancel"), None, _("Cancel selected jobs"), self.on_job_cancel_activate), ("delete-job", Gtk.STOCK_DELETE, _("_Delete"), None, _("Delete selected jobs"), self.on_job_delete_activate), ("hold-job", Gtk.STOCK_MEDIA_PAUSE, _("_Hold"), None, _("Hold selected jobs"), self.on_job_hold_activate), ("release-job", Gtk.STOCK_MEDIA_PLAY, _("_Release"), None, _("Release selected jobs"), self.on_job_release_activate), ("reprint-job", Gtk.STOCK_REDO, _("Re_print"), None, _("Reprint selected jobs"), self.on_job_reprint_activate), ("retrieve-job", Gtk.STOCK_SAVE_AS, _("Re_trieve"), None, _("Retrieve selected jobs"), self.on_job_retrieve_activate), ("move-job", None, _("_Move To"), None, None, None), ("authenticate-job", None, _("_Authenticate"), None, None, self.on_job_authenticate_activate), ("job-attributes", None, _("_View Attributes"), None, None, self.on_job_attributes_activate), ("close", Gtk.STOCK_CLOSE, None, "<ctrl>w", _("Close this window"), self.on_delete_event) ]) self.job_ui_manager = Gtk.UIManager () self.job_ui_manager.insert_action_group (job_action_group, -1) self.job_ui_manager.add_ui_from_string ( """ <ui> <accelerator action="cancel-job"/> <accelerator action="delete-job"/> <accelerator action="hold-job"/> <accelerator action="release-job"/> <accelerator action="reprint-job"/> <accelerator action="retrieve-job"/> <accelerator action="move-job"/> <accelerator action="authenticate-job"/> <accelerator action="job-attributes"/> <accelerator action="close"/> </ui> """ ) self.job_ui_manager.ensure_update () self.JobsWindow.add_accel_group (self.job_ui_manager.get_accel_group ()) self.job_context_menu = Gtk.Menu () for action_name in ["cancel-job", "delete-job", "hold-job", "release-job", "reprint-job", "retrieve-job", "move-job", None, "authenticate-job", "job-attributes"]: if not action_name: item = Gtk.SeparatorMenuItem () else: action = job_action_group.get_action (action_name) action.set_sensitive (False) item = action.create_menu_item () if action_name == 'move-job': self.move_job_menuitem = item printers = Gtk.Menu () item.set_submenu (printers) item.show () self.job_context_menu.append (item) for action_name in ["cancel-job", "delete-job", "hold-job", "release-job", "reprint-job", "retrieve-job", "close"]: action = job_action_group.get_action (action_name) action.set_sensitive (action_name == "close") action.set_is_important (action_name == "close") item = action.create_tool_item () item.show () self.toolbar.insert (item, -1) for skip, ellipsize, name, setter in \ [(False, False, _("Job"), self._set_job_job_number_text), (True, False, _("User"), self._set_job_user_text), (False, True, _("Document"), self._set_job_document_text), (False, True, _("Printer"), self._set_job_printer_text), (False, False, _("Size"), self._set_job_size_text)]: if applet and skip: # Skip the user column when running as applet. continue cell = Gtk.CellRendererText() if ellipsize: # Ellipsize the 'Document' and 'Printer' columns. cell.set_property ("ellipsize", Pango.EllipsizeMode.END) cell.set_property ("width-chars", 20) column = Gtk.TreeViewColumn(name, cell) column.set_cell_data_func (cell, setter, None) column.set_resizable(True) self.treeview.append_column(column) cell = Gtk.CellRendererText () column = Gtk.TreeViewColumn (_("Time submitted"), cell, text=1) column.set_resizable (True) self.treeview.append_column (column) column = Gtk.TreeViewColumn (_("Status")) icon = Gtk.CellRendererPixbuf () column.pack_start (icon, False) text = Gtk.CellRendererText () text.set_property ("ellipsize", Pango.EllipsizeMode.END) text.set_property ("width-chars", 20) column.pack_start (text, True) column.set_cell_data_func (icon, self._set_job_status_icon, None) column.set_cell_data_func (text, self._set_job_status_text, None) self.treeview.append_column (column) self.store = Gtk.TreeStore(int, str) self.store.set_sort_column_id (0, Gtk.SortType.DESCENDING) self.treeview.set_model(self.store) self.treeview.set_rules_hint (True) self.selection = self.treeview.get_selection() self.selection.set_mode(Gtk.SelectionMode.MULTIPLE) self.selection.connect('changed', self.on_selection_changed) self.treeview.connect ('button_release_event', self.on_treeview_button_release_event) self.treeview.connect ('popup-menu', self.on_treeview_popup_menu) self.JobsWindow.set_icon_name (ICON) self.JobsWindow.hide () if specific_dests: the_dests = reduce (lambda x, y: x + ", " + y, specific_dests) if my_jobs: if specific_dests: title = _("my jobs on %s") % the_dests else: title = _("my jobs") else: if specific_dests: title = "%s" % the_dests else: title = _("all jobs") self.JobsWindow.set_title (_("Document Print Status (%s)") % title) if parent: self.JobsWindow.set_transient_for (parent) def load_icon(theme, icon): try: pixbuf = theme.load_icon (icon, ICON_SIZE, 0) except GObject.GError: debugprint ("No %s icon available" % icon) # Just create an empty pixbuf. pixbuf = GdkPixbuf.Pixbuf.new (GdkPixbuf.Colorspace.RGB, True, 8, ICON_SIZE, ICON_SIZE) pixbuf.fill (0) return pixbuf 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) # D-Bus if bus is None: bus = dbus.SystemBus () self.connect_signals () self.set_process_pending (True) self.host = cups.getServer () self.port = cups.getPort () self.encryption = cups.getEncryption () self.monitor = monitor.Monitor (bus=bus, my_jobs=my_jobs, specific_dests=specific_dests, host=self.host, port=self.port, encryption=self.encryption) self.monitor.connect ('refresh', self.on_refresh) self.monitor.connect ('job-added', self.job_added) self.monitor.connect ('job-event', self.job_event) self.monitor.connect ('job-removed', self.job_removed) self.monitor.connect ('state-reason-added', self.state_reason_added) self.monitor.connect ('state-reason-removed', self.state_reason_removed) self.monitor.connect ('still-connecting', self.still_connecting) self.monitor.connect ('now-connected', self.now_connected) self.monitor.connect ('printer-added', self.printer_added) self.monitor.connect ('printer-event', self.printer_event) self.monitor.connect ('printer-removed', self.printer_removed) self.monitor.refresh () self.my_monitor = None if not my_jobs: self.my_monitor = monitor.Monitor(bus=bus, my_jobs=True, host=self.host, port=self.port, encryption=self.encryption) self.my_monitor.connect ('job-added', self.job_added) self.my_monitor.connect ('job-event', self.job_event) self.my_monitor.refresh () if not self.applet: self.JobsWindow.show () self.JobsAttributesWindow = Gtk.Window() self.JobsAttributesWindow.set_title (_("Job attributes")) self.JobsAttributesWindow.set_position(Gtk.WindowPosition.MOUSE) self.JobsAttributesWindow.set_default_size(600, 600) self.JobsAttributesWindow.set_transient_for (self.JobsWindow) self.JobsAttributesWindow.connect("delete_event", self.job_attributes_on_delete_event) self.JobsAttributesWindow.add_accel_group (self.job_ui_manager.get_accel_group ()) attrs_action_group = Gtk.ActionGroup (name="AttrsActionGroup") attrs_action_group.add_actions ([ ("close", Gtk.STOCK_CLOSE, None, "<ctrl>w", _("Close this window"), self.job_attributes_on_delete_event) ]) self.attrs_ui_manager = Gtk.UIManager () self.attrs_ui_manager.insert_action_group (attrs_action_group, -1) self.attrs_ui_manager.add_ui_from_string ( """ <ui> <accelerator action="close"/> </ui> """ ) self.attrs_ui_manager.ensure_update () self.JobsAttributesWindow.add_accel_group (self.attrs_ui_manager.get_accel_group ()) vbox = Gtk.VBox () self.JobsAttributesWindow.add (vbox) toolbar = Gtk.Toolbar () action = self.attrs_ui_manager.get_action ("/close") item = action.create_tool_item () item.set_is_important (True) toolbar.insert (item, 0) vbox.pack_start (toolbar, False, False, 0) self.notebook = Gtk.Notebook() vbox.pack_start (self.notebook, True, True, 0) def cleanup (self): self.monitor.cleanup () if self.my_monitor: self.my_monitor.cleanup () self.JobsWindow.hide () # Close any open notifications. for l in [self.new_printer_notifications.values (), self.state_reason_notifications.values ()]: for notification in l: if getattr (notification, 'closed', None) != True: try: notification.close () except GLib.GError: # Can fail if the notification wasn't even shown # yet (as in bug #571603). pass notification.closed = True if self.job_creation_times_timer is not None: GLib.source_remove (self.job_creation_times_timer) self.job_creation_times_timer = None for op in self.ops: op.destroy () if self.applet and not self.notify_has_persistence: self.statusicon.set_visible (False) self.emit ('finished') def set_process_pending (self, whether): self.process_pending_events = whether def on_delete_event(self, *args): if self.applet or not self.loop: self.JobsWindow.hide () self.JobsWindow.visible = False if not self.applet: # Being run from main app, not applet self.cleanup () else: self.loop.quit () return True def job_attributes_on_delete_event(self, widget, event=None): for page in range(self.notebook.get_n_pages()): self.notebook.remove_page(-1) self.jobs_attrs = {} self.JobsAttributesWindow.hide() return True 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): 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 () 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.visible = not visible def on_show_completed_jobs_clicked(self, toggletoolbutton): if toggletoolbutton.get_active(): which_jobs = "all" else: which_jobs = "not-completed" self.monitor.refresh(which_jobs=which_jobs, refresh_all=False) if self.my_monitor: self.my_monitor.refresh(which_jobs=which_jobs, refresh_all=False) def update_job_creation_times(self): now = time.time () need_update = False for job, data in self.jobs.items(): t = _("Unknown") if 'time-at-creation' in data: created = data['time-at-creation'] ago = now - created need_update = True if ago < 2 * 60: t = _("a minute ago") elif ago < 60 * 60: mins = int (ago / 60) t = _("%d minutes ago") % mins elif ago < 24 * 60 * 60: hours = int (ago / (60 * 60)) if hours == 1: t = _("an hour ago") else: t = _("%d hours ago") % hours elif ago < 7 * 24 * 60 * 60: days = int (ago / (24 * 60 * 60)) if days == 1: t = _("yesterday") else: t = _("%d days ago") % days elif ago < 6 * 7 * 24 * 60 * 60: weeks = int (ago / (7 * 24 * 60 * 60)) if weeks == 1: t = _("last week") else: t = _("%d weeks ago") % weeks else: need_update = False t = time.strftime ("%B %Y", time.localtime (created)) if job in self.jobiters: iter = self.jobiters[job] self.store.set_value (iter, 1, t) if need_update and not self.job_creation_times_timer: def update_times_with_locking (): Gdk.threads_enter () ret = self.update_job_creation_times () Gdk.threads_leave () return ret t = GLib.timeout_add_seconds (60, update_times_with_locking) self.job_creation_times_timer = t if not need_update: if self.job_creation_times_timer: GLib.source_remove (self.job_creation_times_timer) self.job_creation_times_timer = None # Return code controls whether the timeout will recur. return need_update def print_error_dialog_response(self, dialog, response, jobid): dialog.hide () dialog.destroy () self.stopped_job_prompts.remove (jobid) if response == Gtk.ResponseType.NO: # Diagnose if 'troubleshooter' not in self.__dict__: import troubleshoot troubleshooter = troubleshoot.run (self.on_troubleshoot_quit) self.troubleshooter = troubleshooter def on_troubleshoot_quit(self, troubleshooter): del self.troubleshooter def add_job (self, job, data, connection=None): self.update_job (job, data, connection=connection) # There may have been an error fetching additional attributes, # in which case we need to give up. if job not in self.jobs: return store = self.store iter = self.store.append (None) store.set_value (iter, 0, job) debugprint ("Job %d added" % job) self.jobiters[job] = iter range = self.treeview.get_visible_range () if range is not None: (start, end) = range if (self.store.get_sort_column_id () == (0, Gtk.SortType.DESCENDING) and start == Gtk.TreePath(1)): # This job was added job above the visible range, and # we are sorting by descending job ID. Scroll to it. self.treeview.scroll_to_cell (Gtk.TreePath(), None, False, 0.0, 0.0) if not self.job_creation_times_timer: def start_updating_job_creation_times(): Gdk.threads_enter () self.update_job_creation_times () Gdk.threads_leave () return False GLib.timeout_add (500, start_updating_job_creation_times) def update_monitor (self): self.monitor.update () if self.my_monitor: self.my_monitor.update () def update_job (self, job, data, connection=None): # Fetch required attributes for this job if they are missing. r = self.required_job_attributes - set (data.keys ()) # If we are showing attributes of this job at this moment, update them. if job in self.jobs_attrs: self.update_job_attributes_viewer(job) if r: attrs = None try: if connection is None: connection = cups.Connection (host=self.host, port=self.port, encryption=self.encryption) debugprint ("requesting %s" % r) r = list (r) attrs = connection.getJobAttributes (job, requested_attributes=r) except RuntimeError: pass except AttributeError: pass except cups.IPPError: # someone else may have purged the job return if attrs: data.update (attrs) self.jobs[job] = data job_requires_auth = False try: jstate = data.get ('job-state', cups.IPP_JOB_PROCESSING) s = int (jstate) if s in [cups.IPP_JOB_HELD, cups.IPP_JOB_STOPPED]: jattrs = ['job-state', 'job-hold-until', 'job-printer-uri'] pattrs = ['auth-info-required', 'device-uri'] # The current job-printer-uri may differ from the one that # is returned when we request it over the connection. # So while we use it to query the printer attributes we # Update it afterwards to make sure that we really # have the one cups uses in the job attributes. uri = data.get ('job-printer-uri') c = authconn.Connection (self.JobsWindow, host=self.host, port=self.port, encryption=self.encryption) attrs = c.getPrinterAttributes (uri = uri, requested_attributes=pattrs) try: auth_info_required = attrs['auth-info-required'] except KeyError: debugprint ("No auth-info-required attribute; " "guessing instead") auth_info_required = ['username', 'password'] if not isinstance (auth_info_required, list): auth_info_required = [auth_info_required] attrs['auth-info-required'] = auth_info_required data.update (attrs) attrs = c.getJobAttributes (job, requested_attributes=jattrs) data.update (attrs) jstate = data.get ('job-state', cups.IPP_JOB_PROCESSING) s = int (jstate) except ValueError: pass except RuntimeError: pass except cups.IPPError: pass # Invalidate the cached status description and redraw the treeview. try: del data['_status_text'] except KeyError: pass self.treeview.queue_draw () # Check whether authentication is required. job_requires_auth = (s == cups.IPP_JOB_HELD and data.get ('job-hold-until', 'none') == 'auth-info-required') if job_requires_auth: # Try to get the authentication information. If we are not # running as an applet just try to get the information silently # and not prompt the user. self.get_authentication (job, data.get ('device-uri'), data.get ('job-printer-uri'), data.get ('auth-info-required', []), self.applet) self.submenu_set = False self.update_sensitivity () def get_authentication (self, job, device_uri, printer_uri, auth_info_required, show_dialog): # Check if we have requested authentication for this job already if job not in self.auth_info_dialogs: try: cups.require ("1.9.37") except: debugprint ("Authentication required but " "authenticateJob() not available") return # Find out which auth-info is required. try_secret = USE_SECRET informational_attrs = dict() auth_info = None if try_secret and 'password' in auth_info_required: (scheme, rest) = urllib.parse.splittype (device_uri) if scheme == 'smb': uri = smburi.SMBURI (uri=device_uri) (group, server, share, user, password) = uri.separate () informational_attrs["domain"] = str (group) else: (serverport, rest) = urllib.parse.splithost (rest) if serverport is None: server = None else: (server, port) = urllib.parse.splitnport (serverport) if scheme is None or server is None: try_secret = False else: informational_attrs.update ({ "server": str (server.lower ()), "protocol": str (scheme)}) if job in self.authenticated_jobs: # We've already tried to authenticate this job before. try_secret = False # To increase compatibility and resolve problems with # multiple printers on one host we use the printers URI # as the identifying attribute. Versions <= 1.4.4 used # a combination of domain / server / protocol instead. # The old attributes are still used as a fallback for identifying # the secret but are otherwise only informational. identifying_attrs = { "uri": str (printer_uri) } if try_secret and 'password' in auth_info_required: for keyring_attrs in [identifying_attrs, informational_attrs]: attrs = dict() for key, val in keyring_attrs.items (): key_val_dict = {key : val} attrs.update (key_val_dict) service_obj = ServiceGet() service = service_obj.get_service() search_obj = ItemSearch(service, attrs) items = search_obj.get_items() if items: auth_info = ['' for x in auth_info_required] ind = auth_info_required.index ('username') auth_info[ind] = items[0].get_attributes().get("user") ind = auth_info_required.index ('password') auth_info[ind] = items[0].get_secret().get().decode() break else: debugprint ("Failed to find secret in keyring.") if try_secret: try: c = authconn.Connection (self.JobsWindow, host=self.host, port=self.port, encryption=self.encryption) except RuntimeError: try_secret = False if try_secret and auth_info is not None: try: c._begin_operation (_("authenticating job")) c.authenticateJob (job, auth_info) c._end_operation () self.update_monitor () debugprint ("Automatically authenticated job %d" % job) self.authenticated_jobs.add (job) return except cups.IPPError: c._end_operation () nonfatalException () return except: c._end_operation () nonfatalException () if auth_info_required and show_dialog: username = pwd.getpwuid (os.getuid ())[0] keyring_attrs = informational_attrs.copy() keyring_attrs.update(identifying_attrs) keyring_attrs["user"] = str (username) self.display_auth_info_dialog (job, keyring_attrs) def display_auth_info_dialog (self, job, keyring_attrs=None): data = self.jobs[job] try: auth_info_required = data['auth-info-required'] except KeyError: debugprint ("No auth-info-required attribute; " "guessing instead") auth_info_required = ['username', 'password'] dialog = authconn.AuthDialog (auth_info_required=auth_info_required, allow_remember=USE_SECRET) dialog.keyring_attrs = keyring_attrs dialog.auth_info_required = auth_info_required dialog.set_position (Gtk.WindowPosition.CENTER) # Pre-fill 'username' field. auth_info = ['' for x in auth_info_required] username = pwd.getpwuid (os.getuid ())[0] if 'username' in auth_info_required: try: ind = auth_info_required.index ('username') auth_info[ind] = username dialog.set_auth_info (auth_info) except: nonfatalException () # Focus on the first empty field. index = 0 for field in auth_info_required: if auth_info[index] == '': dialog.field_grab_focus (field) break index += 1 dialog.set_prompt (_("Authentication required for " "printing document `%s' (job %d)") % (data.get('job-name', _("Unknown")), job)) self.auth_info_dialogs[job] = dialog dialog.connect ('response', self.auth_info_dialog_response) dialog.connect ('delete-event', self.auth_info_dialog_delete) dialog.job_id = job dialog.show_all () dialog.set_keep_above (True) dialog.show_now () def auth_info_dialog_delete (self, dialog, event): self.auth_info_dialog_response (dialog, Gtk.ResponseType.CANCEL) def auth_info_dialog_response (self, dialog, response): jobid = dialog.job_id del self.auth_info_dialogs[jobid] if response != Gtk.ResponseType.OK: dialog.destroy () return auth_info = dialog.get_auth_info () try: c = authconn.Connection (self.JobsWindow, host=self.host, port=self.port, encryption=self.encryption) except RuntimeError: debugprint ("Error connecting to CUPS for authentication") return remember = False c._begin_operation (_("authenticating job")) try: c.authenticateJob (jobid, auth_info) remember = dialog.get_remember_password () self.authenticated_jobs.add (jobid) self.update_monitor () except cups.IPPError as e: (e, m) = e.args self.show_IPP_Error (e, m) c._end_operation () if remember: try: keyring_attrs = getattr (dialog, "keyring_attrs", None) auth_info_required = getattr (dialog, "auth_info_required", None) if keyring_attrs is not None and auth_info_required is not None: try: ind = auth_info_required.index ('username') keyring_attrs['user'] = auth_info[ind] except IndexError: pass name = "%s@%s (%s)" % (keyring_attrs.get ("user"), keyring_attrs.get ("server"), keyring_attrs.get ("protocol")) ind = auth_info_required.index ('password') secret = auth_info[ind] attrs = dict() for key, val in keyring_attrs.items (): key_val_dict = {key : val} attrs.update (key_val_dict) password_obj = PasswordStore(attrs, name, secret) debugprint ("keyring: created id %d for %s" % (id, name)) except: nonfatalException () dialog.destroy () def set_statusicon_visibility (self): if not self.applet: return if self.suppress_icon_hide: # Avoid hiding the icon if we've been woken up to notify # about a new printer. self.suppress_icon_hide = False return open_notifications = len (self.new_printer_notifications.keys ()) open_notifications += len (self.completed_job_notifications.keys ()) for reason, notification in self.state_reason_notifications.items(): if getattr (notification, 'closed', None) != True: open_notifications += 1 num_jobs = len (self.active_jobs) debugprint ("open notifications: %d" % open_notifications) 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) self.statusicon.set_visible (open_notifications > 0 or num_jobs > self.num_jobs_when_hidden) # Let the icon show/hide itself before continuing. while self.process_pending_events and Gtk.events_pending (): Gtk.main_iteration () def on_treeview_popup_menu (self, treeview): event = Gdk.Event (Gdk.EventType.NOTHING) self.show_treeview_popup_menu (treeview, event, 0) def on_treeview_button_release_event(self, treeview, event): if event.button == 3: self.show_treeview_popup_menu (treeview, event, event.button) def update_sensitivity (self, selection = None): if (selection is None): selection = self.treeview.get_selection () (model, pathlist) = selection.get_selected_rows() cancel = self.job_ui_manager.get_action ("/cancel-job") delete = self.job_ui_manager.get_action ("/delete-job") hold = self.job_ui_manager.get_action ("/hold-job") release = self.job_ui_manager.get_action ("/release-job") reprint = self.job_ui_manager.get_action ("/reprint-job") retrieve = self.job_ui_manager.get_action ("/retrieve-job") authenticate = self.job_ui_manager.get_action ("/authenticate-job") attributes = self.job_ui_manager.get_action ("/job-attributes") move = self.job_ui_manager.get_action ("/move-job") if len (pathlist) == 0: for widget in [cancel, delete, hold, release, reprint, retrieve, move, authenticate, attributes]: widget.set_sensitive (False) return cancel_sensitive = True hold_sensitive = True release_sensitive = True reprint_sensitive = True authenticate_sensitive = True move_sensitive = False other_printers = self.printer_uri_index.all_printer_names () job_printers = dict() self.jobids = [] for path in pathlist: iter = self.store.get_iter (path) jobid = self.store.get_value (iter, 0) self.jobids.append(jobid) job = self.jobs[jobid] if 'job-state' in job: s = job['job-state'] if s >= cups.IPP_JOB_CANCELED: cancel_sensitive = False if s != cups.IPP_JOB_PENDING: hold_sensitive = False if s != cups.IPP_JOB_HELD: release_sensitive = False if (not job.get('job-preserved', False)): reprint_sensitive = False if (job.get ('job-state', cups.IPP_JOB_CANCELED) != cups.IPP_JOB_HELD or job.get ('job-hold-until', 'none') != 'auth-info-required'): authenticate_sensitive = False uri = job.get ('job-printer-uri', None) if uri: try: printer = self.printer_uri_index.lookup (uri) except KeyError: printer = uri job_printers[printer] = uri if len (job_printers.keys ()) == 1: try: other_printers.remove (list(job_printers.keys ())[0]) except KeyError: pass if len (other_printers) > 0: printers_menu = Gtk.Menu () other_printers = list (other_printers) other_printers.sort () for printer in other_printers: try: uri = self.printer_uri_index.lookup_cached_by_name (printer) except KeyError: uri = None menuitem = Gtk.MenuItem (label=printer) menuitem.set_sensitive (uri is not None) menuitem.show () self._submenu_connect_hack (menuitem, self.on_job_move_activate, uri) printers_menu.append (menuitem) self.move_job_menuitem.set_submenu (printers_menu) move_sensitive = True cancel.set_sensitive(cancel_sensitive) delete.set_sensitive(not cancel_sensitive) hold.set_sensitive(hold_sensitive) release.set_sensitive(release_sensitive) reprint.set_sensitive(reprint_sensitive) retrieve.set_sensitive(reprint_sensitive) move.set_sensitive (move_sensitive) authenticate.set_sensitive(authenticate_sensitive) attributes.set_sensitive(True) def on_selection_changed (self, selection): self.update_sensitivity (selection) def show_treeview_popup_menu (self, treeview, event, event_button): # Right-clicked. 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 () def on_icon_configure_printers_activate(self, menuitem): env = {} for name, value in os.environ.items (): if name == "SYSTEM_CONFIG_PRINTER_UI": continue env[name] = value p = subprocess.Popen ([ "system-config-printer" ], close_fds=True, env=env) GLib.timeout_add_seconds (10, self.poll_subprocess, p) def poll_subprocess(self, process): returncode = process.poll () return returncode is None def on_icon_quit_activate (self, menuitem): self.cleanup () if self.loop: self.loop.quit () def on_job_cancel_activate(self, menuitem): self.on_job_cancel_activate2(False) def on_job_delete_activate(self, menuitem): self.on_job_cancel_activate2(True) def on_job_cancel_activate2(self, purge_job): if self.jobids: op = CancelJobsOperation (self.JobsWindow, self.host, self.port, self.encryption, self.jobids, purge_job) self.ops.append (op) op.connect ('finished', self.on_canceljobs_finished) op.connect ('ipp-error', self.on_canceljobs_error) def on_canceljobs_finished (self, canceljobsoperation): canceljobsoperation.destroy () i = self.ops.index (canceljobsoperation) del self.ops[i] self.update_monitor () def on_canceljobs_error (self, canceljobsoperation, jobid, exc): self.update_monitor () if type (exc) == cups.IPPError: (e, m) = exc.args if (e != cups.IPP_NOT_POSSIBLE and e != cups.IPP_NOT_FOUND): self.show_IPP_Error (e, m) return raise exc def on_job_hold_activate(self, menuitem): try: c = authconn.Connection (self.JobsWindow, host=self.host, port=self.port, encryption=self.encryption) except RuntimeError: return for jobid in self.jobids: c._begin_operation (_("holding job")) try: c.setJobHoldUntil (jobid, "indefinite") except cups.IPPError as e: (e, m) = e.args if (e != cups.IPP_NOT_POSSIBLE and e != cups.IPP_NOT_FOUND): self.show_IPP_Error (e, m) self.update_monitor () c._end_operation () return c._end_operation () del c self.update_monitor () def on_job_release_activate(self, menuitem): try: c = authconn.Connection (self.JobsWindow, host=self.host, port=self.port, encryption=self.encryption) except RuntimeError: return for jobid in self.jobids: c._begin_operation (_("releasing job")) try: c.setJobHoldUntil (jobid, "no-hold") except cups.IPPError as e: (e, m) = e.args if (e != cups.IPP_NOT_POSSIBLE and e != cups.IPP_NOT_FOUND): self.show_IPP_Error (e, m) self.update_monitor () c._end_operation () return c._end_operation () del c self.update_monitor () def on_job_reprint_activate(self, menuitem): try: c = authconn.Connection (self.JobsWindow, host=self.host, port=self.port, encryption=self.encryption) for jobid in self.jobids: c.restartJob (jobid) del c except cups.IPPError as e: (e, m) = e.args self.show_IPP_Error (e, m) self.update_monitor () return except RuntimeError: return self.update_monitor () def on_job_retrieve_activate(self, menuitem): try: c = authconn.Connection (self.JobsWindow, host=self.host, port=self.port, encryption=self.encryption) except RuntimeError: return for jobid in self.jobids: try: attrs=c.getJobAttributes(jobid) printer_uri=attrs['job-printer-uri'] try: document_count = attrs['number-of-documents'] except KeyError: document_count = attrs.get ('document-count', 0) for document_number in range(1, document_count+1): document=c.getDocument(printer_uri, jobid, document_number) tempfile = document.get('file') name = document.get('document-name') format = document.get('document-format', '') # if there's no document-name retrieved if name is None: # give the default filename some meaningful name name = _("retrieved")+str(document_number) # add extension according to format if format == 'application/postscript': name = name + ".ps" elif format.find('application/vnd.') != -1: name = name + format.replace('application/vnd', '') elif format.find('application/') != -1: name = name + format.replace('application/', '.') if tempfile is not None: dialog = Gtk.FileChooserDialog (title=_("Save File"), transient_for=self.JobsWindow, action=Gtk.FileChooserAction.SAVE) dialog.add_buttons ( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK) dialog.set_current_name(name) dialog.set_do_overwrite_confirmation(True) response = dialog.run() if response == Gtk.ResponseType.OK: file_to_save = dialog.get_filename() try: shutil.copyfile(tempfile, file_to_save) except (IOError, shutil.Error): debugprint("Unable to save file "+file_to_save) elif response == Gtk.ResponseType.CANCEL: pass dialog.destroy() os.unlink(tempfile) else: debugprint("Unable to retrieve file from job file") return except cups.IPPError as e: (e, m) = e.args self.show_IPP_Error (e, m) self.update_monitor () return del c self.update_monitor () def _submenu_connect_hack (self, item, callback, *args): # See https://bugzilla.gnome.org/show_bug.cgi?id=695488 only_once = threading.Semaphore (1) def handle_event (item, event=None): if only_once.acquire (False): GObject.idle_add (callback, item, *args) return (item.connect ('button-press-event', handle_event), item.connect ('activate', handle_event)) def on_job_move_activate(self, menuitem, job_printer_uri): try: c = authconn.Connection (self.JobsWindow, host=self.host, port=self.port, encryption=self.encryption) for jobid in self.jobids: c.moveJob (job_id=jobid, job_printer_uri=job_printer_uri) del c except cups.IPPError as e: (e, m) = e.args self.show_IPP_Error (e, m) self.update_monitor () return except RuntimeError: return self.update_monitor () def on_job_authenticate_activate(self, menuitem): try: c = cups.Connection (host=self.host, port=self.port, encryption=self.encryption) except RuntimeError: return False jattrs_req = ['job-printer-uri'] pattrs_req = ['auth-info-required', 'device-uri'] for jobid in self.jobids: # Get the required attributes for this job jattrs = c.getJobAttributes (jobid, requested_attributes=jattrs_req) uri = jattrs.get ('job-printer-uri') pattrs = c.getPrinterAttributes (uri = uri, requested_attributes=pattrs_req) try: auth_info_required = pattrs['auth-info-required'] except KeyError: debugprint ("No auth-info-required attribute; " "guessing instead") auth_info_required = ['username', 'password'] self.get_authentication (jobid, pattrs.get ('device-uri'), uri, auth_info_required, True) def on_refresh_clicked(self, toolbutton): self.monitor.refresh () if self.my_monitor: self.my_monitor.refresh () self.update_job_creation_times () def on_job_attributes_activate(self, menuitem): """ For every selected job create notebook page with attributes. """ try: c = cups.Connection (host=self.host, port=self.port, encryption=self.encryption) except RuntimeError: return False for jobid in self.jobids: if jobid not in self.jobs_attrs: # add new notebook page with scrollable treeview scrolledwindow = Gtk.ScrolledWindow() label = Gtk.Label(label=str(jobid)) # notebook page has label with jobid page_index = self.notebook.append_page(scrolledwindow, label) attr_treeview = Gtk.TreeView() scrolledwindow.add(attr_treeview) cell = Gtk.CellRendererText () attr_treeview.insert_column_with_attributes(0, _("Name"), cell, text=0) cell = Gtk.CellRendererText () attr_treeview.insert_column_with_attributes(1, _("Value"), cell, text=1) attr_store = Gtk.ListStore(str, str) attr_treeview.set_model(attr_store) attr_treeview.get_selection().set_mode(Gtk.SelectionMode.NONE) attr_store.set_sort_column_id (0, Gtk.SortType.ASCENDING) self.jobs_attrs[jobid] = (attr_store, page_index) self.update_job_attributes_viewer (jobid, conn=c) self.JobsAttributesWindow.show_all () def update_job_attributes_viewer(self, jobid, conn=None): """ Update attributes store with new values. """ if conn is not None: c = conn else: try: c = cups.Connection (host=self.host, port=self.port, encryption=self.encryption) except RuntimeError: return False if jobid in self.jobs_attrs: (attr_store, page) = self.jobs_attrs[jobid] try: attrs = c.getJobAttributes(jobid) # new attributes except AttributeError: return except cups.IPPError: # someone else may have purged the job, # remove jobs notebook page self.notebook.remove_page(page) del self.jobs_attrs[jobid] return attr_store.clear() # remove old attributes for name, value in attrs.items(): if name in ['job-id', 'job-printer-up-time']: continue attr_store.append([name, str(value)]) def job_is_active (self, jobdata): state = jobdata.get ('job-state', cups.IPP_JOB_CANCELED) if state >= cups.IPP_JOB_CANCELED: return False return True ## Icon manipulation def add_state_reason_emblem (self, pixbuf, printer=None): worst_reason = None if printer is None and self.worst_reason is not None: # Check that it's valid. printer = self.worst_reason.get_printer () found = False for reason in self.printer_state_reasons.get (printer, []): if reason == self.worst_reason: worst_reason = self.worst_reason break if worst_reason is None: self.worst_reason = None if printer is not None: for reason in self.printer_state_reasons.get (printer, []): if worst_reason is None: worst_reason = reason elif reason > worst_reason: worst_reason = reason if worst_reason is not None: level = worst_reason.get_level () if level > StateReason.REPORT: # Add an emblem to the icon. icon = StateReason.LEVEL_ICON[level] pixbuf = pixbuf.copy () try: theme = Gtk.IconTheme.get_default () emblem = theme.load_icon (icon, 22, 0) emblem.composite (pixbuf, pixbuf.get_width () / 2, pixbuf.get_height () / 2, emblem.get_width () / 2, emblem.get_height () / 2, pixbuf.get_width () / 2, pixbuf.get_height () / 2, 0.5, 0.5, GdkPixbuf.InterpType.BILINEAR, 255) except GObject.GError: debugprint ("No %s icon available" % icon) 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 if tooltip is None: num_jobs = len (self.jobs) if num_jobs == 0: tooltip = _("No documents queued") elif num_jobs == 1: tooltip = _("1 document queued") else: tooltip = _("%d documents queued") % num_jobs self.statusicon.set_tooltip_markup (tooltip) def update_status (self, have_jobs=None): # Found out which printer state reasons apply to our active jobs. upset_printers = set() for printer, reasons in self.printer_state_reasons.items (): if len (reasons) > 0: upset_printers.add (printer) debugprint ("Upset printers: %s" % upset_printers) my_upset_printers = set() if len (upset_printers): my_upset_printers = set() for jobid in self.active_jobs: # 'job-printer-name' is set by job_added/job_event printer = self.jobs[jobid]['job-printer-name'] if printer in upset_printers: my_upset_printers.add (printer) debugprint ("My upset printers: %s" % my_upset_printers) my_reasons = [] for printer in my_upset_printers: my_reasons.extend (self.printer_state_reasons[printer]) # Find out which is the most problematic. self.worst_reason = None if len (my_reasons) > 0: worst_reason = my_reasons[0] for reason in my_reasons: if reason > worst_reason: worst_reason = reason self.worst_reason = worst_reason debugprint ("Worst reason: %s" % worst_reason) Gdk.threads_enter () self.statusbar.pop (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: job_state = self.jobs[jobid]['job-state'] except KeyError: continue if job_state == cups.IPP_JOB_PROCESSING: processing = processing + 1 elif job_state == cups.IPP_JOB_PENDING: pending = pending + 1 if ((processing > 0) or (pending > 0)): 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) self.set_statusicon_visibility () self.set_statusicon_tooltip (tooltip=tooltip) Gdk.threads_leave () ## Notifications def notify_printer_state_reason_if_important (self, reason): level = reason.get_level () if level < StateReason.WARNING: # Not important enough to justify a notification. return blacklist = [ # Some printers report 'other-warning' for no apparent # reason, e.g. Canon iR 3170C, Epson AL-CX11NF. # See bug #520815. "other", # This seems to be some sort of 'magic' state reason that # is for internal use only. "com.apple.print.recoverable", # Human-readable text for this reason has misleading wording, # suppress it. "connecting-to-device", # "cups-remote-..." reasons have no human-readable text yet and # so get considered as errors, suppress them, too. "cups-remote-pending", "cups-remote-pending-held", "cups-remote-processing", "cups-remote-stopped", "cups-remote-canceled", "cups-remote-aborted", "cups-remote-completed", # The cups-waiting-for-job-completed job state reason is normal and should not cause a desktop notification "cups-waiting-for-job-completed" ] if reason.get_reason () in blacklist: return self.notify_printer_state_reason (reason) def notify_printer_state_reason (self, reason): tuple = reason.get_tuple () if tuple in self.state_reason_notifications: debugprint ("Already sent notification for %s" % repr (reason)) return level = reason.get_level () if (level == StateReason.ERROR or reason.get_reason () == "connecting-to-device"): urgency = Notify.Urgency.NORMAL else: urgency = Notify.Urgency.LOW (title, text) = reason.get_description () notification = Notify.Notification.new (title, text, 'printer') reason.user_notified = True notification.set_urgency (urgency) if self.notify_has_actions: notification.set_timeout (Notify.EXPIRES_NEVER) notification.connect ('closed', self.on_state_reason_notification_closed) self.state_reason_notifications[reason.get_tuple ()] = notification self.set_statusicon_visibility () try: notification.show () except GObject.GError: nonfatalException () def on_state_reason_notification_closed (self, notification, reason=None): debugprint ("Notification %s closed" % repr (notification)) notification.closed = True self.set_statusicon_visibility () return def notify_completed_job (self, jobid): job = self.jobs.get (jobid, {}) document = job.get ('job-name', _("Unknown")) printer_uri = job.get ('job-printer-uri') if printer_uri is not None: # Determine if this printer is remote. There's no need to # show a notification if the printer is connected to this # machine. # Find out the device URI. We might already have # determined this if authentication was required. device_uri = job.get ('device-uri') if device_uri is None: pattrs = ['device-uri'] c = authconn.Connection (self.JobsWindow, host=self.host, port=self.port, encryption=self.encryption) try: attrs = c.getPrinterAttributes (uri=printer_uri, requested_attributes=pattrs) except cups.IPPError: return device_uri = attrs.get ('device-uri') if device_uri is not None: (scheme, rest) = urllib.parse.splittype (device_uri) if scheme not in ['socket', 'ipp', 'http', 'smb']: return printer = job.get ('job-printer-name', _("Unknown")) notification = Notify.Notification.new (_("Document printed"), _("Document `%s' has been sent " "to `%s' for printing.") % (document, printer), 'printer') notification.set_urgency (Notify.Urgency.LOW) notification.connect ('closed', self.on_completed_job_notification_closed) notification.jobid = jobid self.completed_job_notifications[jobid] = notification self.set_statusicon_visibility () try: notification.show () except GObject.GError: nonfatalException () def on_completed_job_notification_closed (self, notification, reason=None): jobid = notification.jobid del self.completed_job_notifications[jobid] self.set_statusicon_visibility () ## Monitor signal handlers def on_refresh (self, mon): self.store.clear () self.jobs = {} self.active_jobs = set() self.jobiters = {} self.printer_uri_index = PrinterURIIndex () def job_added (self, mon, jobid, eventname, event, jobdata): uri = jobdata.get ('job-printer-uri', '') try: printer = self.printer_uri_index.lookup (uri) except KeyError: printer = uri if self.specific_dests and printer not in self.specific_dests: return jobdata['job-printer-name'] = printer # We may be showing this job already, perhaps because we are showing # completed jobs and one was reprinted. if jobid not in self.jobiters: self.add_job (jobid, jobdata) elif mon == self.my_monitor: # Copy over any missing attributes such as user and title. for attr, value in jobdata.items (): if attr not in self.jobs[jobid]: self.jobs[jobid][attr] = value debugprint ("Add %s=%s (my job)" % (attr, value)) # If we failed to get required attributes for the job, bail. if jobid not in self.jobiters: return if self.job_is_active (jobdata): self.active_jobs.add (jobid) elif jobid in self.active_jobs: self.active_jobs.remove (jobid) self.update_status (have_jobs=True) if self.applet: if not self.job_is_active (jobdata): return for reason in self.printer_state_reasons.get (printer, []): if not reason.user_notified: self.notify_printer_state_reason_if_important (reason) def job_event (self, mon, jobid, eventname, event, jobdata): uri = jobdata.get ('job-printer-uri', '') try: printer = self.printer_uri_index.lookup (uri) except KeyError: printer = uri if self.specific_dests and printer not in self.specific_dests: return jobdata['job-printer-name'] = printer if self.job_is_active (jobdata): self.active_jobs.add (jobid) elif jobid in self.active_jobs: self.active_jobs.remove (jobid) self.update_job (jobid, jobdata) self.update_status () # Check that the job still exists, as update_status re-enters # the main loop in order to paint/hide the tray icon. Really # that should probably be deferred to the idle handler, but # for the moment just deal with the fact that the job might # have gone (bug #640904). if jobid not in self.jobs: return jobdata = self.jobs[jobid] # If the job has finished, let the user know. if self.applet and (eventname == 'job-completed' or (eventname == 'job-state-changed' and event['job-state'] == cups.IPP_JOB_COMPLETED)): reasons = event['job-state-reasons'] if type (reasons) != list: reasons = [reasons] canceled = False for reason in reasons: if reason.startswith ("job-canceled"): canceled = True break if not canceled: self.notify_completed_job (jobid) # Look out for stopped jobs. if (self.applet and (eventname == 'job-stopped' or (eventname == 'job-state-changed' and event['job-state'] in [cups.IPP_JOB_STOPPED, cups.IPP_JOB_PENDING])) and not jobid in self.stopped_job_prompts): # Why has the job stopped? It might be due to a job error # of some sort, or it might be that the backend requires # authentication. If the latter, the job will be held not # stopped, and the job-hold-until attribute will be # 'auth-info-required'. This was already checked for in # update_job. may_be_problem = True jstate = jobdata['job-state'] if (jstate == cups.IPP_JOB_PROCESSING or (jstate == cups.IPP_JOB_HELD and jobdata['job-hold-until'] == 'auth-info-required')): # update_job already dealt with this. may_be_problem = False else: # Other than that, unfortunately the only # clue we get is the notify-text, which is not # translated into our native language. We'd better # try parsing it. In CUPS-1.3.6 the possible strings # are: # # "Job stopped due to filter errors; please consult # the error_log file for details." # # "Job stopped due to backend errors; please consult # the error_log file for details." # # "Job held due to backend errors; please consult the # error_log file for details." # # "Authentication is required for job %d." # [This case is handled in the update_job method.] # # "Job stopped due to printer being paused" # [This should be ignored, as the job was doing just # fine until the printer was stopped for other reasons.] notify_text = event['notify-text'] document = jobdata['job-name'] if notify_text.find ("backend errors") != -1: message = (_("There was a problem sending document `%s' " "(job %d) to the printer.") % (document, jobid)) elif notify_text.find ("filter errors") != -1: message = _("There was a problem processing document `%s' " "(job %d).") % (document, jobid) elif (notify_text.find ("being paused") != -1 or jstate != cups.IPP_JOB_STOPPED): may_be_problem = False else: # Give up and use the provided message untranslated. message = (_("There was a problem printing document `%s' " "(job %d): `%s'.") % (document, jobid, notify_text)) if may_be_problem: debugprint ("Problem detected") self.toggle_window_display (None, force_show=True) dialog = Gtk.Dialog (title=_("Print Error"), transient_for=self.JobsWindow) dialog.add_buttons (_("_Diagnose"), Gtk.ResponseType.NO, Gtk.STOCK_OK, Gtk.ResponseType.OK) dialog.set_default_response (Gtk.ResponseType.OK) dialog.set_border_width (6) dialog.set_resizable (False) dialog.set_icon_name (ICON) hbox = Gtk.HBox.new (False, 12) hbox.set_border_width (6) image = Gtk.Image () image.set_from_stock (Gtk.STOCK_DIALOG_ERROR, Gtk.IconSize.DIALOG) hbox.pack_start (image, False, False, 0) vbox = Gtk.VBox.new (False, 12) markup = ('<span weight="bold" size="larger">' + _("Print Error") + '</span>\n\n' + saxutils.escape (message)) try: if event['printer-state'] == cups.IPP_PRINTER_STOPPED: name = event['printer-name'] markup += ' ' markup += (_("The printer called `%s' has " "been disabled.") % name) except KeyError: pass label = Gtk.Label(label=markup) label.set_use_markup (True) label.set_line_wrap (True) label.set_alignment (0, 0) vbox.pack_start (label, False, False, 0) hbox.pack_start (vbox, False, False, 0) dialog.vbox.pack_start (hbox, False, False, 0) dialog.connect ('response', self.print_error_dialog_response, jobid) self.stopped_job_prompts.add (jobid) dialog.show_all () def job_removed (self, mon, jobid, eventname, event): # If the job has finished, let the user know. if self.applet and (eventname == 'job-completed' or (eventname == 'job-state-changed' and event['job-state'] == cups.IPP_JOB_COMPLETED)): reasons = event['job-state-reasons'] debugprint (reasons) if type (reasons) != list: reasons = [reasons] canceled = False for reason in reasons: if reason.startswith ("job-canceled"): canceled = True break if not canceled: self.notify_completed_job (jobid) if jobid in self.jobiters: self.store.remove (self.jobiters[jobid]) del self.jobiters[jobid] del self.jobs[jobid] if jobid in self.active_jobs: self.active_jobs.remove (jobid) if jobid in self.jobs_attrs: del self.jobs_attrs[jobid] self.update_status () def state_reason_added (self, mon, reason): (title, text) = reason.get_description () printer = reason.get_printer () try: l = self.printer_state_reasons[printer] except KeyError: l = [] self.printer_state_reasons[printer] = l reason.user_notified = False l.append (reason) self.update_status () self.treeview.queue_draw () if not self.applet: return # Find out if the user has jobs queued for that printer. for job, data in self.jobs.items (): if not self.job_is_active (data): continue if data['job-printer-name'] == printer: # Yes! Notify them of the state reason, if necessary. self.notify_printer_state_reason_if_important (reason) break def state_reason_removed (self, mon, reason): printer = reason.get_printer () try: reasons = self.printer_state_reasons[printer] except KeyError: debugprint ("Printer not found") return try: i = reasons.index (reason) except IndexError: debugprint ("Reason not found") return del reasons[i] self.update_status () self.treeview.queue_draw () if not self.applet: return tuple = reason.get_tuple () try: notification = self.state_reason_notifications[tuple] if getattr (notification, 'closed', None) != True: try: notification.close () except GLib.GError: # Can fail if the notification wasn't even shown # yet (as in bug #545733). pass del self.state_reason_notifications[tuple] self.set_statusicon_visibility () except KeyError: pass def still_connecting (self, mon, reason): if not self.applet: return self.notify_printer_state_reason (reason) def now_connected (self, mon, printer): if not self.applet: return # Find the connecting-to-device state reason. try: reasons = self.printer_state_reasons[printer] reason = None for r in reasons: if r.get_reason () == "connecting-to-device": reason = r break except KeyError: debugprint ("Couldn't find state reason (no reasons)!") if reason is not None: tuple = reason.get_tuple () else: debugprint ("Couldn't find state reason in list!") tuple = None for (level, p, r) in self.state_reason_notifications.keys (): if p == printer and r == "connecting-to-device": debugprint ("Found from notifications list") tuple = (level, p, r) break if tuple is None: debugprint ("Unexpected now_connected signal " "(reason not in notifications list)") return try: notification = self.state_reason_notifications[tuple] except KeyError: debugprint ("Unexpected now_connected signal") return if getattr (notification, 'closed', None) != True: try: notification.close () except GLib.GError: # Can fail if the notification wasn't even shown pass notification.closed = True def printer_added (self, mon, printer): self.printer_uri_index.add_printer (printer) def printer_event (self, mon, printer, eventname, event): self.printer_uri_index.update_from_attrs (printer, event) def printer_removed (self, mon, printer): self.printer_uri_index.remove_printer (printer) ### Cell data functions def _set_job_job_number_text (self, column, cell, model, iter, *data): cell.set_property("text", str (model.get_value (iter, 0))) def _set_job_user_text (self, column, cell, model, iter, *data): jobid = model.get_value (iter, 0) try: job = self.jobs[jobid] except KeyError: return cell.set_property("text", job.get ('job-originating-user-name', _("Unknown"))) def _set_job_document_text (self, column, cell, model, iter, *data): jobid = model.get_value (iter, 0) try: job = self.jobs[jobid] except KeyError: return cell.set_property("text", job.get('job-name', _("Unknown"))) def _set_job_printer_text (self, column, cell, model, iter, *data): jobid = model.get_value (iter, 0) try: reasons = self.jobs[jobid].get('job-state-reasons') except KeyError: return if reasons == 'printer-stopped': reason = ' - ' + _("disabled") else: reason = '' cell.set_property("text", self.jobs[jobid]['job-printer-name']+reason) def _set_job_size_text (self, column, cell, model, iter, *data): jobid = model.get_value (iter, 0) try: job = self.jobs[jobid] except KeyError: return size = _("Unknown") if 'job-k-octets' in job: size = str (job['job-k-octets']) + 'k' cell.set_property("text", size) def _find_job_state_text (self, job): try: data = self.jobs[job] except KeyError: return jstate = data.get ('job-state', cups.IPP_JOB_PROCESSING) s = int (jstate) job_requires_auth = (s == cups.IPP_JOB_HELD and data.get ('job-hold-until', 'none') == 'auth-info-required') state = None if job_requires_auth: state = _("Held for authentication") elif s == cups.IPP_JOB_HELD: state = _("Held") until = data.get ('job-hold-until') if until is not None: try: colon1 = until.find (':') if colon1 != -1: now = time.gmtime () hh = int (until[:colon1]) colon2 = until[colon1 + 1:].find (':') if colon2 != -1: colon2 += colon1 + 1 mm = int (until[colon1 + 1:colon2]) ss = int (until[colon2 + 1:]) else: mm = int (until[colon1 + 1:]) ss = 0 day = now.tm_mday if (hh < now.tm_hour or (hh == now.tm_hour and (mm < now.tm_min or (mm == now.tm_min and ss < now.tm_sec)))): day += 1 hold = (now.tm_year, now.tm_mon, day, hh, mm, ss, 0, 0, -1) old_tz = os.environ.get("TZ") os.environ["TZ"] = "UTC" simpletime = time.mktime (hold) if old_tz is None: del os.environ["TZ"] else: os.environ["TZ"] = old_tz local = time.localtime (simpletime) state = (_("Held until %s") % time.strftime ("%X", local)) except ValueError: pass if until == "day-time": state = _("Held until day-time") elif until == "evening": state = _("Held until evening") elif until == "night": state = _("Held until night-time") elif until == "second-shift": state = _("Held until second shift") elif until == "third-shift": state = _("Held until third shift") elif until == "weekend": state = _("Held until weekend") else: try: state = { cups.IPP_JOB_PENDING: _("Pending"), cups.IPP_JOB_PROCESSING: _("Processing"), cups.IPP_JOB_STOPPED: _("Stopped"), cups.IPP_JOB_CANCELED: _("Canceled"), cups.IPP_JOB_ABORTED: _("Aborted"), cups.IPP_JOB_COMPLETED: _("Completed") }[s] except KeyError: pass if state is None: state = _("Unknown") return state def _set_job_status_icon (self, column, cell, model, iter, *data): jobid = model.get_value (iter, 0) try: data = self.jobs[jobid] except KeyError: return jstate = data.get ('job-state', cups.IPP_JOB_PROCESSING) s = int (jstate) if s == cups.IPP_JOB_PROCESSING: icon = self.icon_jobs_processing else: icon = self.icon_jobs if s == cups.IPP_JOB_HELD: try: theme = Gtk.IconTheme.get_default () emblem = theme.load_icon (Gtk.STOCK_MEDIA_PAUSE, 22 / 2, 0) copy = icon.copy () emblem.composite (copy, 0, 0, copy.get_width (), copy.get_height (), copy.get_width () / 2 - 1, copy.get_height () / 2 - 1, 1.0, 1.0, GdkPixbuf.InterpType.BILINEAR, 255) icon = copy except GObject.GError: debugprint ("No %s icon available" % Gtk.STOCK_MEDIA_PAUSE) else: # Check state reasons. printer = data['job-printer-name'] icon = self.add_state_reason_emblem (icon, printer=printer) cell.set_property ("pixbuf", icon) def _set_job_status_text (self, column, cell, model, iter, *data): jobid = model.get_value (iter, 0) try: data = self.jobs[jobid] except KeyError: return try: text = data['_status_text'] except KeyError: text = self._find_job_state_text (jobid) data['_status_text'] = text printer = data['job-printer-name'] reasons = self.printer_state_reasons.get (printer, []) if len (reasons) > 0: worst_reason = reasons[0] for reason in reasons[1:]: if reason > worst_reason: worst_reason = reason (title, unused) = worst_reason.get_description () text += " - " + title cell.set_property ("text", text)