~neon/project-neon/printer-applet

« back to all changes in this revision

Viewing changes to printer-applet.py

  • Committer: Jonathan Riddell
  • Date: 2008-03-24 00:53:35 UTC
  • Revision ID: git-v1:1a8383f25060c9c9682766319f2e68a7621e5768
add printer-applet, a replacement for kjobviewer.  a Python app.  please test.

svn path=/trunk/kdereview/printer-applet/; revision=789361

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
 
 
3
#############################################################################
 
4
##
 
5
## Copyright 2007 Canonical Ltd
 
6
## Author: Jonathan Riddell <jriddell@ubuntu.com>
 
7
##
 
8
## Includes code from System Config Printer
 
9
## Copyright 2007 Tim Waugh <twaugh@redhat.com>
 
10
## Copyright 2007 Red Hat, Inc.
 
11
##
 
12
## This program is free software; you can redistribute it and/or
 
13
## modify it under the terms of the GNU General Public License as
 
14
## published by the Free Software Foundation; either version 2 of 
 
15
## the License, or (at your option) any later version.
 
16
##
 
17
## This program is distributed in the hope that it will be useful,
 
18
## but WITHOUT ANY WARRANTY; without even the implied warranty of
 
19
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
20
## GNU General Public License for more details.
 
21
##
 
22
## You should have received a copy of the GNU General Public License
 
23
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
24
##
 
25
#############################################################################
 
26
 
 
27
"""
 
28
A systray applet to show that documents are being printed, show printer warnings and errors and a GUI for hal-cups-utils automatic setup for new printers.
 
29
 
 
30
It is a Qt port of the applet from Red Hat's System Config Printer
 
31
http://cyberelk.net/tim/software/system-config-printer/
 
32
svn co http://svn.fedorahosted.org/svn/system-config-printer/trunk
 
33
"""
 
34
 
 
35
import os
 
36
import subprocess
 
37
import sys
 
38
 
 
39
try:
 
40
    KDE4CONFIG = "kde4-config"
 
41
    KDEDIR = subprocess.Popen([KDE4CONFIG, "--prefix"], stdout=subprocess.PIPE).communicate()[0]
 
42
    KDEDATADIR = subprocess.Popen([KDE4CONFIG, "--path", "data"], stdout=subprocess.PIPE).communicate()[0]
 
43
except OSError:
 
44
    try:
 
45
        KDE4CONFIG = "/usr/lib/kde4/bin/kde4-config"
 
46
        KDEDIR = subprocess.Popen([KDE4CONFIG, "--prefix"], stdout=subprocess.PIPE).communicate()[0]
 
47
        KDEDATADIR = subprocess.Popen([KDE4CONFIG, "--path", "data"], stdout=subprocess.PIPE).communicate()[0]
 
48
    except OSError:
 
49
        print "Could not find kde4-config, exiting."
 
50
        sys.exit(1)
 
51
KDEDIR = KDEDIR.rstrip()
 
52
KDEDATADIR = KDEDATADIR.rstrip()
 
53
KDEDATADIR = KDEDATADIR.split(":")[-1]
 
54
 
 
55
if os.path.exists("printer-applet.ui"):
 
56
    APPDIR="."
 
57
else:
 
58
    APPDIR=KDEDATADIR + "/printer-applet"
 
59
 
 
60
MIN_REFRESH_INTERVAL = 1 # seconds
 
61
DOMAIN="printer-applet"
 
62
CONNECTING_TIMEOUT = 60 # seconds
 
63
 
 
64
import time
 
65
 
 
66
from PyQt4.QtCore import *
 
67
from PyQt4.QtGui import *
 
68
from PyQt4 import uic
 
69
 
 
70
import gettext
 
71
gettext.textdomain(DOMAIN)
 
72
def i18n(string):
 
73
    return unicode(gettext.gettext(string), "utf-8")
 
74
 
 
75
def translate(self, prop):
 
76
    """reimplement method from uic to change it to use gettext"""
 
77
    if prop.get("notr", None) == "true":
 
78
        return self._cstring(prop)
 
79
    else:
 
80
        if prop.text is None:
 
81
            return ""
 
82
        text = prop.text.encode("UTF-8")
 
83
        return i18n(text)
 
84
 
 
85
uic.properties.Properties._string = translate
 
86
 
 
87
import cups
 
88
 
 
89
import dbus
 
90
import dbus.mainloop.qt
 
91
import dbus.service
 
92
 
 
93
class MainWindow(QMainWindow):
 
94
    """Our main GUI dialogue, overridden so that closing it doesn't quit the app"""
 
95
 
 
96
    def closeEvent(self, event):
 
97
        event.ignore()
 
98
        self.hide()
 
99
 
 
100
class PrintersWindow(QWidget):
 
101
    """The printer status dialogue, overridden so that closing it doesn't quit the app and to untick the show menu entry"""
 
102
 
 
103
    def __init__(self, applet):
 
104
        QWidget.__init__(self)
 
105
        self.applet = applet
 
106
 
 
107
    def closeEvent(self, event):
 
108
        event.ignore()
 
109
        self.applet.on_printer_status_delete_event()
 
110
        self.hide()
 
111
 
 
112
class StateReason:
 
113
    REPORT=1
 
114
    WARNING=2
 
115
    ERROR=3
 
116
 
 
117
    LEVEL_ICON={
 
118
        REPORT: "dialog-info",
 
119
        WARNING: "dialog-warning",
 
120
        ERROR: "dialog-error"
 
121
        }
 
