home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / system-config-printer / jobviewer.py < prev    next >
Encoding:
Python Source  |  2010-09-28  |  85.8 KB  |  2,214 lines

  1.  
  2. ## Copyright (C) 2007, 2008, 2009, 2010 Red Hat, Inc.
  3. ## Authors:
  4. ##  Tim Waugh <twaugh@redhat.com>
  5. ##  Jiri Popelka <jpopelka@redhat.com>
  6.  
  7. ## This program is free software; you can redistribute it and/or modify
  8. ## it under the terms of the GNU General Public License as published by
  9. ## the Free Software Foundation; either version 2 of the License, or
  10. ## (at your option) any later version.
  11.  
  12. ## This program is distributed in the hope that it will be useful,
  13. ## but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  15. ## GNU General Public License for more details.
  16.  
  17. ## You should have received a copy of the GNU General Public License
  18. ## along with this program; if not, write to the Free Software
  19. ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  20.  
  21. import asyncconn
  22. import authconn
  23. import cups
  24. import dbus
  25. import dbus.glib
  26. import dbus.service
  27. import pynotify
  28. import gettext
  29. import glib
  30. import gobject
  31. import gtk
  32. import gtk.gdk
  33. from gui import GtkGUI
  34. import monitor
  35. import os, shutil
  36. import pango
  37. import pwd
  38. import smburi
  39. import subprocess
  40. import sys
  41. import time
  42. import urllib
  43. from xml.sax import saxutils
  44.  
  45. from debug import *
  46. import config
  47. import statereason
  48. import errordialogs
  49.  
  50. try:
  51.     import gnomekeyring
  52.     USE_KEYRING=True
  53. except ImportError:
  54.     USE_KEYRING=False
  55.  
  56. from gettext import gettext as _
  57. DOMAIN="system-config-printer"
  58. gettext.textdomain (DOMAIN)
  59. from statereason import StateReason
  60. statereason.set_gettext_function (_)
  61. errordialogs.set_gettext_function (_)
  62.  
  63. pkgdata = config.pkgdatadir
  64. ICON="printer"
  65. SEARCHING_ICON="document-print-preview"
  66.  
  67. # We need to call pynotify.init before we can check the server for caps
  68. pynotify.init('System Config Printer Notification')
  69.  
  70. class PrinterURIIndex:
  71.     def __init__ (self, names=[]):
  72.         self.printer = {}
  73.         self.names = names
  74.         c = cups.Connection ()
  75.         for name in names:
  76.             self.add_printer (name, connection=c)
  77.  
  78.     def add_printer (self, printer, connection=None):
  79.         try:
  80.             self._map_printer (name=printer, connection=connection)
  81.         except KeyError:
  82.             return
  83.  
  84.     def update_from_attrs (self, printer, attrs):
  85.         uris = []
  86.         if attrs.has_key ('printer-uri-supported'):
  87.             uri_supported = attrs['printer-uri-supported']
  88.             if type (uri_supported) != list:
  89.                 uri_supported = [uri_supported]
  90.             uris.extend (uri_supported)
  91.         if attrs.has_key ('notify-printer-uri'):
  92.             uris.append (attrs['notify-printer-uri'])
  93.         if attrs.has_key ('printer-more-info'):
  94.             uris.append (attrs['printer-more-info'])
  95.  
  96.         for uri in uris:
  97.             self.printer[uri] = printer
  98.  
  99.     def remove_printer (self, printer):
  100.         # Remove references to this printer in the URI map.
  101.         uris = self.printer.keys ()
  102.         for uri in uris:
  103.             if self.printer[uri] == printer:
  104.                 del self.printer[uri]
  105.  
  106.     def lookup (self, uri, connection=None):
  107.         try:
  108.             return self.printer[uri]
  109.         except KeyError:
  110.             return self._map_printer (uri=uri, connection=connection)
  111.  
  112.     def all_printer_names (self):
  113.         return set (self.printer.values ())
  114.  
  115.     def lookup_cached_by_name (self, name):
  116.         for uri, printer in self.printer.iteritems ():
  117.             if printer == name:
  118.                 return uri
  119.  
  120.         raise KeyError
  121.  
  122.     def _map_printer (self, uri=None, name=None, connection=None):
  123.         if connection == None:
  124.             connection = cups.Connection ()
  125.  
  126.         r = ['printer-name', 'printer-uri-supported', 'printer-more-info']
  127.         try:
  128.             if uri != None:
  129.                 attrs = connection.getPrinterAttributes (uri=uri,
  130.                                                          requested_attributes=r)
  131.             else:
  132.                 attrs = connection.getPrinterAttributes (name,
  133.                                                          requested_attributes=r)
  134.         except cups.IPPError:
  135.             # URI not known.
  136.             raise KeyError
  137.  
  138.         name = attrs['printer-name']
  139.         self.update_from_attrs (name, attrs)
  140.         if uri != None:
  141.             self.printer[uri] = name
  142.         return name
  143.  
  144.  
  145. class CancelJobsOperation:
  146.     def __init__ (self, jobviewer, jobids, purge_job):
  147.         self.jobviewer = jobviewer
  148.         self.jobids = list (jobids)
  149.         self.purge_job = purge_job
  150.         if purge_job:
  151.             if len(self.jobids) > 1:
  152.                 dialog_title = _("Delete Jobs")
  153.                 dialog_label = _("Do you really want to delete these jobs?")
  154.             else:
  155.                 dialog_title = _("Delete Job")
  156.                 dialog_label = _("Do you really want to delete this job?")
  157.         else:
  158.             if len(self.jobids) > 1:
  159.                 dialog_title = _("Cancel Jobs")
  160.                 dialog_label = _("Do you really want to cancel these jobs?")
  161.             else:
  162.                 dialog_title = _("Cancel Job")
  163.                 dialog_label = _("Do you really want to cancel this job?")
  164.  
  165.         dialog = gtk.Dialog (dialog_title, jobviewer.JobsWindow,
  166.                              gtk.DIALOG_MODAL |
  167.                              gtk.DIALOG_DESTROY_WITH_PARENT |
  168.                              gtk.DIALOG_NO_SEPARATOR,
  169.                              (_("Keep Printing"), gtk.RESPONSE_NO,
  170.                               dialog_title, gtk.RESPONSE_YES))
  171.         dialog.set_default_response (gtk.RESPONSE_NO)
  172.         dialog.set_border_width (6)
  173.         dialog.set_resizable (False)
  174.         hbox = gtk.HBox (False, 12)
  175.         image = gtk.Image ()
  176.         image.set_from_stock (gtk.STOCK_DIALOG_QUESTION, gtk.ICON_SIZE_DIALOG)
  177.         image.set_alignment (0.0, 0.0)
  178.         hbox.pack_start (image, False, False, 0)
  179.         label = gtk.Label (dialog_label)
  180.         label.set_line_wrap (True)
  181.         label.set_alignment (0.0, 0.0)
  182.         hbox.pack_start (label, False, False, 0)
  183.         dialog.vbox.pack_start (hbox, False, False, 0)
  184.         dialog.set_data ('job-ids', self.jobids)
  185.         dialog.connect ("response", self.on_job_cancel_prompt_response)
  186.         dialog.connect ("delete-event", self.on_job_cancel_prompt_delete)
  187.         dialog.show_all ()
  188.         self.dialog = dialog
  189.         self.connection = None
  190.         debugprint ("+%s" % self)
  191.  
  192.     def __del__ (self):
  193.         debugprint ("-%s" % self)
  194.  
  195.     def destroy (self):
  196.         self.jobviewer = None
  197.         self.connection = None
  198.         if self.dialog:
  199.             self.dialog.destroy ()
  200.             self.dialog = None
  201.  
  202.         debugprint ("DESTROY: %s" % self)
  203.  
  204.     def on_job_cancel_prompt_delete (self, dialog, event):
  205.         self.on_job_cancel_prompt_response (dialog, gtk.RESPONSE_NO)
  206.  
  207.     def on_job_cancel_prompt_response (self, dialog, response):
  208.         dialog.destroy ()
  209.         self.dialog = None
  210.  
  211.         if response != gtk.RESPONSE_YES:
  212.             return
  213.  
  214.         c = asyncconn.Connection (host=self.jobviewer.host,
  215.                                   port=self.jobviewer.port,
  216.                                   encryption=self.jobviewer.encryption)
  217.         self.connection = c
  218.  
  219.         if self.purge_job:
  220.             operation = _("deleting job")
  221.         else:
  222.             operation = _("canceling job")
  223.  
  224.         self.connection._begin_operation (operation)
  225.         self.connection.cancelJob (self.jobids[0], self.purge_job,
  226.                                    reply_handler=self.cancelJob_finish,
  227.                                    error_handler=self.cancelJob_error)
  228.  
  229.     def cancelJob_error (self, connection, exc):
  230.         debugprint ("cancelJob_error %s:%s" % (connection, repr (exc)))
  231.         if self.jobviewer == None:
  232.             return
  233.  
  234.         if type (exc) == cups.IPPError:
  235.             (e, m) = exc.args
  236.             if (e != cups.IPP_NOT_POSSIBLE and
  237.                 e != cups.IPP_NOT_FOUND):
  238.                 self.jobviewer.show_IPP_Error (e, m)
  239.             self.cancelJob_finish(connection, None)
  240.         else:
  241.             self.connection._end_operation ()
  242.             self.connection.destroy ()
  243.             self.jobviewer.update_monitor ()
  244.             raise exc
  245.  
  246.     def cancelJob_finish (self, connection, result):
  247.         debugprint ("cancelJob_finish %s:%s" % (connection, repr (result)))
  248.         if self.jobviewer == None:
  249.             return
  250.  
  251.         del self.jobids[0]
  252.         if not self.jobids:
  253.             # Last job canceled.
  254.             self.connection._end_operation ()
  255.             self.connection.destroy ()
  256.             self.jobviewer.update_monitor ()
  257.         else:
  258.             # there are other jobs to cancel/delete
  259.             connection.cancelJob (self.jobids[0], self.purge_job,
  260.                                   reply_handler=self.cancelJob_finish,
  261.                                   error_handler=self.cancelJob_error)
  262.  
  263. class JobViewer (GtkGUI, monitor.Watcher):
  264.     required_job_attributes = set(['job-k-octets',
  265.                                    'job-name',
  266.                                    'job-originating-user-name',
  267.                                    'job-printer-uri',
  268.                                    'job-state',
  269.                                    'time-at-creation'])
  270.  
  271.     def __init__(self, bus=None, loop=None,
  272.                  trayicon=False, suppress_icon_hide=False,
  273.                  my_jobs=True, specific_dests=None, exit_handler=None,
  274.                  parent=None):
  275.         self.loop = loop
  276.         self.trayicon = trayicon
  277.         self.suppress_icon_hide = suppress_icon_hide
  278.         self.my_jobs = my_jobs
  279.         self.specific_dests = specific_dests
  280.         self.exit_handler = exit_handler
  281.  
  282.         self.jobs = {}
  283.         self.jobiters = {}
  284.         self.jobids = []
  285.         self.jobs_attrs = {} # dict of jobid->(GtkListStore, page_index)
  286.         self.active_jobs = set() # of job IDs
  287.         self.stopped_job_prompts = set() # of job IDs
  288.         self.printer_state_reasons = {}
  289.         self.num_jobs_when_hidden = 0
  290.         self.connecting_to_device = {} # dict of printer->time first seen
  291.         self.state_reason_notifications = {}
  292.         self.auth_info_dialogs = {} # by job ID
  293.         self.job_creation_times_timer = None
  294.         self.special_status_icon = False
  295.         self.new_printer_notifications = {}
  296.         self.completed_job_notifications = {}
  297.         self.authenticated_jobs = set() # of job IDs
  298.         self.ops = []
  299.  
  300.         self.getWidgets ({"JobsWindow":
  301.                               ["JobsWindow",
  302.                                "treeview",
  303.                                "statusbar",
  304.                                "toolbar"],
  305.                           "statusicon_popupmenu":
  306.                               ["statusicon_popupmenu"]},
  307.  
  308.                          domain=DOMAIN)
  309.  
  310.         job_action_group = gtk.ActionGroup ("JobActionGroup")
  311.         job_action_group.add_actions ([
  312.                 ("cancel-job", gtk.STOCK_CANCEL, _("_Cancel"), None,
  313.                  _("Cancel selected jobs"), self.on_job_cancel_activate),
  314.                 ("delete-job", gtk.STOCK_DELETE, _("_Delete"), None,
  315.                  _("Delete selected jobs"), self.on_job_delete_activate),
  316.                 ("hold-job", gtk.STOCK_MEDIA_PAUSE, _("_Hold"), None,
  317.                  _("Hold selected jobs"), self.on_job_hold_activate),
  318.                 ("release-job", gtk.STOCK_MEDIA_PLAY, _("_Release"), None,
  319.                  _("Release selected jobs"), self.on_job_release_activate),
  320.                 ("reprint-job", gtk.STOCK_REDO, _("Re_print"), None,
  321.                  _("Reprint selected jobs"), self.on_job_reprint_activate),
  322.                 ("retrieve-job", gtk.STOCK_SAVE_AS, _("Re_trieve"), None,
  323.                  _("Retrieve selected jobs"), self.on_job_retrieve_activate),
  324.                 ("move-job", None, _("_Move To"), None, None, None),
  325.                 ("authenticate-job", None, _("_Authenticate"), None, None,
  326.                  self.on_job_authenticate_activate),
  327.                 ("job-attributes", None, _("_View Attributes"), None, None,
  328.                  self.on_job_attributes_activate),
  329.                 ("close", gtk.STOCK_CLOSE, None, "<ctrl>w",
  330.                  _("Close this window"), self.on_delete_event)
  331.                 ])
  332.         self.job_ui_manager = gtk.UIManager ()
  333.         self.job_ui_manager.insert_action_group (job_action_group, -1)
  334.         self.job_ui_manager.add_ui_from_string (
  335. """
  336. <ui>
  337.  <accelerator action="cancel-job"/>
  338.  <accelerator action="delete-job"/>
  339.  <accelerator action="hold-job"/>
  340.  <accelerator action="release-job"/>
  341.  <accelerator action="reprint-job"/>
  342.  <accelerator action="retrieve-job"/>
  343.  <accelerator action="move-job"/>
  344.  <accelerator action="authenticate-job"/>
  345.  <accelerator action="job-attributes"/>
  346.  <accelerator action="close"/>
  347. </ui>
  348. """
  349. )
  350.         self.job_ui_manager.ensure_update ()
  351.         self.JobsWindow.add_accel_group (self.job_ui_manager.get_accel_group ())
  352.         self.job_context_menu = gtk.Menu ()
  353.         for action_name in ["cancel-job",
  354.                             "delete-job",
  355.                             "hold-job",
  356.                             "release-job",
  357.                             "reprint-job",
  358.                             "retrieve-job",
  359.                             "move-job",
  360.                             None,
  361.                             "authenticate-job",
  362.                             "job-attributes"]:
  363.             if not action_name:
  364.                 item = gtk.SeparatorMenuItem ()
  365.             else:
  366.                 action = job_action_group.get_action (action_name)
  367.                 action.set_sensitive (False)
  368.                 item = action.create_menu_item ()
  369.  
  370.                 if action_name == 'move-job':
  371.                     self.move_job_menuitem = item
  372.                     printers = gtk.Menu ()
  373.                     item.set_submenu (printers)
  374.  
  375.             item.show ()
  376.             self.job_context_menu.append (item)
  377.  
  378.         for action_name in ["cancel-job",
  379.                             "delete-job",
  380.                             "hold-job",
  381.                             "release-job",
  382.                             "reprint-job",
  383.                             "retrieve-job",
  384.                             "close"]:
  385.             action = job_action_group.get_action (action_name)
  386.             action.set_sensitive (action_name == "close")
  387.             action.set_is_important (action_name == "close")
  388.             item = action.create_tool_item ()
  389.             item.show ()
  390.             self.toolbar.insert (item, -1)
  391.  
  392.         for skip, ellipsize, name, setter in \
  393.                 [(False, False, _("Job"), self._set_job_job_number_text),
  394.                  (True, False, _("User"), self._set_job_user_text),
  395.                  (False, True, _("Document"), self._set_job_document_text),
  396.                  (False, True, _("Printer"), self._set_job_printer_text),
  397.                  (False, False, _("Size"), self._set_job_size_text)]:
  398.             if trayicon and skip:
  399.                 # Skip the user column for the trayicon.
  400.                 continue
  401.  
  402.             cell = gtk.CellRendererText()
  403.             if ellipsize:
  404.                 # Ellipsize the 'Document' and 'Printer' columns.
  405.                 cell.set_property ("ellipsize", pango.ELLIPSIZE_END)
  406.                 cell.set_property ("width-chars", 20)
  407.             column = gtk.TreeViewColumn(name, cell)
  408.             column.set_cell_data_func (cell, setter)
  409.             column.set_resizable(True)
  410.             self.treeview.append_column(column)
  411.  
  412.         cell = gtk.CellRendererText ()
  413.         column = gtk.TreeViewColumn (_("Time submitted"), cell, text=1)
  414.         column.set_resizable (True)
  415.         self.treeview.append_column (column)
  416.  
  417.         column = gtk.TreeViewColumn (_("Status"))
  418.         icon = gtk.CellRendererPixbuf ()
  419.         column.pack_start (icon, False)
  420.         text = gtk.CellRendererText ()
  421.         text.set_property ("ellipsize", pango.ELLIPSIZE_END)
  422.         text.set_property ("width-chars", 20)
  423.         column.pack_start (text, True)
  424.         column.set_cell_data_func (icon, self._set_job_status_icon)
  425.         column.set_cell_data_func (text, self._set_job_status_text)
  426.         self.treeview.append_column (column)
  427.  
  428.         self.store = gtk.TreeStore(int, str)
  429.         self.store.set_sort_column_id (0, gtk.SORT_DESCENDING)
  430.         self.treeview.set_model(self.store)
  431.         self.treeview.set_rules_hint (True)
  432.         self.selection = self.treeview.get_selection()
  433.         self.selection.set_mode(gtk.SELECTION_MULTIPLE)
  434.         self.selection.connect('changed', self.on_selection_changed)
  435.         self.treeview.connect ('button_release_event',
  436.                                self.on_treeview_button_release_event)
  437.         self.treeview.connect ('popup-menu', self.on_treeview_popup_menu)
  438.         self.treeview.set_has_tooltip (True)
  439.         self.treeview.connect ('query-tooltip', self._on_treeview_query_tooltip)
  440.  
  441.         self.JobsWindow.set_icon_name (ICON)
  442.         self.JobsWindow.hide ()
  443.  
  444.         if specific_dests:
  445.             the_dests = reduce (lambda x, y: x + ", " + y, specific_dests)
  446.  
  447.         if my_jobs:
  448.             if specific_dests:
  449.                 title = _("my jobs on %s") % the_dests
  450.             else:
  451.                 title = _("my jobs")
  452.         else:
  453.             if specific_dests:
  454.                 title = "%s" % the_dests
  455.             else:
  456.                 title = _("all jobs")
  457.         self.JobsWindow.set_title (_("Document Print Status (%s)") % title)
  458.  
  459.         if parent:
  460.             self.JobsWindow.set_transient_for (parent)
  461.  
  462.         self.statusbar_set = False
  463.  
  464.         theme = gtk.icon_theme_get_default ()
  465.         self.icon_jobs = theme.load_icon (ICON, 22, 0)
  466.         self.icon_jobs_processing = theme.load_icon ("printer-printing",
  467.                                                      22, 0)
  468.         self.icon_no_jobs = self.icon_jobs.copy ()
  469.         self.icon_no_jobs.fill (0)
  470.         self.icon_jobs.composite (self.icon_no_jobs,
  471.                                   0, 0,
  472.                                   self.icon_no_jobs.get_width(),
  473.                                   self.icon_no_jobs.get_height(),
  474.                                   0, 0,
  475.                                   1.0, 1.0,
  476.                                   gtk.gdk.INTERP_BILINEAR,
  477.                                   127)
  478.         if self.trayicon:
  479.             self.statusicon = gtk.StatusIcon ()
  480.             pixbuf = theme.load_icon (ICON, 22, 0)
  481.             self.statusicon.set_from_pixbuf (pixbuf)    
  482.             self.set_statusicon_from_pixbuf (self.icon_no_jobs)
  483.             self.statusicon.connect ('activate', self.toggle_window_display)
  484.             self.statusicon.connect ('popup-menu', self.on_icon_popupmenu)
  485.             self.statusicon.set_visible (False)
  486.  
  487.         # D-Bus
  488.         if bus == None:
  489.             bus = dbus.SystemBus ()
  490.  
  491.         self.set_process_pending (True)
  492.         self.host = cups.getServer ()
  493.         self.port = cups.getPort ()
  494.         self.encryption = cups.getEncryption ()
  495.         self.monitor = monitor.Monitor (self, bus=bus, my_jobs=my_jobs,
  496.                                         host=self.host, port=self.port,
  497.                                         encryption=self.encryption)
  498.  
  499.         if not self.trayicon:
  500.             self.JobsWindow.show ()
  501.  
  502.         self.JobsAttributesWindow = gtk.Window()
  503.         self.JobsAttributesWindow.set_title (_("Job attributes"))
  504.         self.JobsAttributesWindow.set_position(gtk.WIN_POS_MOUSE)
  505.         self.JobsAttributesWindow.set_default_size(600, 600)
  506.         self.JobsAttributesWindow.set_transient_for (self.JobsWindow)
  507.         self.JobsAttributesWindow.connect("delete_event",
  508.                                           self.job_attributes_on_delete_event)
  509.         self.JobsAttributesWindow.add_accel_group (self.job_ui_manager.get_accel_group ())
  510.         attrs_action_group = gtk.ActionGroup ("AttrsActionGroup")
  511.         attrs_action_group.add_actions ([
  512.                 ("close", gtk.STOCK_CLOSE, None, "<ctrl>w",
  513.                  _("Close this window"), self.job_attributes_on_delete_event)
  514.                 ])
  515.         self.attrs_ui_manager = gtk.UIManager ()
  516.         self.attrs_ui_manager.insert_action_group (attrs_action_group, -1)
  517.         self.attrs_ui_manager.add_ui_from_string (
  518. """
  519. <ui>
  520.  <accelerator action="close"/>
  521. </ui>
  522. """
  523. )
  524.         self.attrs_ui_manager.ensure_update ()
  525.         self.JobsAttributesWindow.add_accel_group (self.attrs_ui_manager.get_accel_group ())
  526.         vbox = gtk.VBox ()
  527.         self.JobsAttributesWindow.add (vbox)
  528.         toolbar = gtk.Toolbar ()
  529.         action = self.attrs_ui_manager.get_action ("/close")
  530.         item = action.create_tool_item ()
  531.         item.set_is_important (True)
  532.         toolbar.insert (item, 0)
  533.         vbox.pack_start (toolbar, False, False, 0)
  534.         self.notebook = gtk.Notebook()
  535.         vbox.pack_start (self.notebook)
  536.  
  537.     def cleanup (self):
  538.         self.monitor.cleanup ()
  539.  
  540.         # Close any open notifications.
  541.         for l in [self.new_printer_notifications.values (),
  542.                   self.state_reason_notifications.values ()]:
  543.             for notification in l:
  544.                 if notification.get_data ('closed') != True:
  545.                     try:
  546.                         notification.close ()
  547.                     except glib.GError:
  548.                         # Can fail if the notification wasn't even shown
  549.                         # yet (as in bug #571603).
  550.                         pass
  551.                     notification.set_data ('closed', True)
  552.  
  553.         if self.job_creation_times_timer != None:
  554.             gobject.source_remove (self.job_creation_times_timer)
  555.             self.job_creation_times_timer = None
  556.  
  557.         for op in self.ops:
  558.             op.destroy ()
  559.  
  560.         if self.exit_handler:
  561.             self.exit_handler (self)
  562.  
  563.     def set_process_pending (self, whether):
  564.         self.process_pending_events = whether
  565.  
  566.     # Handle "special" status icon
  567.     def set_special_statusicon (self, iconname, tooltip=None):
  568.         self.special_status_icon = True
  569.         self.statusicon.set_from_icon_name (iconname)
  570.         self.set_statusicon_visibility ()
  571.         if tooltip != None:
  572.             self.set_statusicon_tooltip (tooltip=tooltip)
  573.  
  574.     def unset_special_statusicon (self):
  575.         self.special_status_icon = False
  576.         self.statusicon.set_from_pixbuf (self.saved_statusicon_pixbuf)
  577.         self.set_statusicon_visibility ()
  578.         self.set_statusicon_tooltip ()
  579.  
  580.     def notify_new_printer (self, printer, notification):
  581.         self.new_printer_notifications[printer] = notification
  582.         notification.set_data ('printer-name', printer)
  583.         notification.connect ('closed', self.on_new_printer_notification_closed)
  584.         self.set_statusicon_visibility ()
  585.         notification.attach_to_status_icon (self.statusicon)
  586.         try:
  587.             notification.show ()
  588.         except gobject.GError:
  589.             nonfatalException ()
  590.  
  591.     def on_new_printer_notification_closed (self, notification, reason=None):
  592.         printer = notification.get_data ('printer-name')
  593.         del self.new_printer_notifications[printer]
  594.         self.set_statusicon_visibility ()
  595.  
  596.     def set_statusicon_from_pixbuf (self, pb):
  597.         self.saved_statusicon_pixbuf = pb
  598.         if not self.special_status_icon:
  599.             self.statusicon.set_from_pixbuf (pb)
  600.  
  601.     def on_delete_event(self, *args):
  602.         if self.trayicon or not self.loop:
  603.             self.JobsWindow.hide ()
  604.             self.JobsWindow.set_data ('visible', False)
  605.             if not self.loop:
  606.                 # Being run from main app, not applet
  607.                 self.cleanup ()
  608.         else:
  609.             self.loop.quit ()
  610.         return True
  611.  
  612.     def job_attributes_on_delete_event(self, widget, event=None):
  613.         for page in range(self.notebook.get_n_pages()):
  614.             self.notebook.remove_page(-1)
  615.         self.jobs_attrs = {}
  616.         self.JobsAttributesWindow.hide_all()
  617.         return True
  618.  
  619.     def show_IPP_Error(self, exception, message):
  620.         return errordialogs.show_IPP_Error (exception, message, self.JobsWindow)
  621.  
  622.     def toggle_window_display(self, icon, force_show=False):
  623.         visible = self.JobsWindow.get_data('visible')
  624.         if force_show:
  625.             visible = False
  626.  
  627.         if visible:
  628.             w = self.JobsWindow.window
  629.             aw = self.JobsAttributesWindow.window
  630.             (s, area, o) = self.statusicon.get_geometry ()
  631.             w.set_skip_taskbar_hint (True)
  632.             if aw != None:
  633.                 aw.set_skip_taskbar_hint (True)
  634.  
  635.             w.property_change ("_NET_WM_ICON_GEOMETRY",
  636.                                "CARDINAL", 32,
  637.                                gtk.gdk.PROP_MODE_REPLACE,
  638.                                list (area))
  639.             self.JobsWindow.iconify ()
  640.         else:
  641.             self.JobsWindow.present ()
  642.             self.JobsWindow.window.set_skip_taskbar_hint (False)
  643.             aw = self.JobsAttributesWindow.window
  644.             if aw != None:
  645.                 aw.set_skip_taskbar_hint (False)
  646.  
  647.         self.JobsWindow.set_data ('visible', not visible)
  648.  
  649.     def on_show_completed_jobs_clicked(self, toggletoolbutton):
  650.         if toggletoolbutton.get_active():
  651.             which_jobs = "all"
  652.         else:
  653.             which_jobs = "not-completed"
  654.         self.monitor.refresh(which_jobs=which_jobs, refresh_all=False)
  655.  
  656.  
  657.     def update_job_creation_times(self):
  658.         now = time.time ()
  659.         need_update = False
  660.         for job, data in self.jobs.iteritems():
  661.             t = _("Unknown")
  662.             if data.has_key ('time-at-creation'):
  663.                 created = data['time-at-creation']
  664.                 ago = now - created
  665.                 need_update = True
  666.                 if ago < 2 * 60:
  667.                     t = _("a minute ago")
  668.                 elif ago < 60 * 60:
  669.                     mins = int (ago / 60)
  670.                     t = _("%d minutes ago") % mins
  671.                 elif ago < 24 * 60 * 60:
  672.                     hours = int (ago / (60 * 60))
  673.                     if hours == 1:
  674.                         t = _("an hour ago")
  675.                     else:
  676.                         t = _("%d hours ago") % hours
  677.                 elif ago < 7 * 24 * 60 * 60:
  678.                     days = int (ago / (24 * 60 * 60))
  679.                     if days == 1:
  680.                         t = _("yesterday")
  681.                     else:
  682.                         t = _("%d days ago") % days
  683.                 elif ago < 6 * 7 * 24 * 60 * 60:
  684.                     weeks = int (ago / (7 * 24 * 60 * 60))
  685.                     if weeks == 1:
  686.                         t = _("last week")
  687.                     else:
  688.                         t = _("%d weeks ago") % weeks
  689.                 else:
  690.                     need_update = False
  691.                     t = time.strftime ("%B %Y", time.localtime (created))
  692.  
  693.             if self.jobiters.has_key (job):
  694.                 iter = self.jobiters[job]
  695.                 self.store.set_value (iter, 1, t)
  696.  
  697.         if need_update and not self.job_creation_times_timer:
  698.             def update_times_with_locking ():
  699.                 gtk.gdk.threads_enter ()
  700.                 ret = self.update_job_creation_times ()
  701.                 gtk.gdk.threads_leave ()
  702.                 return ret
  703.  
  704.             t = gobject.timeout_add_seconds (60, update_times_with_locking)
  705.             self.job_creation_times_timer = t
  706.  
  707.         if not need_update:
  708.             if self.job_creation_times_timer:
  709.                 gobject.source_remove (self.job_creation_times_timer)
  710.                 self.job_creation_times_timer = None
  711.  
  712.         # Return code controls whether the timeout will recur.
  713.         return need_update
  714.  
  715.     def print_error_dialog_response(self, dialog, response, jobid):
  716.         dialog.hide ()
  717.         dialog.destroy ()
  718.         self.stopped_job_prompts.remove (jobid)
  719.         if response == gtk.RESPONSE_NO:
  720.             # Diagnose
  721.             if not self.__dict__.has_key ('troubleshooter'):
  722.                 import troubleshoot
  723.                 troubleshooter = troubleshoot.run (self.on_troubleshoot_quit)
  724.                 self.troubleshooter = troubleshooter
  725.  
  726.     def on_troubleshoot_quit(self, troubleshooter):
  727.         del self.troubleshooter
  728.  
  729.     def add_job (self, job, data, connection=None):
  730.         self.update_job (job, data, connection=connection)
  731.  
  732.         store = self.store
  733.         iter = self.store.append (None)
  734.         store.set_value (iter, 0, job)
  735.         debugprint ("Job %d added" % job)
  736.         self.jobiters[job] = iter
  737.  
  738.         range = self.treeview.get_visible_range ()
  739.         if range != None:
  740.             (start, end) = range
  741.             if (self.store.get_sort_column_id () == (0,
  742.                                                      gtk.SORT_DESCENDING) and
  743.                 start == (1,)):
  744.                 # This job was added job above the visible range, and
  745.                 # we are sorting by descending job ID.  Scroll to it.
  746.                 self.treeview.scroll_to_cell ((0,), None, False, 0.0, 0.0)
  747.  
  748.         if not self.job_creation_times_timer:
  749.             def start_updating_job_creation_times():
  750.                 gtk.gdk.threads_enter ()
  751.                 self.update_job_creation_times ()
  752.                 gtk.gdk.threads_leave ()
  753.                 return False
  754.  
  755.             gobject.timeout_add (500, start_updating_job_creation_times)
  756.  
  757.     def update_monitor (self):
  758.         self.monitor.update ()
  759.  
  760.     def update_job (self, job, data, connection=None):
  761.         # Fetch required attributes for this job if they are missing.
  762.         r = self.required_job_attributes - set (data.keys ())
  763.  
  764.         # If we are showing attributes of this job at this moment, update them.
  765.         if job in self.jobs_attrs:
  766.             self.update_job_attributes_viewer(job)
  767.  
  768.         if r:
  769.             attrs = None
  770.             try:
  771.                 if connection == None:
  772.                     connection = cups.Connection (host=self.host,
  773.                                                   port=self.port,
  774.                                                   encryption=self.encryption)
  775.  
  776.                 debugprint ("requesting %s" % r)
  777.                 r = list (r)
  778.                 attrs = connection.getJobAttributes (job,
  779.                                                      requested_attributes=r)
  780.             except RuntimeError:
  781.                 pass
  782.             except AttributeError:
  783.                 pass
  784.             except cups.IPPError:
  785.                 # someone else may have purged the job
  786.                 return
  787.  
  788.             if attrs:
  789.                 data.update (attrs)
  790.  
  791.         self.jobs[job] = data
  792.  
  793.         job_requires_auth = False
  794.         try:
  795.             jstate = data.get ('job-state', cups.IPP_JOB_PROCESSING)
  796.             s = int (jstate)
  797.  
  798.             if s in [cups.IPP_JOB_HELD, cups.IPP_JOB_STOPPED]:
  799.                 jattrs = ['job-state', 'job-hold-until']
  800.                 pattrs = ['auth-info-required', 'device-uri']
  801.                 uri = data.get ('job-printer-uri')
  802.                 c = authconn.Connection (self.JobsWindow,
  803.                                          host=self.host,
  804.                                          port=self.port,
  805.                                          encryption=self.encryption)
  806.                 attrs = c.getPrinterAttributes (uri = uri,
  807.                                                 requested_attributes=pattrs)
  808.  
  809.                 try:
  810.                     auth_info_required = attrs['auth-info-required']
  811.                 except KeyError:
  812.                     debugprint ("No auth-info-required attribute; "
  813.                                 "guessing instead")
  814.                     auth_info_required = ['username', 'password']
  815.  
  816.                 if not isinstance (auth_info_required, list):
  817.                     auth_info_required = [auth_info_required]
  818.                     attrs['auth-info-required'] = auth_info_required
  819.  
  820.                 data.update (attrs)
  821.  
  822.                 attrs = c.getJobAttributes (job,
  823.                                             requested_attributes=jattrs)
  824.                 data.update (attrs)
  825.                 jstate = data.get ('job-state', cups.IPP_JOB_PROCESSING)
  826.                 s = int (jstate)
  827.         except ValueError:
  828.             pass
  829.         except RuntimeError:
  830.             pass
  831.         except cups.IPPError, (e, m):
  832.             pass
  833.  
  834.         # Invalidate the cached status description and redraw the treeview.
  835.         try:
  836.             del data['_status_text']
  837.         except KeyError:
  838.             pass
  839.         self.treeview.queue_draw ()
  840.  
  841.         # Check whether authentication is required.
  842.         if self.trayicon:
  843.             job_requires_auth = (s == cups.IPP_JOB_HELD and
  844.                                  data.get ('job-hold-until', 'none') ==
  845.                                  'auth-info-required')
  846.  
  847.             if (job_requires_auth and
  848.                 not self.auth_info_dialogs.has_key (job)):
  849.                 try:
  850.                     cups.require ("1.9.37")
  851.                 except:
  852.                     debugprint ("Authentication required but "
  853.                                 "authenticateJob() not available")
  854.                     return
  855.  
  856.                 # Find out which auth-info is required.
  857.                 try_keyring = USE_KEYRING
  858.                 keyring_attrs = dict()
  859.                 auth_info = None
  860.                 if try_keyring and 'password' in auth_info_required:
  861.                     auth_info_required = data.get ('auth-info-required', [])
  862.                     device_uri = data.get ("device-uri")
  863.                     (scheme, rest) = urllib.splittype (device_uri)
  864.                     if scheme == 'smb':
  865.                         uri = smburi.SMBURI (uri=device_uri)
  866.                         (group, server, share,
  867.                          user, password) = uri.separate ()
  868.                         keyring_attrs["domain"] = str (group)
  869.                     else:
  870.                         (serverport, rest) = urllib.splithost (rest)
  871.                         (server, port) = urllib.splitnport (serverport)
  872.                     keyring_attrs.update ({ "server": str (server.lower ()),
  873.                                             "protocol": str (scheme)})
  874.  
  875.                 if job in self.authenticated_jobs:
  876.                     # We've already tried to authenticate this job before.
  877.                     try_keyring = False
  878.  
  879.                 if try_keyring and 'password' in auth_info_required:
  880.                     type = gnomekeyring.ITEM_NETWORK_PASSWORD
  881.                     try:
  882.                         items = gnomekeyring.find_items_sync (type,
  883.                                                               keyring_attrs)
  884.                         auth_info = map (lambda x: '', auth_info_required)
  885.                         ind = auth_info_required.index ('username')
  886.                         auth_info[ind] = items[0].attributes.get ('user', '')
  887.                         ind = auth_info_required.index ('password')
  888.                         auth_info[ind] = items[0].secret
  889.                     except gnomekeyring.NoMatchError:
  890.                         debugprint ("gnomekeyring: no match for %s" %
  891.                                     keyring_attrs)
  892.                     except gnomekeyring.DeniedError:
  893.                         debugprint ("gnomekeyring: denied for %s" %
  894.                                     keyring_attrs)
  895.  
  896.                 if try_keyring and c == None:
  897.                     try:
  898.                         c = authconn.Connection (self.JobsWindow,
  899.                                                  host=self.host,
  900.                                                  port=self.port,
  901.                                                  encryption=self.encryption)
  902.                     except RuntimeError:
  903.                         try_keyring = False
  904.  
  905.                 if try_keyring and auth_info != None:
  906.                     try:
  907.                         c._begin_operation (_("authenticating job"))
  908.                         c.authenticateJob (job, auth_info)
  909.                         c._end_operation ()
  910.                         self.monitor.update ()
  911.                         debugprint ("Automatically authenticated job %d" % job)
  912.                         self.authenticated_jobs.add (job)
  913.                         return
  914.                     except cups.IPPError, (e, m):
  915.                         c._end_operation ()
  916.                         nonfatalException ()
  917.                         return
  918.                     except:
  919.                         c._end_operation ()
  920.                         nonfatalException ()
  921.  
  922.                 username = pwd.getpwuid (os.getuid ())[0]
  923.                 keyring_attrs["user"] = str (username)
  924.                 self.display_auth_info_dialog (job, keyring_attrs)
  925.         self.update_sensitivity ()
  926.  
  927.     def display_auth_info_dialog (self, job, keyring_attrs=None):
  928.         data = self.jobs[job]
  929.         auth_info_required = data['auth-info-required']
  930.         dialog = authconn.AuthDialog (auth_info_required=auth_info_required,
  931.                                       allow_remember=USE_KEYRING)
  932.         dialog.set_data ('keyring-attrs', keyring_attrs)
  933.         dialog.set_data ('auth-info-required', auth_info_required)
  934.         dialog.set_position (gtk.WIN_POS_CENTER)
  935.  
  936.         # Pre-fill 'username' field.
  937.         auth_info = map (lambda x: '', auth_info_required)
  938.         username = pwd.getpwuid (os.getuid ())[0]
  939.         if 'username' in auth_info_required:
  940.             try:
  941.                 ind = auth_info_required.index ('username')
  942.                 auth_info[ind] = username
  943.                 dialog.set_auth_info (auth_info)
  944.             except:
  945.                 nonfatalException ()
  946.  
  947.         # Focus on the first empty field.
  948.         index = 0
  949.         for field in auth_info_required:
  950.             if auth_info[index] == '':
  951.                 dialog.field_grab_focus (field)
  952.                 break
  953.             index += 1
  954.  
  955.         dialog.set_prompt (_("Authentication required for "
  956.                              "printing document `%s' (job %d)") %
  957.                            (data.get('job-name', _("Unknown")), job))
  958.         self.auth_info_dialogs[job] = dialog
  959.         dialog.connect ('response', self.auth_info_dialog_response)
  960.         dialog.connect ('delete-event', self.auth_info_dialog_delete)
  961.         dialog.set_data ('job-id', job)
  962.         dialog.show_all ()
  963.         dialog.set_keep_above (True)
  964.         dialog.show_now ()
  965.  
  966.     def auth_info_dialog_delete (self, dialog, event):
  967.         self.auth_info_dialog_response (dialog, gtk.RESPONSE_CANCEL)
  968.  
  969.     def auth_info_dialog_response (self, dialog, response):
  970.         jobid = dialog.get_data ('job-id')
  971.         del self.auth_info_dialogs[jobid]
  972.  
  973.         if response != gtk.RESPONSE_OK:
  974.             dialog.destroy ()
  975.             return
  976.  
  977.         auth_info = dialog.get_auth_info ()
  978.         try:
  979.             c = authconn.Connection (self.JobsWindow,
  980.                                      host=self.host,
  981.                                      port=self.port,
  982.                                      encryption=self.encryption)
  983.         except RuntimeError:
  984.             debugprint ("Error connecting to CUPS for authentication")
  985.             return
  986.  
  987.         remember = False
  988.         c._begin_operation (_("authenticating job"))
  989.         try:
  990.             c.authenticateJob (jobid, auth_info)
  991.             remember = dialog.get_remember_password ()
  992.             self.authenticated_jobs.add (jobid)
  993.             self.monitor.update ()
  994.         except cups.IPPError, (e, m):
  995.             self.show_IPP_Error (e, m)
  996.  
  997.         c._end_operation ()
  998.  
  999.         if remember:
  1000.             try:
  1001.                 keyring = gnomekeyring.get_default_keyring_sync ()
  1002.                 type = gnomekeyring.ITEM_NETWORK_PASSWORD
  1003.                 attrs = dialog.get_data ("keyring-attrs")
  1004.                 auth_info_required = dialog.get_data ('auth-info-required')
  1005.                 if attrs != None and auth_info_required != None:
  1006.                     try:
  1007.                         ind = auth_info_required.index ('username')
  1008.                         attrs['user'] = auth_info[ind]
  1009.                     except IndexError:
  1010.                         pass
  1011.  
  1012.                     name = "%s@%s (%s)" % (attrs.get ("user"),
  1013.                                            attrs.get ("server"),
  1014.                                            attrs.get ("protocol"))
  1015.                     ind = auth_info_required.index ('password')
  1016.                     secret = auth_info[ind]
  1017.                     id = gnomekeyring.item_create_sync (keyring, type, name,
  1018.                                                         attrs, secret, True)
  1019.                     debugprint ("keyring: created id %d for %s" % (id, name))
  1020.             except:
  1021.                 nonfatalException ()
  1022.  
  1023.         dialog.destroy ()
  1024.  
  1025.     def set_statusicon_visibility (self):
  1026.         if not self.trayicon:
  1027.             return
  1028.  
  1029.         if self.suppress_icon_hide:
  1030.             # Avoid hiding the icon if we've been woken up to notify
  1031.             # about a new printer.
  1032.             self.suppress_icon_hide = False
  1033.             return
  1034.  
  1035.         open_notifications = len (self.new_printer_notifications.keys ())
  1036.         open_notifications += len (self.completed_job_notifications.keys ())
  1037.         for reason, notification in self.state_reason_notifications.iteritems():
  1038.             if notification.get_data ('closed') != True:
  1039.                 open_notifications += 1
  1040.         num_jobs = len (self.active_jobs)
  1041.  
  1042.         debugprint ("open notifications: %d" % open_notifications)
  1043.         debugprint ("num_jobs: %d" % num_jobs)
  1044.         debugprint ("num_jobs_when_hidden: %d" % self.num_jobs_when_hidden)
  1045.  
  1046.         self.statusicon.set_visible (self.special_status_icon or
  1047.                                      open_notifications > 0 or
  1048.                                      num_jobs > self.num_jobs_when_hidden)
  1049.  
  1050.         # Let the icon show/hide itself before continuing.
  1051.         while self.process_pending_events and gtk.events_pending ():
  1052.             gtk.main_iteration ()
  1053.  
  1054.     def on_treeview_popup_menu (self, treeview):
  1055.         event = gtk.gdk.Event (gtk.gdk.NOTHING)
  1056.         self.show_treeview_popup_menu (treeview, event, 0)
  1057.  
  1058.     def on_treeview_button_release_event(self, treeview, event):
  1059.         if event.button == 3:
  1060.             self.show_treeview_popup_menu (treeview, event, event.button)
  1061.  
  1062.     def update_sensitivity (self, selection = None):
  1063.         if (selection is None):
  1064.             selection = self.treeview.get_selection () 
  1065.         (model, pathlist) = selection.get_selected_rows()
  1066.         cancel = self.job_ui_manager.get_action ("/cancel-job")
  1067.         delete = self.job_ui_manager.get_action ("/delete-job")
  1068.         hold = self.job_ui_manager.get_action ("/hold-job")
  1069.         release = self.job_ui_manager.get_action ("/release-job")
  1070.         reprint = self.job_ui_manager.get_action ("/reprint-job")
  1071.         retrieve = self.job_ui_manager.get_action ("/retrieve-job")
  1072.         authenticate = self.job_ui_manager.get_action ("/authenticate-job")
  1073.         attributes = self.job_ui_manager.get_action ("/job-attributes")
  1074.         move = self.job_ui_manager.get_action ("/move-job")
  1075.         if len (pathlist) == 0:
  1076.             for widget in [cancel, delete, hold, release, reprint, retrieve,
  1077.                            move, authenticate, attributes]:
  1078.                 widget.set_sensitive (False)
  1079.             return
  1080.  
  1081.         cancel_sensitive = True
  1082.         hold_sensitive = True
  1083.         release_sensitive = True
  1084.         reprint_sensitive = True
  1085.         authenticate_sensitive = True
  1086.         move_sensitive = False
  1087.         other_printers = self.printer_uri_index.all_printer_names ()
  1088.         job_printers = dict()
  1089.  
  1090.         self.jobids = []
  1091.         for path in pathlist:
  1092.             iter = self.store.get_iter (path)
  1093.             jobid = self.store.get_value (iter, 0)
  1094.             self.jobids.append(jobid)
  1095.             job = self.jobs[jobid]
  1096.  
  1097.             if job.has_key ('job-state'):
  1098.                 s = job['job-state']
  1099.                 if s >= cups.IPP_JOB_CANCELED:
  1100.                     cancel_sensitive = False
  1101.                 if s != cups.IPP_JOB_PENDING:
  1102.                     hold_sensitive = False
  1103.                 if s != cups.IPP_JOB_HELD:
  1104.                     release_sensitive = False
  1105.                 if (not job.get('job-preserved', False)):
  1106.                     reprint_sensitive = False
  1107.  
  1108.             if (job.get ('job-state',
  1109.                          cups.IPP_JOB_CANCELED) != cups.IPP_JOB_HELD or
  1110.                 job.get ('job-hold-until', 'none') != 'auth-info-required'):
  1111.                 authenticate_sensitive = False
  1112.  
  1113.             uri = job.get ('job-printer-uri', None)
  1114.             if uri:
  1115.                 printer = self.printer_uri_index.lookup (uri)
  1116.                 job_printers[printer] = uri
  1117.  
  1118.         if len (job_printers.keys ()) == 1:
  1119.             try:
  1120.                 other_printers.remove (job_printers.keys ()[0])
  1121.             except KeyError:
  1122.                 pass
  1123.  
  1124.         if len (other_printers) > 0:
  1125.             printers_menu = gtk.Menu ()
  1126.             other_printers = list (other_printers)
  1127.             other_printers.sort ()
  1128.             for printer in other_printers:
  1129.                 uri = self.printer_uri_index.lookup_cached_by_name (printer)
  1130.                 menuitem = gtk.MenuItem (printer, False)
  1131.                 menuitem.set_sensitive (True)
  1132.                 menuitem.show ()
  1133.                 menuitem.connect ('activate', self.on_job_move_activate, uri)
  1134.                 printers_menu.append (menuitem)
  1135.  
  1136.             self.move_job_menuitem.set_submenu (printers_menu)
  1137.             move_sensitive = True
  1138.  
  1139.         cancel.set_sensitive(cancel_sensitive)
  1140.         delete.set_sensitive(True)
  1141.         hold.set_sensitive(hold_sensitive)
  1142.         release.set_sensitive(release_sensitive)
  1143.         reprint.set_sensitive(reprint_sensitive)
  1144.         retrieve.set_sensitive(reprint_sensitive)
  1145.         move.set_sensitive (move_sensitive)
  1146.         authenticate.set_sensitive(authenticate_sensitive)
  1147.         attributes.set_sensitive(True)
  1148.  
  1149.     def on_selection_changed (self, selection):
  1150.         self.update_sensitivity (selection)
  1151.  
  1152.     def show_treeview_popup_menu (self, treeview, event, event_button):
  1153.         # Right-clicked.
  1154.         self.job_context_menu.popup (None, None, None, event_button,
  1155.                                      event.get_time ())
  1156.  
  1157.     def on_icon_popupmenu(self, icon, button, time):
  1158.         self.statusicon_popupmenu.popup (None, None, None, button, time)
  1159.  
  1160.     def on_icon_hide_activate(self, menuitem):
  1161.         self.num_jobs_when_hidden = len (self.jobs.keys ())
  1162.         self.set_statusicon_visibility ()
  1163.  
  1164.     def on_icon_configure_printers_activate(self, menuitem):
  1165.         if self.loop:
  1166.             env = {}
  1167.             for name, value in os.environ.iteritems ():
  1168.                 if name == "SYSTEM_CONFIG_PRINTER_UI":
  1169.                     continue
  1170.                 env[name] = value
  1171.             p = subprocess.Popen ([ "system-config-printer" ],
  1172.                                   close_fds=True, env=env)
  1173.             gobject.timeout_add_seconds (10, self.poll_subprocess, p)
  1174.  
  1175.     def poll_subprocess(self, process):
  1176.         returncode = process.poll ()
  1177.         return returncode == None
  1178.  
  1179.     def on_icon_quit_activate (self, menuitem):
  1180.         if self.loop:
  1181.             self.loop.quit ()
  1182.  
  1183.     def on_job_cancel_activate(self, menuitem):
  1184.         self.on_job_cancel_activate2(False)
  1185.  
  1186.     def on_job_delete_activate(self, menuitem):
  1187.         self.on_job_cancel_activate2(True)
  1188.  
  1189.     def on_job_cancel_activate2(self, purge_job):
  1190.         if self.jobids:
  1191.             op = CancelJobsOperation (self, self.jobids, purge_job)
  1192.             self.ops.append (op)
  1193.  
  1194.     def on_job_hold_activate(self, menuitem):
  1195.         try:
  1196.             c = authconn.Connection (self.JobsWindow,
  1197.                                      host=self.host,
  1198.                                      port=self.port,
  1199.                                      encryption=self.encryption)
  1200.         except RuntimeError:
  1201.             return
  1202.  
  1203.         for jobid in self.jobids:
  1204.             c._begin_operation (_("holding job"))
  1205.             try:
  1206.                 c.setJobHoldUntil (jobid, "indefinite")
  1207.             except cups.IPPError, (e, m):
  1208.                 if (e != cups.IPP_NOT_POSSIBLE and
  1209.                     e != cups.IPP_NOT_FOUND):
  1210.                     self.show_IPP_Error (e, m)
  1211.                 self.monitor.update ()
  1212.                 c._end_operation ()
  1213.                 return
  1214.             c._end_operation ()
  1215.  
  1216.         del c
  1217.         self.monitor.update ()
  1218.  
  1219.     def on_job_release_activate(self, menuitem):
  1220.         try:
  1221.             c = authconn.Connection (self.JobsWindow,
  1222.                                      host=self.host,
  1223.                                      port=self.port,
  1224.                                      encryption=self.encryption)
  1225.         except RuntimeError:
  1226.             return
  1227.  
  1228.         for jobid in self.jobids:
  1229.             c._begin_operation (_("releasing job"))
  1230.             try:
  1231.                 c.setJobHoldUntil (jobid, "no-hold")
  1232.             except cups.IPPError, (e, m):
  1233.                 if (e != cups.IPP_NOT_POSSIBLE and
  1234.                     e != cups.IPP_NOT_FOUND):
  1235.                     self.show_IPP_Error (e, m)
  1236.                 self.monitor.update ()
  1237.                 c._end_operation ()
  1238.                 return
  1239.             c._end_operation ()
  1240.  
  1241.         del c
  1242.         self.monitor.update ()
  1243.  
  1244.     def on_job_reprint_activate(self, menuitem):
  1245.         try:
  1246.             c = authconn.Connection (self.JobsWindow,
  1247.                                      host=self.host,
  1248.                                      port=self.port,
  1249.                                      encryption=self.encryption)
  1250.             for jobid in self.jobids:
  1251.                 c.restartJob (jobid)
  1252.             del c
  1253.         except cups.IPPError, (e, m):
  1254.             self.show_IPP_Error (e, m)
  1255.             self.monitor.update ()
  1256.             return
  1257.         except RuntimeError:
  1258.             return
  1259.  
  1260.         self.monitor.update ()
  1261.  
  1262.     def on_job_retrieve_activate(self, menuitem):
  1263.         try:
  1264.             c = authconn.Connection (self.JobsWindow,
  1265.                                      host=self.host,
  1266.                                      port=self.port,
  1267.                                      encryption=self.encryption)
  1268.         except RuntimeError:
  1269.             return
  1270.  
  1271.         for jobid in self.jobids:
  1272.             try:
  1273.                 attrs=c.getJobAttributes(jobid)
  1274.                 printer_uri=attrs['job-printer-uri']
  1275.                 document_count=attrs['document-count']
  1276.                 for document_number in range(1, document_count+1):
  1277.                     document=c.getDocument(printer_uri, jobid, document_number)
  1278.                     tempfile = document.get('file')
  1279.                     name = document.get('document-name')
  1280.                     format = document.get('document-format', '')
  1281.  
  1282.                     # if there's no document-name retrieved
  1283.                     if name == None:
  1284.                         # give the default filename some meaningful name
  1285.                         name = _("retrieved")+str(document_number)
  1286.                         # add extension according to format
  1287.                         if format == 'application/postscript':
  1288.                             name = name + ".ps"
  1289.                         elif format.find('application/vnd.') != -1:
  1290.                             name = name + format.replace('application/vnd', '')
  1291.                         elif format.find('application/') != -1:
  1292.                             name = name + format.replace('application/', '.')
  1293.  
  1294.                     if tempfile != None:
  1295.                         dialog = gtk.FileChooserDialog (_("Save File"),
  1296.                                                         self.JobsWindow,
  1297.                                                   gtk.FILE_CHOOSER_ACTION_SAVE,
  1298.                                         (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
  1299.                                          gtk.STOCK_SAVE, gtk.RESPONSE_OK))
  1300.                         dialog.set_current_name(name)
  1301.                         dialog.set_do_overwrite_confirmation(True)
  1302.  
  1303.                         response = dialog.run()
  1304.                         if response == gtk.RESPONSE_OK:
  1305.                             file_to_save = dialog.get_filename()
  1306.                             try:
  1307.                                 shutil.copyfile(tempfile, file_to_save)
  1308.                             except (IOError, shutil.Error):
  1309.                                 debugprint("Unable to save file "+file_to_save)
  1310.                         elif response == gtk.RESPONSE_CANCEL:
  1311.                             pass
  1312.                         dialog.destroy()
  1313.                         os.unlink(tempfile)
  1314.                     else:
  1315.                         debugprint("Unable to retrieve file from job file")
  1316.                         return
  1317.  
  1318.             except cups.IPPError, (e, m):
  1319.                 self.show_IPP_Error (e, m)
  1320.                 self.monitor.update ()
  1321.                 return
  1322.  
  1323.         del c
  1324.         self.monitor.update ()
  1325.  
  1326.     def on_job_move_activate(self, menuitem, job_printer_uri):
  1327.         try:
  1328.             c = authconn.Connection (self.JobsWindow,
  1329.                                      host=self.host,
  1330.                                      port=self.port,
  1331.                                      encryption=self.encryption)
  1332.             for jobid in self.jobids:
  1333.                 c.moveJob (job_id=jobid, job_printer_uri=job_printer_uri)
  1334.             del c
  1335.         except cups.IPPError, (e, m):
  1336.             self.show_IPP_Error (e, m)
  1337.             self.monitor.update ()
  1338.             return
  1339.         except RuntimeError:
  1340.             return
  1341.         except AttributeError:
  1342.             # Requires pycups >= 1.9.47
  1343.             debugprint ("Move requires pycups >= 1.9.47")
  1344.             return
  1345.  
  1346.         self.monitor.update ()
  1347.  
  1348.     def on_job_authenticate_activate(self, menuitem):
  1349.         for jobid in self.jobids:
  1350.             self.display_auth_info_dialog (jobid)
  1351.  
  1352.     def on_refresh_clicked(self, toolbutton):
  1353.         self.monitor.refresh ()
  1354.         self.update_job_creation_times ()
  1355.  
  1356.     def on_job_attributes_activate(self, menuitem):
  1357.         """ For every selected job create notebook page with attributes. """
  1358.         try:
  1359.             c = cups.Connection (host=self.host,
  1360.                                  port=self.port,
  1361.                                  encryption=self.encryption)
  1362.         except RuntimeError:
  1363.             self.monitor.watcher.cups_connection_error (self)
  1364.             return False
  1365.  
  1366.         for jobid in self.jobids:
  1367.             if jobid not in self.jobs_attrs:
  1368.                 # add new notebook page with scrollable treeview
  1369.                 scrolledwindow = gtk.ScrolledWindow()
  1370.                 label = gtk.Label(jobid) # notebook page has label with jobid
  1371.                 page_index = self.notebook.append_page(scrolledwindow, label)
  1372.                 attr_treeview = gtk.TreeView()
  1373.                 scrolledwindow.add(attr_treeview)
  1374.                 cell = gtk.CellRendererText ()
  1375.                 attr_treeview.insert_column_with_attributes(0, _("Name"),
  1376.                                                             cell, text=0)
  1377.                 cell = gtk.CellRendererText ()
  1378.                 attr_treeview.insert_column_with_attributes(1, _("Value"),
  1379.                                                             cell, text=1)
  1380.                 attr_store = gtk.ListStore(gobject.TYPE_STRING,
  1381.                                            gobject.TYPE_STRING)
  1382.                 attr_treeview.set_model(attr_store)
  1383.                 attr_treeview.get_selection().set_mode(gtk.SELECTION_NONE)
  1384.                 attr_store.set_sort_column_id (0, gtk.SORT_ASCENDING)
  1385.                 self.jobs_attrs[jobid] = (attr_store, page_index)
  1386.                 self.update_job_attributes_viewer (jobid, conn=c)
  1387.  
  1388.         self.JobsAttributesWindow.show_all ()
  1389.  
  1390.     def update_job_attributes_viewer(self, jobid, conn=None):
  1391.         """ Update attributes store with new values. """
  1392.         if conn != None:
  1393.             c = conn
  1394.         else:
  1395.             try:
  1396.                 c = cups.Connection (host=self.host,
  1397.                                      port=self.port,
  1398.                                      encryption=self.encryption)
  1399.             except RuntimeError:
  1400.                 self.monitor.watcher.cups_connection_error (self)
  1401.                 return False
  1402.  
  1403.         if jobid in self.jobs_attrs:
  1404.             (attr_store, page) = self.jobs_attrs[jobid]
  1405.             try:
  1406.                 attrs = c.getJobAttributes(jobid)       # new attributes
  1407.             except AttributeError:
  1408.                 return
  1409.             except cups.IPPError:
  1410.                 # someone else may have purged the job,
  1411.                 # remove jobs notebook page
  1412.                 self.notebook.remove_page(page)
  1413.                 del self.jobs_attrs[jobid]
  1414.                 return
  1415.  
  1416.             attr_store.clear()                          # remove old attributes
  1417.             for name, value in attrs.iteritems():
  1418.                 if name in ['job-id', 'job-printer-up-time']:
  1419.                     continue
  1420.                 attr_store.append([name, value])
  1421.  
  1422.     def job_is_active (self, jobdata):
  1423.         state = jobdata.get ('job-state', cups.IPP_JOB_CANCELED)
  1424.         if state >= cups.IPP_JOB_CANCELED:
  1425.             return False
  1426.  
  1427.         return True
  1428.  
  1429.     ## Icon manipulation
  1430.     def add_state_reason_emblem (self, pixbuf, printer=None):
  1431.         worst_reason = None
  1432.         if printer == None and self.worst_reason != None:
  1433.             # Check that it's valid.
  1434.             printer = self.worst_reason.get_printer ()
  1435.             found = False
  1436.             for reason in self.printer_state_reasons.get (printer, []):
  1437.                 if reason == self.worst_reason:
  1438.                     worst_reason = self.worst_reason
  1439.                     break
  1440.             if worst_reason == None:
  1441.                 self.worst_reason = None
  1442.  
  1443.         if printer != None:
  1444.             for reason in self.printer_state_reasons.get (printer, []):
  1445.                 if worst_reason == None:
  1446.                     worst_reason = reason
  1447.                 elif reason > worst_reason:
  1448.                     worst_reason = reason
  1449.  
  1450.         if worst_reason != None:
  1451.             level = worst_reason.get_level ()
  1452.             if level > StateReason.REPORT:
  1453.                 # Add an emblem to the icon.
  1454.                 icon = StateReason.LEVEL_ICON[level]
  1455.                 pixbuf = pixbuf.copy ()
  1456.                 try:
  1457.                     theme = gtk.icon_theme_get_default ()
  1458.                     emblem = theme.load_icon (icon, 22, 0)
  1459.                     emblem.composite (pixbuf,
  1460.                                       pixbuf.get_width () / 2,
  1461.                                       pixbuf.get_height () / 2,
  1462.                                       emblem.get_width () / 2,
  1463.                                       emblem.get_height () / 2,
  1464.                                       pixbuf.get_width () / 2,
  1465.                                       pixbuf.get_height () / 2,
  1466.                                       0.5, 0.5,
  1467.                                       gtk.gdk.INTERP_BILINEAR, 255)
  1468.                 except gobject.GError:
  1469.                     debugprint ("No %s icon available" % icon)
  1470.  
  1471.         return pixbuf
  1472.  
  1473.     def get_icon_pixbuf (self, have_jobs=None):
  1474.         if not self.trayicon:
  1475.             return
  1476.  
  1477.         if have_jobs == None:
  1478.             have_jobs = len (self.jobs.keys ()) > 0
  1479.  
  1480.         if have_jobs:
  1481.             pixbuf = self.icon_jobs
  1482.             for jobid, jobdata in self.jobs.iteritems ():
  1483.                 jstate = jobdata.get ('job-state', cups.IPP_JOB_PENDING)
  1484.                 if jstate == cups.IPP_JOB_PROCESSING:
  1485.                     pixbuf = self.icon_jobs_processing
  1486.                     break
  1487.         else:
  1488.             pixbuf = self.icon_no_jobs
  1489.  
  1490.         try:
  1491.             pixbuf = self.add_state_reason_emblem (pixbuf)
  1492.         except:
  1493.             nonfatalException ()
  1494.  
  1495.         return pixbuf
  1496.  
  1497.     def set_statusicon_tooltip (self, tooltip=None):
  1498.         if not self.trayicon:
  1499.             return
  1500.  
  1501.         if tooltip == None:
  1502.             num_jobs = len (self.jobs)
  1503.             if num_jobs == 0:
  1504.                 tooltip = _("No documents queued")
  1505.             elif num_jobs == 1:
  1506.                 tooltip = _("1 document queued")
  1507.             else:
  1508.                 tooltip = _("%d documents queued") % num_jobs
  1509.  
  1510.         self.statusicon.set_tooltip (tooltip)
  1511.  
  1512.     def update_status (self, have_jobs=None):
  1513.         # Found out which printer state reasons apply to our active jobs.
  1514.         upset_printers = set()
  1515.         for printer, reasons in self.printer_state_reasons.iteritems ():
  1516.             if len (reasons) > 0:
  1517.                 upset_printers.add (printer)
  1518.         debugprint ("Upset printers: %s" % upset_printers)
  1519.  
  1520.         my_upset_printers = set()
  1521.         if len (upset_printers):
  1522.             my_upset_printers = set()
  1523.             for jobid in self.active_jobs:
  1524.                 # 'job-printer-name' is set by job_added/job_event
  1525.                 printer = self.jobs[jobid]['job-printer-name']
  1526.                 if printer in upset_printers:
  1527.                     my_upset_printers.add (printer)
  1528.             debugprint ("My upset printers: %s" % my_upset_printers)
  1529.  
  1530.         my_reasons = []
  1531.         for printer in my_upset_printers:
  1532.             my_reasons.extend (self.printer_state_reasons[printer])
  1533.  
  1534.         # Find out which is the most problematic.
  1535.         self.worst_reason = None
  1536.         if len (my_reasons) > 0:
  1537.             worst_reason = my_reasons[0]
  1538.             for reason in my_reasons:
  1539.                 if reason > worst_reason:
  1540.                     worst_reason = reason
  1541.             self.worst_reason = worst_reason
  1542.             debugprint ("Worst reason: %s" % worst_reason)
  1543.  
  1544.         if self.worst_reason != None:
  1545.             (title, tooltip) = self.worst_reason.get_description ()
  1546.             if self.statusbar_set:
  1547.                 self.statusbar.pop (0)
  1548.             self.statusbar.push (0, tooltip)
  1549.             self.statusbar_set = True
  1550.         else:
  1551.             tooltip = None
  1552.             if self.statusbar_set:
  1553.                 self.statusbar.pop (0)
  1554.                 self.statusbar_set = False
  1555.  
  1556.         if self.trayicon:
  1557.             pixbuf = self.get_icon_pixbuf (have_jobs=have_jobs)
  1558.             self.set_statusicon_from_pixbuf (pixbuf)
  1559.             self.set_statusicon_visibility ()
  1560.             self.set_statusicon_tooltip (tooltip=tooltip)
  1561.  
  1562.     ## Notifications
  1563.     def notify_printer_state_reason_if_important (self, reason):
  1564.         level = reason.get_level ()
  1565.         if level < StateReason.WARNING:
  1566.             # Not important enough to justify a notification.
  1567.             return
  1568.  
  1569.         self.notify_printer_state_reason (reason)
  1570.  
  1571.     def notify_printer_state_reason (self, reason):
  1572.         tuple = reason.get_tuple ()
  1573.         if self.state_reason_notifications.has_key (tuple):
  1574.             debugprint ("Already sent notification for %s" % repr (reason))
  1575.             return
  1576.  
  1577.         if reason.get_reason () == "com.apple.print.recoverable":
  1578.             return
  1579.  
  1580.         level = reason.get_level ()
  1581.         if (level == StateReason.ERROR or
  1582.             reason.get_reason () == "connecting-to-device"):
  1583.             urgency = pynotify.URGENCY_NORMAL
  1584.         else:
  1585.             urgency = pynotify.URGENCY_LOW
  1586.  
  1587.         (title, text) = reason.get_description ()
  1588.         notification = pynotify.Notification (title, text, 'printer')
  1589.         reason.user_notified = True
  1590.         notification.set_urgency (urgency)
  1591.         if "actions" in pynotify.get_server_caps():
  1592.             notification.set_timeout (pynotify.EXPIRES_NEVER)
  1593.         notification.connect ('closed',
  1594.                               self.on_state_reason_notification_closed)
  1595.         self.state_reason_notifications[reason.get_tuple ()] = notification
  1596.         self.set_statusicon_visibility ()
  1597.         notification.attach_to_status_icon (self.statusicon)
  1598.         try:
  1599.             notification.show ()
  1600.         except gobject.GError:
  1601.             nonfatalException ()
  1602.  
  1603.     def on_state_reason_notification_closed (self, notification, reason=None):
  1604.         debugprint ("Notification %s closed" % repr (notification))
  1605.         notification.set_data ('closed', True)
  1606.         self.set_statusicon_visibility ()
  1607.         return
  1608.  
  1609.     def notify_completed_job (self, jobid):
  1610.         job = self.jobs.get (jobid, {})
  1611.         document = job.get ('job-name', _("Unknown"))
  1612.         printer_uri = job.get ('job-printer-uri')
  1613.         if printer_uri != None:
  1614.             # Determine if this printer is remote.  There's no need to
  1615.             # show a notification if the printer is connected to this
  1616.             # machine.
  1617.  
  1618.             # Find out the device URI.  We might already have
  1619.             # determined this if authentication was required.
  1620.             device_uri = job.get ('device-uri')
  1621.  
  1622.             if device_uri == None:
  1623.                 pattrs = ['device-uri']
  1624.                 c = authconn.Connection (self.JobsWindow,
  1625.                                          host=self.host,
  1626.                                          port=self.port,
  1627.                                          encryption=self.encryption)
  1628.                 try:
  1629.                     attrs = c.getPrinterAttributes (uri=printer_uri,
  1630.                                                     requested_attributes=pattrs)
  1631.                 except cups.IPPError:
  1632.                     return
  1633.  
  1634.                 device_uri = attrs.get ('device-uri')
  1635.  
  1636.             if device_uri != None:
  1637.                 (scheme, rest) = urllib.splittype (device_uri)
  1638.                 if scheme not in ['socket', 'ipp', 'http', 'smb']:
  1639.                     return
  1640.  
  1641.         printer = job.get ('job-printer-name', _("Unknown"))
  1642.         notification = pynotify.Notification (_("Document printed"),
  1643.                                               _("Document `%s' has been sent "
  1644.                                                 "to `%s' for printing.") %
  1645.                                               (document, printer),
  1646.                                               'printer')
  1647.         notification.set_urgency (pynotify.URGENCY_LOW)
  1648.         notification.connect ('closed',
  1649.                               self.on_completed_job_notification_closed)
  1650.         notification.set_data ('jobid', jobid)
  1651.         self.completed_job_notifications[jobid] = notification
  1652.         self.set_statusicon_visibility ()
  1653.         notification.attach_to_status_icon (self.statusicon)
  1654.         try:
  1655.             notification.show ()
  1656.         except gobject.GError:
  1657.             nonfatalException ()
  1658.  
  1659.     def on_completed_job_notification_closed (self, notification, reason=None):
  1660.         jobid = notification.get_data ('jobid')
  1661.         del self.completed_job_notifications[jobid]
  1662.         self.set_statusicon_visibility ()
  1663.  
  1664.     ## monitor.Watcher interface
  1665.     def current_printers_and_jobs (self, mon, printers, jobs):
  1666.         monitor.Watcher.current_printers_and_jobs (self, mon, printers, jobs)
  1667.         self.set_process_pending (False)
  1668.         self.store.clear ()
  1669.         self.jobs = {}
  1670.         self.jobiters = {}
  1671.         self.printer_uri_index = PrinterURIIndex (names=printers)
  1672.         connection = None
  1673.         for jobid, jobdata in jobs.iteritems ():
  1674.             uri = jobdata.get ('job-printer-uri', '')
  1675.             try:
  1676.                 printer = self.printer_uri_index.lookup (uri,
  1677.                                                          connection=connection)
  1678.             except KeyError:
  1679.                 printer = uri
  1680.  
  1681.             if self.specific_dests and printer not in self.specific_dests:
  1682.                 continue
  1683.  
  1684.             jobdata['job-printer-name'] = printer
  1685.  
  1686.             self.add_job (jobid, jobdata, connection=connection)
  1687.  
  1688.         self.jobs = jobs
  1689.         self.active_jobs = set()
  1690.         for jobid, jobdata in jobs.iteritems ():
  1691.             if (self.job_is_active (jobdata) and
  1692.                 jobdata.has_key('job-printer-name')):
  1693.                 self.active_jobs.add (jobid)
  1694.  
  1695.         self.set_process_pending (True)
  1696.         self.update_status ()
  1697.  
  1698.     def job_added (self, mon, jobid, eventname, event, jobdata):
  1699.         monitor.Watcher.job_added (self, mon, jobid, eventname, event, jobdata)
  1700.  
  1701.         uri = jobdata.get ('job-printer-uri', '')
  1702.         try:
  1703.             printer = self.printer_uri_index.lookup (uri)
  1704.         except KeyError:
  1705.             printer = uri
  1706.  
  1707.         if self.specific_dests and printer not in self.specific_dests:
  1708.             return
  1709.  
  1710.         jobdata['job-printer-name'] = printer
  1711.  
  1712.         # We may be showing this job already, perhaps because we are showing
  1713.         # completed jobs and one was reprinted.
  1714.         if not self.jobiters.has_key (jobid):
  1715.             self.add_job (jobid, jobdata)
  1716.  
  1717.         if self.job_is_active (jobdata):
  1718.             self.active_jobs.add (jobid)
  1719.         elif jobid in self.active_jobs:
  1720.             self.active_jobs.remove (jobid)
  1721.  
  1722.         self.update_status (have_jobs=True)
  1723.         if self.trayicon:
  1724.             if not self.job_is_active (jobdata):
  1725.                 return
  1726.  
  1727.             for reason in self.printer_state_reasons.get (printer, []):
  1728.                 if not reason.user_notified:
  1729.                     self.notify_printer_state_reason_if_important (reason)
  1730.  
  1731.     def job_event (self, mon, jobid, eventname, event, jobdata):
  1732.         monitor.Watcher.job_event (self, mon, jobid, eventname, event, jobdata)
  1733.  
  1734.         uri = jobdata.get ('job-printer-uri', '')
  1735.         try:
  1736.             printer = self.printer_uri_index.lookup (uri)
  1737.         except KeyError:
  1738.             printer = uri
  1739.         jobdata['job-printer-name'] = printer
  1740.  
  1741.         if self.job_is_active (jobdata):
  1742.             self.active_jobs.add (jobid)
  1743.         elif jobid in self.active_jobs:
  1744.             self.active_jobs.remove (jobid)
  1745.  
  1746.         self.update_job (jobid, jobdata)
  1747.         self.update_status ()
  1748.         jobdata = self.jobs[jobid]
  1749.  
  1750.         # If the job has finished, let the user know.
  1751.         if self.trayicon and (eventname == 'job-completed' or
  1752.                               (eventname == 'job-state-changed' and
  1753.                                event['job-state'] == cups.IPP_JOB_COMPLETED)):
  1754.             reasons = event['job-state-reasons']
  1755.             if type (reasons) != list:
  1756.                 reasons = [reasons]
  1757.  
  1758.             canceled = False
  1759.             for reason in reasons:
  1760.                 if reason.startswith ("job-canceled"):
  1761.                     canceled = True
  1762.                     break
  1763.  
  1764.             if not canceled:
  1765.                 self.notify_completed_job (jobid)
  1766.  
  1767.         # Look out for stopped jobs.
  1768.         if (self.trayicon and
  1769.             (eventname == 'job-stopped' or
  1770.              (eventname == 'job-state-changed' and
  1771.               event['job-state'] in [cups.IPP_JOB_STOPPED,
  1772.                                      cups.IPP_JOB_PENDING])) and
  1773.             not jobid in self.stopped_job_prompts):
  1774.             # Why has the job stopped?  It might be due to a job error
  1775.             # of some sort, or it might be that the backend requires
  1776.             # authentication.  If the latter, the job will be held not
  1777.             # stopped, and the job-hold-until attribute will be
  1778.             # 'auth-info-required'.  This was already checked for in
  1779.             # update_job.
  1780.             may_be_problem = True
  1781.             jstate = jobdata['job-state']
  1782.             if (jstate == cups.IPP_JOB_PROCESSING or
  1783.                 (jstate == cups.IPP_JOB_HELD and
  1784.                  jobdata['job-hold-until'] == 'auth-info-required')):
  1785.                 # update_job already dealt with this.
  1786.                 may_be_problem = False
  1787.             else:
  1788.                 # Other than that, unfortunately the only
  1789.                 # clue we get is the notify-text, which is not
  1790.                 # translated into our native language.  We'd better
  1791.                 # try parsing it.  In CUPS-1.3.6 the possible strings
  1792.                 # are:
  1793.                 #
  1794.                 # "Job stopped due to filter errors; please consult
  1795.                 # the error_log file for details."
  1796.                 #
  1797.                 # "Job stopped due to backend errors; please consult
  1798.                 # the error_log file for details."
  1799.                 #
  1800.                 # "Job held due to backend errors; please consult the
  1801.                 # error_log file for details."
  1802.                 #
  1803.                 # "Authentication is required for job %d."
  1804.                 # [This case is handled in the update_job method.]
  1805.                 #
  1806.                 # "Job stopped due to printer being paused"
  1807.                 # [This should be ignored, as the job was doing just
  1808.                 # fine until the printer was stopped for other reasons.]
  1809.                 notify_text = event['notify-text']
  1810.                 document = jobdata['job-name']
  1811.                 if notify_text.find ("backend errors") != -1:
  1812.                     message = _("There was a problem sending document `%s' "
  1813.                                 "(job %d) to the printer.") % (document, jobid)
  1814.                 elif notify_text.find ("filter errors") != -1:
  1815.                     message = _("There was a problem processing document `%s' "
  1816.                                 "(job %d).") % (document, jobid)
  1817.                 elif (notify_text.find ("being paused") != -1 or
  1818.                       jstate != cups.IPP_JOB_STOPPED):
  1819.                     may_be_problem = False
  1820.                 else:
  1821.                     # Give up and use the provided message untranslated.
  1822.                     message = _("There was a problem printing document `%s' "
  1823.                                 "(job %d): `%s'.") % (document, jobid,
  1824.                                                       notify_text)
  1825.  
  1826.             if may_be_problem:
  1827.                 debugprint ("Problem detected")
  1828.                 self.toggle_window_display (self.statusicon, force_show=True)
  1829.                 dialog = gtk.Dialog (_("Print Error"), self.JobsWindow, 0,
  1830.                                      (_("_Diagnose"), gtk.RESPONSE_NO,
  1831.                                         gtk.STOCK_OK, gtk.RESPONSE_OK))
  1832.                 dialog.set_default_response (gtk.RESPONSE_OK)
  1833.                 dialog.set_border_width (6)
  1834.                 dialog.set_resizable (False)
  1835.                 dialog.set_icon_name (ICON)
  1836.                 hbox = gtk.HBox (False, 12)
  1837.                 hbox.set_border_width (6)
  1838.                 image = gtk.Image ()
  1839.                 image.set_from_stock (gtk.STOCK_DIALOG_ERROR,
  1840.                                       gtk.ICON_SIZE_DIALOG)
  1841.                 hbox.pack_start (image, False, False, 0)
  1842.                 vbox = gtk.VBox (False, 12)
  1843.  
  1844.                 markup = ('<span weight="bold" size="larger">' +
  1845.                           _("Print Error") + '</span>\n\n' +
  1846.                           saxutils.escape (message))
  1847.                 try:
  1848.                     if event['printer-state'] == cups.IPP_PRINTER_STOPPED:
  1849.                         name = event['printer-name']
  1850.                         markup += ' '
  1851.                         markup += (_("The printer called `%s' has "
  1852.                                      "been disabled.") % name)
  1853.                 except KeyError:
  1854.                     pass
  1855.  
  1856.                 label = gtk.Label (markup)
  1857.                 label.set_use_markup (True)
  1858.                 label.set_line_wrap (True)
  1859.                 label.set_alignment (0, 0)
  1860.                 vbox.pack_start (label, False, False, 0)
  1861.                 hbox.pack_start (vbox, False, False, 0)
  1862.                 dialog.vbox.pack_start (hbox)
  1863.                 dialog.connect ('response',
  1864.                                 self.print_error_dialog_response, jobid)
  1865.                 self.stopped_job_prompts.add (jobid)
  1866.                 dialog.show_all ()
  1867.  
  1868.     def job_removed (self, mon, jobid, eventname, event):
  1869.         monitor.Watcher.job_removed (self, mon, jobid, eventname, event)
  1870.  
  1871.         # If the job has finished, let the user know.
  1872.         if self.trayicon and (eventname == 'job-completed' or
  1873.                               (eventname == 'job-state-changed' and
  1874.                                event['job-state'] == cups.IPP_JOB_COMPLETED)):
  1875.             reasons = event['job-state-reasons']
  1876.             debugprint (reasons)
  1877.             if type (reasons) != list:
  1878.                 reasons = [reasons]
  1879.  
  1880.             canceled = False
  1881.             for reason in reasons:
  1882.                 if reason.startswith ("job-canceled"):
  1883.                     canceled = True
  1884.                     break
  1885.  
  1886.             if not canceled:
  1887.                 self.notify_completed_job (jobid)
  1888.  
  1889.         if self.jobiters.has_key (jobid):
  1890.             self.store.remove (self.jobiters[jobid])
  1891.             del self.jobiters[jobid]
  1892.             del self.jobs[jobid]
  1893.  
  1894.         if jobid in self.active_jobs:
  1895.             self.active_jobs.remove (jobid)
  1896.  
  1897.         if self.jobs_attrs.has_key (jobid):
  1898.             del self.jobs_attrs[jobid]
  1899.  
  1900.         self.update_status ()
  1901.  
  1902.     def state_reason_added (self, mon, reason):
  1903.         monitor.Watcher.state_reason_added (self, mon, reason)
  1904.  
  1905.         (title, text) = reason.get_description ()
  1906.         printer = reason.get_printer ()
  1907.  
  1908.         try:
  1909.             l = self.printer_state_reasons[printer]
  1910.         except KeyError:
  1911.             l = []
  1912.             self.printer_state_reasons[printer] = l
  1913.  
  1914.         reason.user_notified = False
  1915.         l.append (reason)
  1916.         self.update_status ()
  1917.         self.treeview.queue_draw ()
  1918.  
  1919.         if not self.trayicon:
  1920.             return
  1921.  
  1922.         # Find out if the user has jobs queued for that printer.
  1923.         for job, data in self.jobs.iteritems ():
  1924.             if not self.job_is_active (data):
  1925.                 continue
  1926.             if data['job-printer-name'] == printer:
  1927.                 # Yes!  Notify them of the state reason, if necessary.
  1928.                 self.notify_printer_state_reason_if_important (reason)
  1929.                 break
  1930.  
  1931.     def state_reason_removed (self, mon, reason):
  1932.         monitor.Watcher.state_reason_removed (self, mon, reason)
  1933.  
  1934.         printer = reason.get_printer ()
  1935.         try:
  1936.             reasons = self.printer_state_reasons[printer]
  1937.         except KeyError:
  1938.             debugprint ("Printer not found")
  1939.             return
  1940.  
  1941.         try:
  1942.             i = reasons.index (reason)
  1943.         except IndexError:
  1944.             debugprint ("Reason not found")
  1945.             return
  1946.  
  1947.         del reasons[i]
  1948.  
  1949.         self.update_status ()
  1950.         self.treeview.queue_draw ()
  1951.  
  1952.         if not self.trayicon:
  1953.             return
  1954.  
  1955.         tuple = reason.get_tuple ()
  1956.         try:
  1957.             notification = self.state_reason_notifications[tuple]
  1958.             if notification.get_data ('closed') != True:
  1959.                 try:
  1960.                     notification.close ()
  1961.                 except glib.GError:
  1962.                     # Can fail if the notification wasn't even shown
  1963.                     # yet (as in bug #545733).
  1964.                     pass
  1965.  
  1966.             del self.state_reason_notifications[tuple]
  1967.             self.set_statusicon_visibility ()
  1968.         except KeyError:
  1969.             pass
  1970.  
  1971.     def still_connecting (self, mon, reason):
  1972.         monitor.Watcher.still_connecting (self, mon, reason)
  1973.         if not self.trayicon:
  1974.             return
  1975.  
  1976.         self.notify_printer_state_reason (reason)
  1977.  
  1978.     def now_connected (self, mon, printer):
  1979.         monitor.Watcher.now_connected (self, mon, printer)
  1980.  
  1981.         if not self.trayicon:
  1982.             return
  1983.  
  1984.         # Find the connecting-to-device state reason.
  1985.         try:
  1986.             reasons = self.printer_state_reasons[printer]
  1987.             reason = None
  1988.             for r in reasons:
  1989.                 if r.get_reason () == "connecting-to-device":
  1990.                     reason = r
  1991.                     break
  1992.         except KeyError:
  1993.             debugprint ("Couldn't find state reason (no reasons)!")
  1994.  
  1995.         if reason != None:
  1996.             tuple = reason.get_tuple ()
  1997.         else:
  1998.             debugprint ("Couldn't find state reason in list!")
  1999.             for (level,
  2000.                  p,
  2001.                  r) in self.state_reason_notifications.keys ():
  2002.                 if p == printer and r == "connecting-to-device":
  2003.                     debugprint ("Found from notifications list")
  2004.                     tuple = (level, p, r)
  2005.                     break
  2006.  
  2007.         try:
  2008.             notification = self.state_reason_notifications[tuple]
  2009.         except KeyError:
  2010.             debugprint ("Unexpected now_connected signal")
  2011.             return
  2012.  
  2013.         if notification.get_data ('closed') != True:
  2014.             try:
  2015.                 notification.close ()
  2016.             except glib.GError:
  2017.                 # Can fail if the notification wasn't even shown
  2018.                 pass
  2019.             notification.set_data ('closed', True)
  2020.  
  2021.     def printer_added (self, mon, printer):
  2022.         monitor.Watcher.printer_added (self, mon, printer)
  2023.         self.printer_uri_index.add_printer (printer)
  2024.  
  2025.     def printer_event (self, mon, printer, eventname, event):
  2026.         monitor.Watcher.printer_event (self, mon, printer, eventname, event)
  2027.         self.printer_uri_index.update_from_attrs (printer, event)
  2028.  
  2029.     def printer_removed (self, mon, printer):
  2030.         monitor.Watcher.printer_removed (self, mon, printer)
  2031.         self.printer_uri_index.remove_printer (printer)
  2032.  
  2033.     ### Cell data functions
  2034.     def _set_job_job_number_text (self, column, cell, model, iter, *data):
  2035.         cell.set_property("text", str (model.get_value (iter, 0)))
  2036.  
  2037.     def _set_job_user_text (self, column, cell, model, iter, *data):
  2038.         jobid = model.get_value (iter, 0)
  2039.         job = self.jobs[jobid]
  2040.         cell.set_property("text", job.get ('job-originating-user-name',
  2041.                                            _("Unknown")))
  2042.  
  2043.     def _set_job_document_text (self, column, cell, model, iter, *data):
  2044.         jobid = model.get_value (iter, 0)
  2045.         job = self.jobs[jobid]
  2046.         cell.set_property("text", job.get('job-name', _("Unknown")))
  2047.  
  2048.     def _set_job_printer_text (self, column, cell, model, iter, *data):
  2049.         jobid = model.get_value (iter, 0)
  2050.         reasons = self.jobs[jobid].get('job-state-reasons')
  2051.         if reasons == 'printer-stopped':
  2052.             reason = ' - ' + _("disabled")
  2053.         else:
  2054.             reason = ''
  2055.         cell.set_property("text", self.jobs[jobid]['job-printer-name']+reason)
  2056.  
  2057.     def _set_job_size_text (self, column, cell, model, iter, *data):
  2058.         jobid = model.get_value (iter, 0)
  2059.         job = self.jobs[jobid]
  2060.         size = _("Unknown")
  2061.         if job.has_key ('job-k-octets'):
  2062.             size = str (job['job-k-octets']) + 'k'
  2063.         cell.set_property("text", size)
  2064.  
  2065.     def _find_job_state_text (self, job):
  2066.         data = self.jobs[job]
  2067.         jstate = data.get ('job-state', cups.IPP_JOB_PROCESSING)
  2068.         s = int (jstate)
  2069.         job_requires_auth = (s == cups.IPP_JOB_HELD and
  2070.                              data.get ('job-hold-until', 'none') ==
  2071.                              'auth-info-required')
  2072.         state = None
  2073.         if job_requires_auth:
  2074.             state = _("Held for authentication")
  2075.         elif s == cups.IPP_JOB_HELD:
  2076.             state = _("Held")
  2077.             until = data.get ('job-hold-until')
  2078.             if until != None:
  2079.                 try:
  2080.                     colon1 = until.find (':')
  2081.                     if colon1 != -1:
  2082.                         now = time.gmtime ()
  2083.                         hh = int (until[:colon1])
  2084.                         colon2 = until[colon1 + 1:].find (':')
  2085.                         if colon2 != -1:
  2086.                             colon2 += colon1 + 1
  2087.                             mm = int (until[colon1 + 1:colon2])
  2088.                             ss = int (until[colon2 + 1:])
  2089.                         else:
  2090.                             mm = int (until[colon1 + 1:])
  2091.                             ss = 0
  2092.  
  2093.                         day = now.tm_mday
  2094.                         if (hh < now.tm_hour or
  2095.                             (hh == now.tm_hour and
  2096.                              (mm < now.tm_min or
  2097.                               (mm == now.tm_min and ss < now.tm_sec)))):
  2098.                             day += 1
  2099.  
  2100.                         hold = (now.tm_year, now.tm_mon, day,
  2101.                                 hh, mm, ss, 0, 0, -1)
  2102.                         old_tz = os.environ.get("TZ")
  2103.                         os.environ["TZ"] = "UTC"
  2104.                         simpletime = time.mktime (hold)
  2105.  
  2106.                         if old_tz == None:
  2107.                             del os.environ["TZ"]
  2108.                         else:
  2109.                             os.environ["TZ"] = old_tz
  2110.  
  2111.                         local = time.localtime (simpletime)
  2112.                         state = _("Held until %s") % time.strftime ("%X", local)
  2113.                 except ValueError:
  2114.                     pass
  2115.             if until == "day-time":
  2116.                 state = _("Held until day-time")
  2117.             elif until == "evening":
  2118.                 state = _("Held until evening")
  2119.             elif until == "night":
  2120.                 state = _("Held until night-time")
  2121.             elif until == "second-shift":
  2122.                 state = _("Held until second shift")
  2123.             elif until == "third-shift":
  2124.                 state = _("Held until third shift")
  2125.             elif until == "weekend":
  2126.                 state = _("Held until weekend")
  2127.         else:
  2128.             try:
  2129.                 state = { cups.IPP_JOB_PENDING: _("Pending"),
  2130.                           cups.IPP_JOB_PROCESSING: _("Processing"),
  2131.                           cups.IPP_JOB_STOPPED: _("Stopped"),
  2132.                           cups.IPP_JOB_CANCELED: _("Canceled"),
  2133.                           cups.IPP_JOB_ABORTED: _("Aborted"),
  2134.                           cups.IPP_JOB_COMPLETED: _("Completed") }[s]
  2135.             except IndexError:
  2136.                 pass
  2137.  
  2138.         if state == None:
  2139.             state = _("Unknown")
  2140.  
  2141.         return state
  2142.  
  2143.     def _set_job_status_icon (self, column, cell, model, iter, *data):
  2144.         jobid = model.get_value (iter, 0)
  2145.         data = self.jobs[jobid]
  2146.         jstate = data.get ('job-state', cups.IPP_JOB_PROCESSING)
  2147.         s = int (jstate)
  2148.         if s == cups.IPP_JOB_PROCESSING:
  2149.             icon = self.icon_jobs_processing
  2150.         else:
  2151.             icon = self.icon_jobs
  2152.  
  2153.         if s == cups.IPP_JOB_HELD:
  2154.             theme = gtk.icon_theme_get_default ()
  2155.             emblem = theme.load_icon (gtk.STOCK_MEDIA_PAUSE, 22 / 2, 0)
  2156.             copy = icon.copy ()
  2157.             emblem.composite (copy, 0, 0,
  2158.                               copy.get_width (),
  2159.                               copy.get_height (),
  2160.                               copy.get_width () / 2 - 1,
  2161.                               copy.get_height () / 2 - 1,
  2162.                               1.0, 1.0,
  2163.                               gtk.gdk.INTERP_NEAREST, 255)
  2164.             icon = copy
  2165.         else:
  2166.             # Check state reasons.
  2167.             printer = data['job-printer-name']
  2168.             icon = self.add_state_reason_emblem (icon, printer=printer)
  2169.  
  2170.         cell.set_property ("pixbuf", icon)
  2171.  
  2172.     def _set_job_status_text (self, column, cell, model, iter, *data):
  2173.         jobid = model.get_value (iter, 0)
  2174.         data = self.jobs[jobid]
  2175.         try:
  2176.             text = data['_status_text']
  2177.         except KeyError:
  2178.             text = self._find_job_state_text (jobid)
  2179.             data['_status_text'] = text
  2180.  
  2181.         printer = data['job-printer-name']
  2182.         reasons = self.printer_state_reasons.get (printer, [])
  2183.         if len (reasons) > 0:
  2184.             worst_reason = reasons[0]
  2185.             for reason in reasons[1:]:
  2186.                 if reason > worst_reason:
  2187.                     worst_reason = reason
  2188.             (title, unused) = worst_reason.get_description ()
  2189.             text += " - " + title
  2190.  
  2191.         cell.set_property ("text", text)
  2192.  
  2193.     def _on_treeview_query_tooltip (self, tv, x, y, keyboard_mode, tooltip):
  2194.         if keyboard_mode:
  2195.             (path, column) = tv.get_cursor ()
  2196.             if path is None:
  2197.                 return False
  2198.         else:
  2199.             bin_x, bin_y = tv.convert_widget_to_bin_window_coords (x, y)
  2200.             ret = tv.get_path_at_pos (bin_x, bin_y)
  2201.             if ret is None:
  2202.                 return False
  2203.             path = ret[0]
  2204.             column = ret[1]
  2205.  
  2206.         cells = column.get_cell_renderers ()
  2207.         for cell in cells:
  2208.             if type (cell) == gtk.CellRendererText:
  2209.                 tooltip.set_markup (cell.get_property ("text"))
  2210.                 self.treeview.set_tooltip_cell (tooltip, path, column, cell)
  2211.                 break
  2212.  
  2213.         return True
  2214.