Tizen 2.1 base
[platform/upstream/hplip.git] / ui / systemtray.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # (c) Copyright 2003-2008 Hewlett-Packard Development Company, L.P.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
19 #
20 # Authors: Don Welch, Torsten Marek
21
22 # Std Lib
23 import sys
24 import struct
25 import select
26 import os
27 import signal
28 import os.path
29 import time
30
31 # Local
32 from base.g import *
33 from base import device, utils
34 from ui_utils import load_pixmap
35
36 # Qt
37 try:
38     from qt import *
39 except ImportError:
40     log.error("Python bindings for Qt3 not found. Exiting!")
41     sys.exit(1)
42
43 # C types
44 try:
45     import ctypes as c
46     import ctypes.util as cu
47 except ImportError:
48     log.error("Qt3 version of hp-systray requires python-ctypes module. Exiting!")
49     sys.exit(1)
50
51 # dbus
52 try:
53     import dbus
54     from dbus import SessionBus, lowlevel
55 except ImportError:
56     log.error("Python bindings for dbus not found. Exiting!")
57     sys.exit(1)
58
59
60 # pynotify (optional)
61 have_pynotify = True
62 try:
63     import pynotify
64 except ImportError:
65     have_pynotify = False
66
67
68 TrayIcon_Warning = 0
69 TrayIcon_Critical = 1
70 TrayIcon_Information = 2
71
72 theBalloonTip = None
73 UPGRADE_CHECK_DELAY=24*60*60*1000               #1 day
74
75
76 class BalloonTip(QDialog):
77     def __init__(self, msg_icon, title, msg, tray_icon):
78         QDialog.__init__(self, tray_icon, "BalloonTip", False,
79         Qt.WStyle_StaysOnTop | Qt.WStyle_Customize | Qt.WStyle_NoBorder | Qt.WStyle_Tool | Qt.WX11BypassWM)
80
81         self.timerId = None
82         self.bubbleActive = False
83
84         QObject.connect(tray_icon, SIGNAL("destroyed()"), self.close)
85
86         self.titleLabel = QLabel(self)
87         self.titleLabel.installEventFilter(self)
88         self.titleLabel.setText(title)
89         f = self.titleLabel.font()
90         f.setBold(True)
91         self.titleLabel.setFont(f)
92         self.titleLabel.setTextFormat(Qt.PlainText) # to maintain compat with windows
93
94         self.closeButton = QPushButton(self)
95         self.closeButton.setPixmap(load_pixmap('close', '16x16'))
96         self.closeButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
97         self.closeButton.setFixedSize(18, 18)
98         QObject.connect(self.closeButton, SIGNAL("clicked()"), self.close)
99
100         self.msgLabel = QLabel(self)
101         self.msgLabel.installEventFilter(self)
102         self.msgLabel.setText(msg)
103         self.msgLabel.setTextFormat(Qt.PlainText)
104         self.msgLabel.setAlignment(Qt.AlignTop | Qt.AlignLeft)
105
106         layout = QGridLayout(self)
107         if msg_icon is not None:
108             self.iconLabel = QLabel(self)
109             self.iconLabel.setPixmap(msg_icon)
110             self.iconLabel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
111             self.iconLabel.setMargin(2)
112             layout.addWidget(self.iconLabel, 0, 0)
113             layout.addWidget(self.titleLabel, 0, 1)
114         else:
115             layout.addMultiCellWidget(self.titleLabel, 0, 1, 0, 2)
116
117         layout.addWidget(self.closeButton, 0, 3)
118         layout.addMultiCellWidget(self.msgLabel, 1, 1, 0, 3)
119         layout.setMargin(3)
120         self.setPaletteBackgroundColor(QColor(255, 255, 224))
121
122
123     def resizeEvent(self, e):
124         QWidget.resizeEvent(self, e)
125
126
127     def mousePressEvent(self, e):
128         self.close()
129         if e.button() == Qt.LeftButton:
130             pass # TODO
131
132
133     def timerEvent(self, e):
134         if e.timerId() == self.timerId:
135             self.killTimer(self.timerId)
136             self.hide()
137             self.close()
138
139             return
140
141         QWidget.timerEvent(self, e)
142
143
144     def closeEvent(self, event):
145         self.bubbleActive = False
146         event.accept()
147
148
149     def balloon(self, pos, msecs, showArrow):
150         if self.bubbleActive:
151             return
152
153         self.bubbleActive = True
154
155         scr = QApplication.desktop().screenGeometry(pos)
156         sh = self.sizeHint()
157         ao = 18
158         if pos.y() + ao > scr.bottom():
159             self.move(pos.x()-sh.width(), pos.y()-sh.height()-ao)
160         else:
161             self.move(pos.x()-sh.width(), pos.y()+ao)
162
163         if msecs > 0:
164             self.timerId = self.startTimer(msecs)
165
166         self.show()
167
168
169 def showBalloon(msg_icon, title, msg, tray_icon, pos, timeout, showArrow=True):
170     global theBalloonTip
171     hideBalloon()
172
173     theBalloonTip = BalloonTip(msg_icon, msg, title, tray_icon)
174
175     if timeout < 0:
176         timeout = 5000
177
178     theBalloonTip.balloon(pos, timeout, showArrow)
179
180
181 def hideBalloon():
182     global theBalloonTip
183     if theBalloonTip is None:
184         return
185
186     theBalloonTip.hide()
187     del theBalloonTip
188     theBalloonTip = None
189
190
191
192 class SystrayIcon(QLabel):
193     """ On construction, you have to supply a QPixmap instance holding the
194         application icon.  The pixmap should not be bigger than 32x32,
195         preferably 22x22. Currently, no check is made.
196
197         The class can emits two signals:
198             Leftclick on icon: activated()
199             Rightclick on icon: contextMenuRequested(const QPoint&)
200
201         Based on code: (C) 2004 Torsten Marek
202         License: Public domain
203     """
204
205     def __init__(self, icon, parent=None, name=""):
206         QLabel.__init__(self, parent, name, Qt.WMouseNoMask | Qt.WRepaintNoErase |
207                            Qt.WType_TopLevel | Qt.WStyle_Customize |
208                            Qt.WStyle_NoBorder | Qt.WStyle_StaysOnTop)
209
210         self.setMinimumSize(22, 22)
211         self.setBackgroundMode(Qt.X11ParentRelative)
212         self.setBackgroundOrigin(QWidget.WindowOrigin)
213
214         self.libX11 = c.cdll.LoadLibrary(cu.find_library('X11'))
215
216         # get all functions, set arguments + return types
217         self.XternAtom = self.libX11.XInternAtom
218         self.XternAtom.argtypes = [c.c_void_p, c.c_char_p, c.c_int]
219
220         XSelectInput = self.libX11.XSelectInput
221         XSelectInput.argtypes = [c.c_void_p, c.c_int, c.c_long]
222
223         XUngrabServer = self.libX11.XUngrabServer
224         XUngrabServer.argtypes = [c.c_void_p]
225
226         XFlush = self.libX11.XFlush
227         XFlush.argtypes = [c.c_void_p]
228
229         class data(c.Union):
230             _fields_ = [("b", c.c_char * 20),
231                         ("s", c.c_short * 10),
232                         ("l", c.c_long * 5)]
233
234         class XClientMessageEvent(c.Structure):
235             _fields_ = [("type", c.c_int),
236                         ("serial", c.c_ulong),
237                         ("send_event", c.c_int),
238                         ("display", c.c_void_p),
239                         ("window", c.c_int),
240                         ("message_type", c.c_int),
241                         ("format", c.c_int),
242                         ("data", data)]
243
244         XSendEvent = self.libX11.XSendEvent
245         XSendEvent.argtypes = [c.c_void_p, c.c_int, c.c_int, c.c_long, c.c_void_p]
246
247         XSync = self.libX11.XSync
248         XSync.argtypes = [c.c_void_p, c.c_int]
249
250         XChangeProperty = self.libX11.XChangeProperty
251         XChangeProperty.argtypes = [c.c_void_p, c.c_long, c.c_int, c.c_int,
252                                     c.c_int, c.c_int, c.c_char_p, c.c_int]
253
254         dpy = int(qt_xdisplay())
255         trayWin  = self.winId()
256
257         x = 0
258         while True:
259             managerWin = self.locateTray(dpy)
260             if managerWin: break
261             x += 1
262             if x > 30: break
263             time.sleep(2.0)
264
265
266         # Make sure KDE puts the icon in the system tray
267         class data2(c.Union):
268             _fields_ = [("i", c.c_int, 32),
269                         ("s", c.c_char * 4)]
270
271         k = data2()
272         k.i = 1
273         pk = c.cast(c.pointer(k), c.c_char_p)
274
275         r = self.XternAtom(dpy, "KWM_DOCKWINDOW", 0)
276         XChangeProperty(dpy, trayWin, r, r, 32, 0, pk, 1)
277
278         r = self.XternAtom(dpy, "_KDE_NET_WM_SYSTEM_TRAY_WINDOW_FOR", 0)
279         XChangeProperty(dpy, trayWin, r, 33, 32, 0, pk, 1)
280
281         if managerWin != 0:
282             # set StructureNotifyMask (1L << 17)
283             XSelectInput(dpy, managerWin, 1L << 17)
284
285         #XUngrabServer(dpy)
286         XFlush(dpy)
287
288         if managerWin != 0:
289             # send "SYSTEM_TRAY_OPCODE_REQUEST_DOCK to managerWin
290             k = data()
291             k.l = (0, # CurrentTime
292                    0, # REQUEST_DOCK
293                    trayWin, # window ID
294                    0, # empty
295                    0) # empty
296             ev = XClientMessageEvent(33, #type: ClientMessage
297                                      0, # serial
298                                      0, # send_event
299                                      dpy, # display
300                                      managerWin, # systray manager
301                                      self.XternAtom(dpy, "_NET_SYSTEM_TRAY_OPCODE", 0), # message type
302                                      32, # format
303                                      k) # message data
304             XSendEvent(dpy, managerWin, 0, 0, c.addressof(ev))
305             XSync(dpy, 0)
306
307         self.setPixmap(icon)
308         self.setAlignment(Qt.AlignHCenter)
309
310         if parent:
311             QToolTip.add(self, parent.caption())
312
313
314
315     def locateTray(self, dpy):
316         # get systray window (holds _NET_SYSTEM_TRAY_S<screen> atom)
317         self.XScreenNumberOfScreen = self.libX11.XScreenNumberOfScreen
318         self.XScreenNumberOfScreen.argtypes = [c.c_void_p]
319
320         XDefaultScreenOfDisplay = self.libX11.XDefaultScreenOfDisplay
321         XDefaultScreenOfDisplay.argtypes = [c.c_void_p]
322         XDefaultScreenOfDisplay.restype = c.c_void_p
323
324         XGetSelectionOwner = self.libX11.XGetSelectionOwner
325         XGetSelectionOwner.argtypes = [c.c_void_p, c.c_int]
326
327         XGrabServer = self.libX11.XGrabServer
328         XGrabServer.argtypes = [c.c_void_p]
329
330         iscreen = self.XScreenNumberOfScreen(XDefaultScreenOfDisplay(dpy))
331         selectionAtom = self.XternAtom(dpy, "_NET_SYSTEM_TRAY_S%i" % iscreen, 0)
332         #XGrabServer(dpy)
333
334         managerWin = XGetSelectionOwner(dpy, selectionAtom)
335         return managerWin
336
337
338     def setTooltipText(self, text):
339         QToolTip.add(self, text)
340
341
342     def mousePressEvent(self, e):
343         if e.button() == Qt.RightButton:
344             self.emit(PYSIGNAL("contextMenuRequested(const QPoint&)"), (e.globalPos(),))
345
346         elif e.button() == Qt.LeftButton:
347             self.emit(PYSIGNAL("activated()"), ())
348
349
350     def supportsMessages(self):
351         return True
352
353
354     def showMessage(self, title, msg, icon, msecs):
355         if have_pynotify and pynotify.init("hplip"):
356             n = pynotify.Notification(title, msg, icon)
357             n.set_timeout(msecs)
358             s.show()
359         else:
360             g = self.mapToGlobal(QPoint(0, 0))
361             showBalloon(icon, msg, title, self,
362                 QPoint(g.x() + self.width()/2, g.y() + self.height()/2), msecs)
363
364
365
366 class TitleItem(QCustomMenuItem):
367     def __init__(self, icon, text):
368         QCustomMenuItem.__init__(self)
369         self.font = QFont()
370         self.font.setBold(True)
371         self.pen = QPen(Qt.black)
372         self.bg_color = qApp.palette().color(QPalette.Active, QColorGroup.Background)
373         self.icon = icon
374         self.text = text
375
376     def paint(self, painter, cg, act, enabled, x, y, w, h):
377         painter.setPen(self.pen)
378         painter.setFont(self.font)
379         painter.setBackgroundColor(self.bg_color)
380         painter.eraseRect(x, y, w, h)
381         painter.drawPixmap(2, 2, self.icon, 0, 0, -1, -1)
382         painter.drawText(x, y, w, h, Qt.AlignLeft | Qt.AlignVCenter | Qt.ShowPrefix | Qt.DontClip, self.text)
383
384     def sizeHint(self):
385         return QFontMetrics(self.font).size(Qt.AlignLeft | Qt.AlignVCenter | Qt.ShowPrefix | Qt.DontClip, self.text)
386
387
388
389 class SystemTrayApp(QApplication):
390     def __init__(self, args, read_pipe):
391         QApplication.__init__(self, args)
392
393         self.read_pipe = read_pipe
394         self.fmt = "80s80sI32sI80sf"
395         self.fmt_size = struct.calcsize(self.fmt)
396         
397         self.user_settings = utils.UserSettings()
398         self.user_settings.load()
399         self.user_settings.debug()
400
401         self.tray_icon = SystrayIcon(load_pixmap("hp_logo", "32x32", (22, 22)))
402         self.menu = QPopupMenu()
403
404         title_item = TitleItem(load_pixmap('hp_logo', '16x16', (16, 16)), "HP Status Service")
405         i = self.menu.insertItem(title_item)
406         self.menu.setItemEnabled(i, False)
407
408         self.menu.insertSeparator()
409
410         self.menu.insertItem(self.tr("HP Device Manager..."), self.toolbox_triggered)
411
412         # TODO:
413         #icon2 = QIconSet(load_pixmap('settings', '16x16'))
414         #self.menu.insertItem(icon2, self.tr("Options..."), self.preferences_triggered)
415
416         self.menu.insertSeparator()
417
418         icon3 = QIconSet(load_pixmap('quit', '16x16'))
419         self.menu.insertItem(icon3, self.tr("Quit"),  self.quit_triggered)
420
421         self.tray_icon.show()
422
423         notifier = QSocketNotifier(self.read_pipe, QSocketNotifier.Read)
424         QObject.connect(notifier, SIGNAL("activated(int)"), self.notifier_activated)
425
426         QObject.connect(self.tray_icon, PYSIGNAL("contextMenuRequested(const QPoint&)"), self.menu_requested)
427
428         self.icon_info = load_pixmap('info', '16x16')
429         self.icon_warn = load_pixmap('warning', '16x16')
430         self.icon_error = load_pixmap('error', '16x16')
431         
432         self.handle_hplip_updation()
433         self.timer = QTimer()
434         self.timer.connect(self.timer,SIGNAL("timeout()"),self.handle_hplip_updation)
435         self.timer.start(UPGRADE_CHECK_DELAY)
436
437         self.ERROR_STATE_TO_ICON = {
438             ERROR_STATE_CLEAR: self.icon_info,
439             ERROR_STATE_OK: self.icon_info,
440             ERROR_STATE_WARNING: self.icon_warn,
441             ERROR_STATE_ERROR: self.icon_error,
442             ERROR_STATE_LOW_SUPPLIES: self.icon_warn,
443             ERROR_STATE_BUSY: self.icon_warn,
444             ERROR_STATE_LOW_PAPER: self.icon_warn,
445             ERROR_STATE_PRINTING: self.icon_info,
446             ERROR_STATE_SCANNING: self.icon_info,
447             ERROR_STATE_PHOTOCARD: self.icon_info,
448             ERROR_STATE_FAXING: self.icon_info,
449             ERROR_STATE_COPYING: self.icon_info,
450         }
451
452
453     def menu_requested(self, pos):
454         self.menu.popup(pos)
455
456
457     def quit_triggered(self):
458         device.Event('', '', EVENT_SYSTEMTRAY_EXIT).send_via_dbus(SessionBus())
459         self.quit()
460
461
462     def toolbox_triggered(self):
463         try:
464             os.waitpid(-1, os.WNOHANG)
465         except OSError:
466             pass
467
468         # See if it is already running...
469         ok, lock_file = utils.lock_app('hp-toolbox', True)
470
471         if ok: # able to lock, not running...
472             utils.unlock(lock_file)
473
474             path = utils.which('hp-toolbox')
475             if path:
476                 path = os.path.join(path, 'hp-toolbox')
477             else:
478                 log.error("Unable to find hp-toolbox on PATH.")
479
480                 self.tray_icon.showMessage("HPLIP Status Service",
481                                 self.__tr("Unable to locate hp-toolbox on system PATH."),
482                                 self.icon_error, 5000)
483
484                 return
485
486             log.debug(path)
487             os.spawnlp(os.P_NOWAIT, path, 'hp-toolbox')
488
489         else: # ...already running, raise it
490             device.Event('', '', EVENT_RAISE_DEVICE_MANAGER).send_via_dbus(SessionBus(), 'com.hplip.Toolbox')
491
492
493     def preferences_triggered(self):
494         #print "\nPARENT: prefs!"
495         pass
496
497
498     def notifier_activated(self, s):
499         m = ''
500         while True:
501             ready = select.select([self.read_pipe], [], [], 1.0)
502
503             if ready[0]:
504                 m = ''.join([m, os.read(self.read_pipe, self.fmt_size)])
505                 if len(m) == self.fmt_size:
506                     event = device.Event(*struct.unpack(self.fmt, m))
507
508                     if event.event_code > EVENT_MAX_USER_EVENT:
509                         continue
510
511                     desc = device.queryString(event.event_code)
512                     #print "BUBBLE:", event.device_uri, event.event_code, event.username
513                     error_state = STATUS_TO_ERROR_STATE_MAP.get(event.event_code, ERROR_STATE_CLEAR)
514                     icon = self.ERROR_STATE_TO_ICON.get(error_state, self.icon_info)
515
516                     if self.tray_icon.supportsMessages():
517                         if event.job_id and event.title:
518                             self.tray_icon.showMessage("HPLIP Device Status",
519                                 QString("%1\n%2\n%3\n(%4/%5/%6)").\
520                                 arg(event.device_uri).arg(event.event_code).\
521                                 arg(desc).arg(event.username).arg(event.job_id).arg(event.title),
522                                 icon, 5000)
523                         else:
524                             self.tray_icon.showMessage("HPLIP Device Status",
525                                 QString("%1\n%2\n%3").arg(event.device_uri).\
526                                 arg(event.event_code).arg(desc),
527                                 icon, 5000)
528
529             else:
530                 break
531
532     def handle_hplip_updation(self):
533         log.debug("handle_hplip_updation upgrade_notify =%d"%(self.user_settings.upgrade_notify))
534         path = utils.which('hp-upgrade')
535         if self.user_settings.upgrade_notify is False:
536             log.debug("upgrade notification is disabled in systray ")
537             if path:
538                 path = os.path.join(path, 'hp-upgrade')
539                 log.debug("Running hp-upgrade: %s " % (path))
540                 # this just updates the available version in conf file. But won't notify
541                 os.spawnlp(os.P_NOWAIT, path, 'hp-upgrade', '--check')
542             return
543
544
545         current_time = time.time()
546
547         if int(current_time) > self.user_settings.upgrade_pending_update_time:
548             path = utils.which('hp-upgrade')
549             if path:
550                 path = os.path.join(path, 'hp-upgrade')
551                 log.debug("Running hp-upgrade: %s " % (path))
552                 os.spawnlp(os.P_NOWAIT, path, 'hp-upgrade', '--notify')
553
554             else:
555                 log.error("Unable to find hp-upgrade --notify on PATH.")
556         else:
557             log.debug("upgrade schedule time is not yet completed. schedule time =%d current time =%d " %(self.user_settings.upgrade_pending_update_time, current_time))
558
559
560
561     def __tr(self,s,c = None):
562         return qApp.translate("SystemTrayApp",s,c)
563
564
565
566
567 def run(read_pipe):
568     log.set_module("hp-systray(qt3)")
569
570     app = SystemTrayApp(sys.argv, read_pipe)
571
572     notifier = QSocketNotifier(read_pipe, QSocketNotifier.Read)
573     QObject.connect(notifier, SIGNAL("activated(int)"), app.notifier_activated)
574
575     try:
576         app.exec_loop()
577     except KeyboardInterrupt:
578         log.debug("Ctrl-C: Exiting...")