122
 
 
123
    def __init__(self, printer, reason):
 
124
        self.printer = printer
 
125
        self.reason = reason
 
126
        self.level = None
 
127
        self.canonical_reason = None
 
128
 
 
129
    def get_printer (self):
 
130
        return self.printer
 
131
 
 
132
    def get_level (self):
 
133
        if self.level != None:
 
134
            return self.level
 
135
 
 
136
        if (self.reason.endswith ("-report") or
 
137
            self.reason == "connecting-to-device"):
 
138
            self.level = self.REPORT
 
139
        elif self.reason.endswith ("-warning"):
 
140
            self.level = self.WARNING
 
141
        else:
 
142
            self.level = self.ERROR
 
143
        return self.level
 
144
 
 
145
    def get_reason (self):
 
146
        if self.canonical_reason:
 
147
            return self.canonical_reason
 
148
 
 
149
        level = self.get_level ()
 
150
        reason = self.reason
 
151
        if level == self.WARNING and reason.endswith ("-warning"):
 
152
            reason = reason[:-8]
 
153
        elif level == self.ERROR and reason.endswith ("-error"):
 
154
            reason = reason[:-6]
 
155
        self.canonical_reason = reason
 
156
        return self.canonical_reason
 
157
 
 
158
    def get_description (self):
 
159
        messages = {
 
160
            'toner-low': (i18n("Toner low"),
 
161
                          i18n("Printer '%s' is low on toner.")),
 
162
            'toner-empty': (i18n("Toner empty"),
 
163
                            i18n("Printer '%s' has no toner left.")),
 
164
            'cover-open': (i18n("Cover open"),
 
165
                           i18n("The cover is open on printer '%s'.")),
 
166
            'door-open': (i18n("Door open"),
 
167
                          i18n("The door is open on printer '%s'.")),
 
168
            'media-low': (i18n("Paper low"),
 
169
                          i18n("Printer '%s' is low on paper.")),
 
170
            'media-empty': (i18n("Out of paper"),
 
171
                            i18n("Printer '%s' is out of paper.")),
 
172
            'marker-supply-low': (i18n("Ink low"),
 
173
                                  i18n("Printer '%s' is low on ink.")),
 
174
            'marker-supply-empty': (i18n("Ink empty"),
 
175
                                    i18n("Printer '%s' has no ink left.")),
 
176
            'connecting-to-device': (i18n("Not connected?"),
 
177
                                     i18n("Printer '%s' may not be connected.")),
 
178
            }
 
179
        try:
 
180
            (title, text) = messages[self.get_reason ()]
 
181
            text = text % self.get_printer ()
 
182
        except KeyError:
 
183
            if self.get_level () == self.REPORT:
 
184
                title = i18n("Printer report")
 
185
            elif self.get_level () == self.WARNING:
 
186
                title = i18n("Printer warning")
 
187
            elif self.get_level () == self.ERROR:
 
188
                title = i18n("Printer error")
 
189
            text = i18n("Printer '%s': '%s'.") % (self.get_printer (),
 
190
                                               self.get_reason ())
 
191
        return (title, text)
 
192
 
 
193
    def get_tuple (self):
 
194
        return (self.get_level (), self.get_printer (), self.get_reason ())
 
195
 
 
196
    def __cmp__(self, other):
 
197
        if other == None:
 
198
            return 1
 
199
        if other.get_level () != self.get_level ():
 
200
            return self.get_level () < other.get_level ()
 
201
        if other.get_printer () != self.get_printer ():
 
202
            return other.get_printer () < self.get_printer ()
 
203
        return other.get_reason () < self.get_reason ()
 
204
 
 
205
def collect_printer_state_reasons (connection):
 
206
    result = []
 
207
    printers = connection.getPrinters ()
 
208
    for name, printer in printers.iteritems ():
 
209
        reasons = printer["printer-state-reasons"]
 
210
        if type (reasons) == str:
 
211
            # Work around a bug that was fixed in pycups-1.9.20.
 
212
            reasons = [reasons]
 
213
        for reason in reasons:
 
214
            if reason == "none":
 
215
                break
 
216
            if (reason.startswith ("moving-to-paused") or
 
217
                reason.startswith ("paused") or
 
218
                reason.startswith ("shutdown") or
 
219
                reason.startswith ("stopping") or
 
220
                reason.startswith ("stopped-partly")):
 
221
                continue
 
222
            result.append (StateReason (name, reason))
 
223
    return result
 
224
 
 
225
def worst_printer_state_reason (connection, printer_reasons=None):
 
226
    """Fetches the printer list and checks printer-state-reason for
 
227
    each printer, returning a StateReason for the most severe
 
228
    printer-state-reason, or None."""
 
229
    worst_reason = None
 
230
    if printer_reasons == None:
 
231
        printer_reasons = collect_printer_state_reasons (connection)
 
232
    for reason in printer_reasons:
 
233
        if worst_reason == None:
 
234
            worst_reason = reason
 
235
            continue
 
236
        if reason > worst_reason:
 
237
            worst_reason = reason
 
238
 
 
239
    return worst_reason
 
240
 
 
241
 
 
242
class JobManager(QObject):
 
243
    """our main class creates the systray icon and the dialogues and refreshes the dialogues for new information"""
 
244
    def __init__(self, parent = None):
 
245
        QObject.__init__(self)
 
246
 
 
247
        self.will_refresh = False # whether timeout is set
 
248
        self.last_refreshed = 0
 
249
        self.trayicon = True
 
250
        self.suppress_icon_hide = False
 
251
        self.which_jobs = "not-completed"
 
252
        self.hidden = False
 
253
        self.jobs = {}
 
254
        self.jobiters = {}
 
255
        self.will_update_job_creation_times = False # whether timeout is set
 
256
        self.statusbar_set = False
 
257
        self.reasons_seen = {}
 
258
        self.connecting_to_device = {} # dict of printer->time first seen
 
259
        self.still_connecting = set()
 
260
        self.special_status_icon = False
 
261
 
 
262
        self.mainWindow = MainWindow()
 
263
        uic.loadUi(APPDIR + "/" + "printer-applet.ui", self.mainWindow)
 
264
 
 
265
        self.printersWindow = PrintersWindow(self)
 
266
        uic.loadUi(APPDIR + "/" + "printer-applet-printers.ui", self.printersWindow)
 
267
 
 
268
        self.sysTray = QSystemTrayIcon(QIcon(KDEDIR + "/share/icons/oxygen/22x22/devices/printer.png"), self.mainWindow)
 
269
        #self.sysTray.show()
 
270
        self.connect(self.sysTray, SIGNAL("activated( QSystemTrayIcon::ActivationReason )"), self.showMainWindow)
 
271
 
 
272
        self.menu = QMenu()
 
273
        self.menu.addAction(i18n("_Hide").replace("_", ""), self.on_icon_hide_activate)
 
274
        self.menu.addAction(QIcon(KDEDIR + "/share/icons/oxygen/16x16/actions/application-exit.png"), i18n("Quit"), self.on_icon_quit_activate)
 
275
        self.sysTray.setContextMenu(self.menu)
 
276
 
 
277
        self.mainWindow.actionClose.setIcon(QIcon(KDEDIR + "/share/icons/oxygen/16x16/actions/window-close.png"))
 
278
        self.mainWindow.actionRefresh_2.setIcon(QIcon(KDEDIR + "/share/icons/oxygen/16x16/actions/view-refresh.png"))
 
279
 
 
280
        self.connect(self.mainWindow.actionRefresh_2, SIGNAL("triggered(bool)"), self.on_refresh_activate)
 
281
        self.connect(self.mainWindow.actionShow_Completed_Jobs_2, SIGNAL("triggered(bool)"), self.on_show_completed_jobs_activate)
 
282
        self.connect(self.mainWindow.actionShow_Printer_Status, SIGNAL("triggered(bool)"), self.on_show_printer_status_activate)
 
283
        self.connect(self.mainWindow.actionClose, SIGNAL("triggered(bool)"), self.hideMainWindow)
 
284
 
 
285
        self.mainWindow.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu)
 
286
        self.connect(self.mainWindow.treeWidget, SIGNAL("customContextMenuRequested(const QPoint&)"), self.on_treeview_button_press_event)
 
287
        #self.connect(self.mainWindow.treeWidget, SIGNAL("itemClicked(QTreeWidgetItem*, int)"), self.printItemClicked)
 
288
        self.rightClickMenu = QMenu(self.mainWindow.treeWidget)
 
289
        self.cancel = self.rightClickMenu.addAction(i18n("Cancel"), self.on_job_cancel_activate)
 
290
        self.hold = self.rightClickMenu.addAction(i18n("_Hold").replace("_",""), self.on_job_hold_activate)
 
291
        self.release = self.rightClickMenu.addAction(i18n("_Release").replace("_",""), self.on_job_release_activate)
 
292
        self.reprint = self.rightClickMenu.addAction(i18n("Re_print").replace("_",""), self.on_job_reprint_activate)
 
293
 
 
294
        cups.setPasswordCB(self.cupsPasswdCallback)
 
295
 
 
296
        dbus.mainloop.qt.DBusQtMainLoop(set_as_default=True)
 
297
 
 
298
        try:
 
299
            bus = dbus.SystemBus()
 
300
        except:
 
301
            print >> sys.stderr, "%s: failed to connect to system D-Bus" % PROGRAM_NAME
 
302
            sys.exit (1)
 
303
 
 
304
        notification = NewPrinterNotification(bus, self)
 
305
 
 
306
        # D-Bus
 
307
        bus.add_signal_receiver (self.handle_dbus_signal,
 
308
                                 path="/com/redhat/PrinterSpooler",
 
309
                                 dbus_interface="com.redhat.PrinterSpooler")
 
310
        self.refresh()
 
311
 
 
312
    """Used in gtk frontend to set magnifing glass icon when configuring printer, I don't have a suitable icon so using bubbles instead
 
313
    # Handle "special" status icon
 
314
    def set_special_statusicon (self, iconname):
 
315
        self.special_status_icon = True
 
316
        self.statusicon.set_from_icon_name (iconname)
 
317
        self.set_statusicon_visibility ()
 
318
 
 
319
    def unset_special_statusicon (self):
 
320
        self.special_status_icon = False
 
321
        self.statusicon.set_from_pixbuf (self.saved_statusicon_pixbuf)
 
322
    """
 
323
 
 
324
    def notify_new_printer (self, printer, title, text):
 
325
        self.hidden = False
 
326
        self.showMessage(title, text)
 
327
 
 
328
    def showMessage(self, title, message):
 
329
        """show a message, delayed slightly to ensure the systray is visible else it appears in the wrong place
 
330
        Gtk uses libnotify, for Qt we just show the message directly"""
 
331
        self.sysTray.show()
 
332
        self.sysTrayTitle = title
 
333
        self.sysTrayMessage = message
 
334
        QTimer.singleShot(1000, self.showSysTrayMessage)
 
335
 
 
336
    def showSysTrayMessage(self):
 
337
        self.sysTray.showMessage(self.sysTrayTitle, self.sysTrayMessage)
 
338
 
 
339
    """unused, see set_special_statusicon
 
340
    def set_statusicon_from_pixbuf (self, pb):
 
341
        self.saved_statusicon_pixbuf = pb
 
342
        if not self.special_status_icon:
 
343
            self.statusicon.set_from_pixbuf (pb)
 
344
    """
 
345
 
 
346
    """unused, see MainWindow and PrintersWindow
 
347
    def on_delete_event(self, *args):
 
348
        if self.trayicon:
 
349
            self.MainWindow.hide ()
 
350
            if self.show_printer_status.get_active ():
 
351
                self.PrintersWindow.hide ()
 
352
        else:
 
353
            self.loop.quit ()
 
354
        return True
 
355
    """
 
356
 
 
357
    def on_printer_status_delete_event(self):
 
358
        self.mainWindow.actionShow_Printer_Status.setChecked(False)
 
359
 
 
360
    def cupsPasswdCallback(self, querystring):
 
361
        (text, ok) = QInputDialog.getText(self.mainWindow, i18n("Password required"), querystring, QLineEdit.Password)
 
362
        if ok:
 
363
            print "ok"
 
364
            return text
 
365
        return ''
 
366
 
 
367
    def show_IPP_Error(self, exception, message):
 
368
        if exception == cups.IPP_NOT_AUTHORIZED:
 
369
            error_text = ('<span weight="bold" size="larger">' +
 
370
                          i18n('Not authorized') + '</span>\n\n' +
 
371
                          i18n('The password may be incorrect.'))
 
372
        else:
 
373
            error_text = ('<span weight="bold" size="larger">' +
 
374
                          i18n('CUPS server error') + '</span>\n\n' +
 
375
                          i18n("There was an error during the CUPS "\
 
376
                            "operation: '%s'.")) % message
 
377
        #fix Gtk's non-HTML for Qt
 
378
        error_text = error_text.replace("\n", "<br />")
 
379
        error_text = error_text.replace("span", "strong")
 
380
        QMessageBox.critical(self.mainWindow, i18n("Error"), error_text)
 
381
 
 
382
    """
 
383
    def toggle_window_display(self, icon):
 
384
    """
 
385
    #FIXME, hide printer status window?
 
386
    def hideMainWindow(self):
 
387
        self.mainWindow.hide()
 
388
 
 
389
    def showMainWindow(self, activationReason):
 
390
        if activationReason == QSystemTrayIcon.Trigger:
 
391
            if self.mainWindow.isVisible():
 
392
                self.mainWindow.hide()
 
393
            else:
 
394
                self.mainWindow.show()
 
395
 
 
396
    def on_show_completed_jobs_activate(self, activated):
 
397
        if activated:
 
398
            self.which_jobs = "all"
 
399
        else:
 
400
            self.which_jobs = "not-completed"
 
401
        self.refresh()
 
402
 
 
403
    def on_show_printer_status_activate(self, activated):
 
404
        if activated:
 
405
            self.printersWindow.show()
 
406
        else:
 
407
            self.printersWindow.hide()
 
408
 
 
409
    def check_still_connecting(self):
 
410
        """Timer callback to check on connecting-to-device reasons."""
 
411
        c = cups.Connection ()
 
412
        printer_reasons = collect_printer_state_reasons (c)
 
413
        del c
 
414
 
 
415
        if self.update_connecting_devices (printer_reasons):
 
416
            self.refresh ()
 
417
 
 
418
        # Don't run this callback again.
 
419
        return False
 
420
 
 
421
    def update_connecting_devices(self, printer_reasons=[]):
 
422
        """Updates connecting_to_device dict and still_connecting set.
 
423
        Returns True if a device has been connecting too long."""
 
424
        time_now = time.time ()
 
425
        connecting_to_device = {}
 
426
        trouble = False
 
427
        for reason in printer_reasons:
 
428
            if reason.get_reason () == "connecting-to-device":
 
429
                # Build a new connecting_to_device dict.  If our existing
 
430
                # dict already has an entry for this printer, use that.
 
431
                printer = reason.get_printer ()
 
432
                t = self.connecting_to_device.get (printer, time_now)
 
433
                connecting_to_device[printer] = t
 
434
                if time_now - t >= CONNECTING_TIMEOUT:
 
435
                    trouble = True
 
436
 
 
437
        # Clear any previously-notified errors that are now fine.
 
438
        remove = set()
 
439
        for printer in self.still_connecting:
 
440
            if not self.connecting_to_device.has_key (printer):
 
441
                remove.add (printer)
 
442
 
 
443
        self.still_connecting = self.still_connecting.difference (remove)
 
444
 
 
445
        self.connecting_to_device = connecting_to_device
 
446
        return trouble
 
447
 
 
448
    def check_state_reasons(self, connection, my_printers=set()):
 
449
        printer_reasons = collect_printer_state_reasons (connection)
 
450
 
 
451
        # Look for any new reasons since we last checked.
 
452
        old_reasons_seen_keys = self.reasons_seen.keys ()
 
453
        reasons_now = set()
 
454
        need_recheck = False
 
455
        for reason in printer_reasons:
 
456
            tuple = reason.get_tuple ()
 
457
            printer = reason.get_printer ()
 
458
            reasons_now.add (tuple)
 
459
            if not self.reasons_seen.has_key (tuple):
 
460
                # New reason.
 
461
                iter = QTreeWidgetItem(self.printersWindow.treeWidget)
 
462
                #iter.setText(0, reason.get_level ())
 
463
                iter.setText(0, reason.get_printer ())
 
464
                title, text = reason.get_description ()
 
465
                iter.setText(1, text)
 
466
                self.printersWindow.treeWidget.addTopLevelItem(iter)
 
467
 
 
468
                self.reasons_seen[tuple] = iter
 
469
                if (reason.get_reason () == "connecting-to-device" and
 
470
                    not self.connecting_to_device.has_key (printer)):
 
471
                    # First time we've seen this.
 
472
                    need_recheck = True
 
473
 
 
474
        if need_recheck:
 
475
            # Check on them again in a minute's time.
 
476
            QTimer.singleShot(CONNECTING_TIMEOUT * 1000, self.check_still_connecting)
 
477
 
 
478
        self.update_connecting_devices (printer_reasons)
 
479
        items = self.reasons_seen.keys ()
 
480
        for tuple in items:
 
481
            if not tuple in reasons_now:
 
482
                # Reason no longer present.
 
483
                iter = self.reasons_seen[tuple]
 
484
                index = self.mainWindow.treeWidget.indexOfTopLevelItem(iter)
 
485
                self.mainWindow.treeWidget.takeTopLevelItem(index)
 
486
                del self.reasons_seen[tuple]
 
487
        # Update statusbar and icon with most severe printer reason
 
488
        # across all printers.
 
489
        self.icon_has_emblem = False
 
490
        reason = worst_printer_state_reason (connection, printer_reasons)
 
491
        if reason != None and reason.get_level () >= StateReason.WARNING:
 
492
            title, text = reason.get_description ()
 
493
            #if self.statusbar_set:
 
494
            #    self.statusbar.pop (0)
 
495
            self.mainWindow.statusBar().showMessage(text)
 
496
            #self.statusbar.push (0, text)
 
497
            self.worst_reason_text = text
 
498
            self.statusbar_set = True
 
499
 
 
500
            if self.trayicon:
 
501
                icon = StateReason.LEVEL_ICON[reason.get_level ()]
 
502
                emblem = QPixmap(KDEDIR + "/share/icons/oxygen/16x16/" + icon+".png")
 
503
                pixbuf = QPixmap(KDEDIR + "/share/icons/oxygen/22x22/devices/printer.png")
 
504
                painter = QPainter(pixbuf)
 
505
                painter.drawPixmap(pixbuf.width()-emblem.width(),pixbuf.height()-emblem.height(),emblem)
 
506
                painter.end()
 
507
                self.sysTray.setIcon(QIcon(pixbuf))
 
508
                self.icon_has_emblem = True
 
509
        else:
 
510
            # No errors
 
511
            if self.statusbar_set:
 
512
                #self.statusbar.pop (0)
 
513
                self.mainWindow.statusBar().clearMessage()
 
514
                self.statusbar_set = False
 
515
 
 
516
    """not using notifications in qt frontend
 
517
    def on_notification_closed(self, notify):
 
518
    """
 
519
 
 
520
    def update_job_creation_times(self):
 
521
        now = time.time ()
 
522
        need_update = False
 
523
        for job, data in self.jobs.iteritems():
 
524
            if self.jobs.has_key (job):
 
525
                iter = self.jobiters[job]
 
526
 
 
527
            t = "Unknown"
 
528
            if data.has_key ('time-at-creation'):
 
529
                created = data['time-at-creation']
 
530
                ago = now - created
 
531
                if ago > 86400:
 
532
                    t = time.ctime (created)
 
533
                elif ago > 3600:
 
534
                    need_update = True
 
535
                    hours = int (ago / 3600)
 
536
                    mins = int ((ago % 3600) / 60)
 
537
                    if hours == 1:
 
538
                        if mins == 0:
 
539
                            t = i18n("1 hour ago")
 
540
                        elif mins == 1:
 
541
                            t = i18n("1 hour and 1 minute ago")
 
542
                        else:
 
543
                            t = i18n("1 hour and %d minutes ago") % mins
 
544
                    else:
 
545
                        if mins == 0:
 
546
                            t = i18n("%d hours ago")
 
547
                        elif mins == 1:
 
548
                            t = i18n("%d hours and 1 minute ago") % hours
 
549
                        else:
 
550
                            t = i18n("%d hours and %d minutes ago") % \
 
551
                                (hours, mins)
 
552
                else:
 
553
                    need_update = True
 
554
                    mins = ago / 60
 
555
                    if mins < 2:
 
556
                        t = i18n("a minute ago")
 
557
                    else:
 
558
                        t = i18n("%d minutes ago") % mins
 
559
 
 
560
            #self.store.set_value (iter, 4, t)
 
561
            iter.setText(4, t)
 
562
 
 
563
        if need_update and not self.will_update_job_creation_times:
 
564
            #gobject.timeout_add (60 * 1000,
 
565
            #                     self.update_job_creation_times)
 
566
            QTimer.singleShot(60 * 1000, self.update_job_creation_times)
 
567
            self.will_update_job_creation_times = True
 
568
 
 
569
        if not need_update:
 
570
            self.will_update_job_creation_times = False
 
571
 
 
572
        # Return code controls whether the timeout will recur.
 
573
        return self.will_update_job_creation_times
 
574
 
 
575
    def refresh(self):
 
576
        """updates the print dialogue"""
 
577
        now = time.time ()
 
578
        if (now - self.last_refreshed) < MIN_REFRESH_INTERVAL:
 
579
            if self.will_refresh:
 
580
                return
 
581
 
 
582
            #gobject.timeout_add (MIN_REFRESH_INTERVAL * 1000,
 
583
            #                     self.refresh)
 
584
            QTimer.singleShot(MIN_REFRESH_INTERVAL * 1000, self.update_job_creation_times)
 
585
            self.will_refresh = True
 
586
            return
 
587
 
 
588
        self.will_refresh = False
 
589
        self.last_refreshed = now
 
590
 
 
591
        try:
 
592
            c = cups.Connection ()
 
593
            jobs = c.getJobs (which_jobs=self.which_jobs, my_jobs=True)
 
594
        except cups.IPPError, (e, m):
 
595
            self.show_IPP_Error (e, m)
 
596
            return
 
597
        except RuntimeError:
 
598
            return
 
599
 
 
600
        if self.which_jobs == "not-completed":
 
601
            num_jobs = len (jobs)
 
602
        else:
 
603
            try:
 
604
                num_jobs = len (c.getJobs (my_jobs=True))
 
605
            except cups.IPPError, (e, m):
 
606
                self.show_IPP_Error (e, m)
 
607
                return
 
608
            except RuntimeError:
 
609
                return
 
610
 
 
611
        if self.trayicon:
 
612
            self.num_jobs = num_jobs
 
613
            if self.hidden and self.num_jobs != self.num_jobs_when_hidden:
 
614
                self.hidden = False
 
615
            if num_jobs == 0:
 
616
                tooltip = i18n("No documents queued")
 
617
                #FIXMEself.set_statusicon_from_pixbuf (self.icon_no_jobs)
 
618
            elif num_jobs == 1:
 
619
                tooltip = i18n("1 document queued")
 
620
                #self.set_statusicon_from_pixbuf (self.icon_jobs)
 
621
            else:
 
622
                tooltip = i18n("%d documents queued") % num_jobs
 
623
                #self.set_statusicon_from_pixbuf (self.icon_jobs)
 
624
 
 
625
        my_printers = set()
 
626
        for job, data in jobs.iteritems ():
 
627
            state = data.get ('job-state', cups.IPP_JOB_CANCELED)
 
628
            if state >= cups.IPP_JOB_CANCELED:
 
629
                continue
 
630
            uri = data.get ('job-printer-uri', '/')
 
631
            i = uri.rfind ('/')
 
632
            my_printers.add (uri[i + 1:])
 
633
 
 
634
        self.check_state_reasons (c, my_printers)
 
635
        del c
 
636
 
 
637
        if self.trayicon:
 
638
            # If there are no jobs but there is a printer
 
639
            # warning/error indicated by the icon, set the icon
 
640
            # tooltip to the reason description.
 
641
            if self.num_jobs == 0 and self.icon_has_emblem:
 
642
                tooltip = self.worst_reason_text
 
643
 
 
644
            self.sysTray.setToolTip (tooltip)
 
645
            self.set_statusicon_visibility ()
 
646
 
 
647
        for job in self.jobs:
 
648
            if not jobs.has_key (job):
 
649
                #self.store.remove (self.jobiters[job])
 
650
                index = self.mainWindow.treeWidget.indexOfTopLevelItem(self.jobiters[job])
 
651
                self.mainWindow.treeWidget.takeTopLevelItem(index)
 
652
                del self.jobiters[job]
 
653
 
 
654
        for job, data in jobs.iteritems():
 
655
            if self.jobs.has_key (job):
 
656
                iter = self.jobiters[job]
 
657
            else:
 
658
                iter = QTreeWidgetItem(self.mainWindow.treeWidget)
 
659
                iter.setText(0, str(job))
 
660
                iter.setText(1, data.get('job-name', 'Unknown'))
 
661
                self.mainWindow.treeWidget.addTopLevelItem(iter)
 
662
                self.jobiters[job] = iter
 
663
 
 
664
            uri = data.get('job-printer-uri', '')
 
665
            i = uri.rfind ('/')
 
666
            if i != -1:
 
667
                printer = uri[i + 1:]
 
668
            iter.setText(2, printer)
 
669
 
 
670
            if data.has_key ('job-k-octets'):
 
671
                size = str (data['job-k-octets']) + 'k'
 
672
            else:
 
673
                size = 'Unknown'
 
674
            iter.setText(3, size)
 
675
            #self.store.set_value (iter, 3, size)
 
676
 
 
677
            state = None
 
678
            if data.has_key ('job-state'):
 
679
                try:
 
680
                    jstate = data['job-state']
 
681
                    s = int (jstate)
 
682
                    state = { cups.IPP_JOB_PENDING:i18n("Pending"),
 
683
                              cups.IPP_JOB_HELD:i18n("Held"),
 
684
                              cups.IPP_JOB_PROCESSING: i18n("Processing"),
 
685
                              cups.IPP_JOB_STOPPED: i18n("Stopped"),
 
686
                              cups.IPP_JOB_CANCELED: i18n("Canceled"),
 
687
                              cups.IPP_JOB_ABORTED: i18n("Aborted"),
 
688
                              cups.IPP_JOB_COMPLETED: i18n("Completed") }[s]
 
689
                except ValueError:
 
690
                    pass
 
691
                except IndexError:
 
692
                    pass    
 
693
            if state == None:
 
694
                state = i18n("Unknown")
 
695
            iter.setText(5, state)
 
696
            columns = self.mainWindow.treeWidget.columnCount()
 
697
            for i in range(columns):
 
698
                self.mainWindow.treeWidget.resizeColumnToContents(i)
 
699
 
 
700
        self.jobs = jobs
 
701
        self.update_job_creation_times ()
 
702
 
 
703
    def set_statusicon_visibility (self):
 
704
        if self.trayicon:
 
705
            if self.suppress_icon_hide:
 
706
                # Avoid hiding the icon if we've been woken up to notify
 
707
                # about a new printer.
 
708
                self.suppress_icon_hide = False
 
709
                return
 
710
 
 
711
            if (not self.hidden) and (self.num_jobs > 0 or self.icon_has_emblem) or self.special_status_icon:
 
712
                self.sysTray.show()
 
713
            else:
 
714
                self.sysTray.hide()
 
715
 
 
716
    def on_treeview_button_press_event(self, postition):
 
717
        # Right-clicked.
 
718
        items = self.mainWindow.treeWidget.selectedItems ()
 
719
        print "items" + str(items)
 
720
        print len(items)
 
721
        if len(items) != 1:
 
722
            return
 
723
        print "selected: " + str(items)
 
724
        iter = items[0]
 
725
        if iter == None:
 
726
            return
 
727
 
 
728
        self.jobid = int(iter.text(0))
 
729
        job = self.jobs[self.jobid]
 
730
        self.cancel.setEnabled (True)
 
731
        self.hold.setEnabled (True)
 
732
        self.release.setEnabled (True)
 
733
        self.reprint.setEnabled (True)
 
734
        if job.has_key ('job-state'):
 
735
            s = job['job-state']
 
736
            print s, "jobstate"
 
737
            if s >= cups.IPP_JOB_CANCELED:
 
738
                self.cancel.setEnabled (False)
 
739
            if s != cups.IPP_JOB_PENDING and s != cups.IPP_JOB_PROCESSING:
 
740
                self.hold.setEnabled (False)
 
741
            if s != cups.IPP_JOB_HELD:
 
742
                self.release.setEnabled (False)
 
743
            if (s != cups.IPP_JOB_CANCELED or
 
744
                not job.get('job-preserved', False)):
 
745
                self.reprint.setEnabled (False)
 
746
        self.rightClickMenu.popup(QCursor.pos())
 
747
 
 
748
    def on_icon_popupmenu(self, icon, button, time):
 
749
        self.icon_popupmenu.popup (None, None, None, button, time)
 
750
 
 
751
    def on_icon_hide_activate(self):
 
752
        self.num_jobs_when_hidden = self.num_jobs
 
753
        self.hidden = True
 
754
        self.set_statusicon_visibility ()
 
755
 
 
756
    def on_icon_quit_activate(self):
 
757
        app.quit()
 
758
 
 
759
    def on_job_cancel_activate(self):
 
760
        try:
 
761
            c = cups.Connection ()
 
762
            c.cancelJob (self.jobid)
 
763
            del c
 
764
        except cups.IPPError, (e, m):
 
765
            self.show_IPP_Error (e, m)
 
766
            return
 
767
        except RuntimeError:
 
768
            return
 
769
 
 
770
    def on_job_hold_activate(self):
 
771
        try:
 
772
            c = cups.Connection ()
 
773
            c.setJobHoldUntil (self.jobid, "indefinite")
 
774
            del c
 
775
        except cups.IPPError, (e, m):
 
776
            self.show_IPP_Error (e, m)
 
777
            return
 
778
        except RuntimeError:
 
779
            return
 
780
 
 
781
    def on_job_release_activate(self):
 
782
        try:
 
783
            c = cups.Connection ()
 
784
            c.setJobHoldUntil (self.jobid, "no-hold")
 
785
            del c
 
786
        except cups.IPPError, (e, m):
 
787
            self.show_IPP_Error (e, m)
 
788
            return
 
789
        except RuntimeError:
 
790
            return
 
791
 
 
792
    def on_job_reprint_activate(self):
 
793
        try:
 
794
            c = cups.Connection ()
 
795
            c.restartJob (self.jobid)
 
796
            del c
 
797
        except cups.IPPError, (e, m):
 
798
            self.show_IPP_Error (e, m)
 
799
            return
 
800
        except RuntimeError:
 
801
            return
 
802
 
 
803
        self.refresh ()
 
804
 
 
805
    def on_refresh_activate(self, menuitem):
 
806
        self.refresh ()
 
807
 
 
808
    def handle_dbus_signal(self, *args):
 
809
        self.refresh ()
 
810
 
 
811
    ## Printer status window
 
812
    """FIXME
 
813
    def set_printer_status_icon (self, column, cell, model, iter, *user_data):
 
814
        level = model.get_value (iter, 0)
 
815
        icon = StateReason.LEVEL_ICON[level]
 
816
        theme = gtk.icon_theme_get_default ()
 
817
        try:
 
818
            pixbuf = theme.load_icon (icon, 22, 0)
 
819
            cell.set_property("pixbuf", pixbuf)
 
820
        except gobject.GError, exc:
 
821
            pass # Couldn't load icon
 
822
    """
 
823
    """FIXME
 
824
    def set_printer_status_name (self, column, cell, model, iter, *user_data):
 
825
        cell.set_property("text", model.get_value (iter, 1))
 
826
    """
 
827
 
 
828
####
 
829
#### NewPrinterNotification DBus server (the 'new' way).  Note: this interface
 
830
#### is not final yet.
 
831
####
 
832
PDS_PATH="/com/redhat/NewPrinterNotification"
 
833
PDS_IFACE="com.redhat.NewPrinterNotification"
 
834
PDS_OBJ="com.redhat.NewPrinterNotification"
 
835
class NewPrinterNotification(dbus.service.Object):
 
836
    """listen for dbus signals"""
 
837
    STATUS_SUCCESS = 0
 
838
    STATUS_MODEL_MISMATCH = 1
 
839
    STATUS_GENERIC_DRIVER = 2
 
840
    STATUS_NO_DRIVER = 3
 
841
 
 
842
    def __init__ (self, bus, jobmanager):
 
843
        self.bus = bus
 
844
        self.getting_ready = 0
 
845
        self.jobmanager = jobmanager
 
846
        bus_name = dbus.service.BusName (PDS_OBJ, bus=bus)
 
847
        dbus.service.Object.__init__ (self, bus_name, PDS_PATH)
 
848
        #self.jobmanager.notify_new_printer ("", i18n("New Printer"), i18n("Configuring New Printer"))
 
849
 
 
850
    """
 
851
    def wake_up (self):
 
852
        global waitloop, runloop, jobmanager
 
853
        do_imports ()
 
854
        if jobmanager == None:
 
855
            waitloop.quit ()
 
856
            runloop = gobject.MainLoop ()
 
857
            jobmanager = JobManager(bus, runloop,
 
858
                                    service_running=service_running,
 
859
                                    trayicon=trayicon, suppress_icon_hide=True)
 
860
    """
 
861
    
 
862
    @dbus.service.method(PDS_IFACE, in_signature='', out_signature='')
 
863
    def GetReady (self):
 
864
        """hal-cups-utils is settings up a new printer"""
 
865
        self.jobmanager.notify_new_printer ("", i18n("New Printer"), i18n("Configuring New Printer"))
 
866
    """
 
867
        self.wake_up ()
 
868
        if self.getting_ready == 0:
 
869
            jobmanager.set_special_statusicon (SEARCHING_ICON)
 
870
 
 
871
        self.getting_ready += 1
 
872
        gobject.timeout_add (60 * 1000, self.timeout_ready)
 
873
 
 
874
    def timeout_ready (self):
 
875
        global jobmanager
 
876
        if self.getting_ready > 0:
 
877
            self.getting_ready -= 1
 
878
        if self.getting_ready == 0:
 
879
            jobmanager.unset_special_statusicon ()
 
880
 
 
881
        return False
 
882
    """
 
883
 
 
884
    # When I plug in my printer HAL calls this with these args:
 
885
    #status: 0
 
886
    #name: PSC_1400_series
 
887
    #mfg: HP
 
888
    #mdl: PSC 1400 series
 
889
    #des:
 
890
    #cmd: LDL,MLC,PML,DYN
 
891
    @dbus.service.method(PDS_IFACE, in_signature='isssss', out_signature='')
 
892
    def NewPrinter (self, status, name, mfg, mdl, des, cmd):
 
893
        """hal-cups-utils has set up a new printer"""
 
894
        """
 
895
        print "status: " + str(status)
 
896
        print "name: " + name
 
897
        print "mfg: " + mfg
 
898
        print "mdl: " + mdl
 
899
        print "des: " + des
 
900
        print "cmd: " + cmd
 
901
        """
 
902
 
 
903
        c = cups.Connection ()
 
904
        try:
 
905
            printer = c.getPrinters ()[name]
 
906
        except KeyError:
 
907
            return
 
908
        del c
 
909
 
 
910
        sys.path.append (APPDIR)
 
911
        from ppds import ppdMakeModelSplit
 
912
        (make, model) = ppdMakeModelSplit (printer['printer-make-and-model'])
 
913
        driver = make + " " + model
 
914
        if status < self.STATUS_GENERIC_DRIVER:
 
915
            title = i18n("Printer added")
 
916
        else:
 
917
            title = i18n("Missing printer driver")
 
918
 
 
919
        if status == self.STATUS_SUCCESS:
 
920
            text = i18n("`%s' is ready for printing.") % name
 
921
        else: # Model mismatch
 
922
            text = i18n("`%s' has been added, using the `%s' driver.") % \
 
923
                   (name, driver)
 
924
 
 
925
        self.jobmanager.notify_new_printer (name, title, text)
 
926
 
 
927
 
 
928
if __name__ == "__main__":
 
929
    """start the application.  TODO, gtk frontend does clever things here to not start the GUI until it has to"""
 
930
    #Oxygen doesn't work with python-dbus (it loads QtDbus which clashes) so force to plastique until that is fixed
 
931
    args = sys.argv + ["-style=plastique"]
 
932
    #KApplication also loads QtDbus which clashes with python-dbus, so stick with QApplication for now
 
933
    app = QApplication(args)
 
934
    app.setWindowIcon(QIcon(KDEDIR + "/share/icons/oxygen/128x128/devices/printer.png"))
 
935
    if app.isSessionRestored():
 
936
         sys.exit(1)
 
937
    applet = JobManager()
 
938
    if "--show" in sys.argv:
 
939
        applet.mainWindow.show()
 
940
        applet.sysTray.show()
 
941
    sys.exit(app.exec_())