#!/usr/bin/env python # -*- coding: utf-8 -*- # # (c) Copyright 2003-2015 HP Development Company, L.P. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Authors: Don Welch, Torsten Marek # Std Lib import sys import struct import select import os import signal import os.path import time # Local from base.g import * from base import device, utils from .ui_utils import load_pixmap # Qt try: from qt import * except ImportError: log.error("Python bindings for Qt3 not found. Exiting!") sys.exit(1) # C types try: import ctypes as c import ctypes.util as cu except ImportError: log.error("Qt3 version of hp-systray requires python-ctypes module. Exiting!") sys.exit(1) # dbus try: import dbus from dbus import SessionBus, lowlevel except ImportError: log.error("Python bindings for dbus not found. Exiting!") sys.exit(1) # pynotify (optional) have_pynotify = True try: import pynotify except ImportError: have_pynotify = False TrayIcon_Warning = 0 TrayIcon_Critical = 1 TrayIcon_Information = 2 theBalloonTip = None UPGRADE_CHECK_DELAY=24*60*60*1000 #1 day CLEAN_EXEC_DELAY=4*60*60*1000 #4 Hrs class BalloonTip(QDialog): def __init__(self, msg_icon, title, msg, tray_icon): QDialog.__init__(self, tray_icon, "BalloonTip", False, Qt.WStyle_StaysOnTop | Qt.WStyle_Customize | Qt.WStyle_NoBorder | Qt.WStyle_Tool | Qt.WX11BypassWM) self.timerId = None self.bubbleActive = False QObject.connect(tray_icon, SIGNAL("destroyed()"), self.close) self.titleLabel = QLabel(self) self.titleLabel.installEventFilter(self) self.titleLabel.setText(title) f = self.titleLabel.font() f.setBold(True) self.titleLabel.setFont(f) self.titleLabel.setTextFormat(Qt.PlainText) # to maintain compat with windows self.closeButton = QPushButton(self) self.closeButton.setPixmap(load_pixmap('close', '16x16')) self.closeButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.closeButton.setFixedSize(18, 18) QObject.connect(self.closeButton, SIGNAL("clicked()"), self.close) self.msgLabel = QLabel(self) self.msgLabel.installEventFilter(self) self.msgLabel.setText(msg) self.msgLabel.setTextFormat(Qt.PlainText) self.msgLabel.setAlignment(Qt.AlignTop | Qt.AlignLeft) layout = QGridLayout(self) if msg_icon is not None: self.iconLabel = QLabel(self) self.iconLabel.setPixmap(msg_icon) self.iconLabel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.iconLabel.setMargin(2) layout.addWidget(self.iconLabel, 0, 0) layout.addWidget(self.titleLabel, 0, 1) else: layout.addMultiCellWidget(self.titleLabel, 0, 1, 0, 2) layout.addWidget(self.closeButton, 0, 3) layout.addMultiCellWidget(self.msgLabel, 1, 1, 0, 3) layout.setMargin(3) self.setPaletteBackgroundColor(QColor(255, 255, 224)) def resizeEvent(self, e): QWidget.resizeEvent(self, e) def mousePressEvent(self, e): self.close() if e.button() == Qt.LeftButton: pass # TODO def timerEvent(self, e): if e.timerId() == self.timerId: self.killTimer(self.timerId) self.hide() self.close() return QWidget.timerEvent(self, e) def closeEvent(self, event): self.bubbleActive = False event.accept() def balloon(self, pos, msecs, showArrow): if self.bubbleActive: return self.bubbleActive = True scr = QApplication.desktop().screenGeometry(pos) sh = self.sizeHint() ao = 18 if pos.y() + ao > scr.bottom(): self.move(pos.x()-sh.width(), pos.y()-sh.height()-ao) else: self.move(pos.x()-sh.width(), pos.y()+ao) if msecs > 0: self.timerId = self.startTimer(msecs) self.show() def showBalloon(msg_icon, title, msg, tray_icon, pos, timeout, showArrow=True): global theBalloonTip hideBalloon() theBalloonTip = BalloonTip(msg_icon, msg, title, tray_icon) if timeout < 0: timeout = 5000 theBalloonTip.balloon(pos, timeout, showArrow) def hideBalloon(): global theBalloonTip if theBalloonTip is None: return theBalloonTip.hide() del theBalloonTip theBalloonTip = None class SystrayIcon(QLabel): """ On construction, you have to supply a QPixmap instance holding the application icon. The pixmap should not be bigger than 32x32, preferably 22x22. Currently, no check is made. The class can emits two signals: Leftclick on icon: activated() Rightclick on icon: contextMenuRequested(const QPoint&) Based on code: (C) 2004 Torsten Marek License: Public domain """ def __init__(self, icon, parent=None, name=""): QLabel.__init__(self, parent, name, Qt.WMouseNoMask | Qt.WRepaintNoErase | Qt.WType_TopLevel | Qt.WStyle_Customize | Qt.WStyle_NoBorder | Qt.WStyle_StaysOnTop) self.setMinimumSize(22, 22) self.setBackgroundMode(Qt.X11ParentRelative) self.setBackgroundOrigin(QWidget.WindowOrigin) self.libX11 = c.cdll.LoadLibrary(cu.find_library('X11')) # get all functions, set arguments + return types self.XternAtom = self.libX11.XInternAtom self.XternAtom.argtypes = [c.c_void_p, c.c_char_p, c.c_int] XSelectInput = self.libX11.XSelectInput XSelectInput.argtypes = [c.c_void_p, c.c_int, c.c_long] XUngrabServer = self.libX11.XUngrabServer XUngrabServer.argtypes = [c.c_void_p] XFlush = self.libX11.XFlush XFlush.argtypes = [c.c_void_p] class data(c.Union): _fields_ = [("b", c.c_char * 20), ("s", c.c_short * 10), ("l", c.c_long * 5)] class XClientMessageEvent(c.Structure): _fields_ = [("type", c.c_int), ("serial", c.c_ulong), ("send_event", c.c_int), ("display", c.c_void_p), ("window", c.c_int), ("message_type", c.c_int), ("format", c.c_int), ("data", data)] XSendEvent = self.libX11.XSendEvent XSendEvent.argtypes = [c.c_void_p, c.c_int, c.c_int, c.c_long, c.c_void_p] XSync = self.libX11.XSync XSync.argtypes = [c.c_void_p, c.c_int] XChangeProperty = self.libX11.XChangeProperty XChangeProperty.argtypes = [c.c_void_p, c.c_long, c.c_int, c.c_int, c.c_int, c.c_int, c.c_char_p, c.c_int] dpy = int(qt_xdisplay()) trayWin = self.winId() x = 0 while True: managerWin = self.locateTray(dpy) if managerWin: break x += 1 if x > 30: break time.sleep(2.0) # Make sure KDE puts the icon in the system tray class data2(c.Union): _fields_ = [("i", c.c_int, 32), ("s", c.c_char * 4)] k = data2() k.i = 1 pk = c.cast(c.pointer(k), c.c_char_p) r = self.XternAtom(dpy, "KWM_DOCKWINDOW", 0) XChangeProperty(dpy, trayWin, r, r, 32, 0, pk, 1) r = self.XternAtom(dpy, "_KDE_NET_WM_SYSTEM_TRAY_WINDOW_FOR", 0) XChangeProperty(dpy, trayWin, r, 33, 32, 0, pk, 1) if managerWin != 0: # set StructureNotifyMask (1L << 17) XSelectInput(dpy, managerWin, 1 << 17) #XUngrabServer(dpy) XFlush(dpy) if managerWin != 0: # send "SYSTEM_TRAY_OPCODE_REQUEST_DOCK to managerWin k = data() k.l = (0, # CurrentTime 0, # REQUEST_DOCK trayWin, # window ID 0, # empty 0) # empty ev = XClientMessageEvent(33, #type: ClientMessage 0, # serial 0, # send_event dpy, # display managerWin, # systray manager self.XternAtom(dpy, "_NET_SYSTEM_TRAY_OPCODE", 0), # message type 32, # format k) # message data XSendEvent(dpy, managerWin, 0, 0, c.addressof(ev)) XSync(dpy, 0) self.setPixmap(icon) self.setAlignment(Qt.AlignHCenter) if parent: QToolTip.add(self, parent.caption()) def locateTray(self, dpy): # get systray window (holds _NET_SYSTEM_TRAY_S atom) self.XScreenNumberOfScreen = self.libX11.XScreenNumberOfScreen self.XScreenNumberOfScreen.argtypes = [c.c_void_p] XDefaultScreenOfDisplay = self.libX11.XDefaultScreenOfDisplay XDefaultScreenOfDisplay.argtypes = [c.c_void_p] XDefaultScreenOfDisplay.restype = c.c_void_p XGetSelectionOwner = self.libX11.XGetSelectionOwner XGetSelectionOwner.argtypes = [c.c_void_p, c.c_int] XGrabServer = self.libX11.XGrabServer XGrabServer.argtypes = [c.c_void_p] iscreen = self.XScreenNumberOfScreen(XDefaultScreenOfDisplay(dpy)) selectionAtom = self.XternAtom(dpy, "_NET_SYSTEM_TRAY_S%i" % iscreen, 0) #XGrabServer(dpy) managerWin = XGetSelectionOwner(dpy, selectionAtom) return managerWin def setTooltipText(self, text): QToolTip.add(self, text) def mousePressEvent(self, e): if e.button() == Qt.RightButton: self.emit(PYSIGNAL("contextMenuRequested(const QPoint&)"), (e.globalPos(),)) elif e.button() == Qt.LeftButton: self.emit(PYSIGNAL("activated()"), ()) def supportsMessages(self): return True def showMessage(self, title, msg, icon, msecs): if have_pynotify and pynotify.init("hplip"): n = pynotify.Notification(title, msg, icon) n.set_timeout(msecs) s.show() else: g = self.mapToGlobal(QPoint(0, 0)) showBalloon(icon, msg, title, self, QPoint(g.x() + self.width()/2, g.y() + self.height()/2), msecs) class TitleItem(QCustomMenuItem): def __init__(self, icon, text): QCustomMenuItem.__init__(self) self.font = QFont() self.font.setBold(True) self.pen = QPen(Qt.black) self.bg_color = qApp.palette().color(QPalette.Active, QColorGroup.Background) self.icon = icon self.text = text def paint(self, painter, cg, act, enabled, x, y, w, h): painter.setPen(self.pen) painter.setFont(self.font) painter.setBackgroundColor(self.bg_color) painter.eraseRect(x, y, w, h) painter.drawPixmap(2, 2, self.icon, 0, 0, -1, -1) painter.drawText(x, y, w, h, Qt.AlignLeft | Qt.AlignVCenter | Qt.ShowPrefix | Qt.DontClip, self.text) def sizeHint(self): return QFontMetrics(self.font).size(Qt.AlignLeft | Qt.AlignVCenter | Qt.ShowPrefix | Qt.DontClip, self.text) class SystemTrayApp(QApplication): def __init__(self, args, read_pipe): QApplication.__init__(self, args) self.read_pipe = read_pipe self.fmt = "80s80sI32sI80sf" self.fmt_size = struct.calcsize(self.fmt) self.user_settings = utils.UserSettings() self.user_settings.load() self.user_settings.debug() self.tray_icon = SystrayIcon(load_pixmap("hp_logo", "32x32", (22, 22))) self.menu = QPopupMenu() title_item = TitleItem(load_pixmap('hp_logo', '16x16', (16, 16)), "HP Status Service") i = self.menu.insertItem(title_item) self.menu.setItemEnabled(i, False) self.menu.insertSeparator() self.menu.insertItem(self.tr("HP Device Manager..."), self.toolbox_triggered) # TODO: #icon2 = QIconSet(load_pixmap('settings', '16x16')) #self.menu.insertItem(icon2, self.tr("Options..."), self.preferences_triggered) self.menu.insertSeparator() icon3 = QIconSet(load_pixmap('quit', '16x16')) self.menu.insertItem(icon3, self.tr("Quit"), self.quit_triggered) self.tray_icon.show() notifier = QSocketNotifier(self.read_pipe, QSocketNotifier.Read) QObject.connect(notifier, SIGNAL("activated(int)"), self.notifier_activated) QObject.connect(self.tray_icon, PYSIGNAL("contextMenuRequested(const QPoint&)"), self.menu_requested) self.icon_info = load_pixmap('info', '16x16') self.icon_warn = load_pixmap('warning', '16x16') self.icon_error = load_pixmap('error', '16x16') if "--ignore-update-firsttime" not in args: self.handle_hplip_updation() self.timer = QTimer() self.timer.connect(self.timer,SIGNAL("timeout()"),self.handle_hplip_updation) self.timer.start(UPGRADE_CHECK_DELAY) # Cleans the /var/log/hp/tmp directory #self.handle_hplip_clean() #self.clean_timer = QTimer() #self.clean_timer.connect(self.clean_timer,SIGNAL("timeout()"),self.handle_hplip_clean) #self.clean_timer.start(CLEAN_EXEC_DELAY) self.ERROR_STATE_TO_ICON = { ERROR_STATE_CLEAR: self.icon_info, ERROR_STATE_OK: self.icon_info, ERROR_STATE_WARNING: self.icon_warn, ERROR_STATE_ERROR: self.icon_error, ERROR_STATE_LOW_SUPPLIES: self.icon_warn, ERROR_STATE_BUSY: self.icon_warn, ERROR_STATE_LOW_PAPER: self.icon_warn, ERROR_STATE_PRINTING: self.icon_info, ERROR_STATE_SCANNING: self.icon_info, ERROR_STATE_PHOTOCARD: self.icon_info, ERROR_STATE_FAXING: self.icon_info, ERROR_STATE_COPYING: self.icon_info, } def menu_requested(self, pos): self.menu.popup(pos) def quit_triggered(self): device.Event('', '', EVENT_SYSTEMTRAY_EXIT).send_via_dbus(SessionBus()) self.quit() def toolbox_triggered(self): try: os.waitpid(-1, os.WNOHANG) except OSError: pass # See if it is already running... ok, lock_file = utils.lock_app('hp-toolbox', True) if ok: # able to lock, not running... utils.unlock(lock_file) path = utils.which('hp-toolbox') if path: path = os.path.join(path, 'hp-toolbox') else: log.error("Unable to find hp-toolbox on PATH.") self.tray_icon.showMessage("HPLIP Status Service", self.__tr("Unable to locate hp-toolbox on system PATH."), self.icon_error, 5000) return log.debug(path) os.spawnlp(os.P_NOWAIT, path, 'hp-toolbox') else: # ...already running, raise it device.Event('', '', EVENT_RAISE_DEVICE_MANAGER).send_via_dbus(SessionBus(), 'com.hplip.Toolbox') def preferences_triggered(self): #print "\nPARENT: prefs!" pass def notifier_activated(self, s): m = '' while True: ready = select.select([self.read_pipe], [], [], 1.0) if ready[0]: m = ''.join([m, os.read(self.read_pipe, self.fmt_size)]) if len(m) == self.fmt_size: event = device.Event(*struct.unpack(self.fmt, m)) if event.event_code > EVENT_MAX_USER_EVENT: continue desc = device.queryString(event.event_code) #print "BUBBLE:", event.device_uri, event.event_code, event.username error_state = STATUS_TO_ERROR_STATE_MAP.get(event.event_code, ERROR_STATE_CLEAR) icon = self.ERROR_STATE_TO_ICON.get(error_state, self.icon_info) if self.tray_icon.supportsMessages(): if event.job_id and event.title: self.tray_icon.showMessage("HPLIP Device Status", QString("%1\n%2\n%3\n(%4/%5/%6)").\ arg(event.device_uri).arg(event.event_code).\ arg(desc).arg(event.username).arg(event.job_id).arg(event.title), icon, 5000) else: self.tray_icon.showMessage("HPLIP Device Status", QString("%1\n%2\n%3").arg(event.device_uri).\ arg(event.event_code).arg(desc), icon, 5000) else: break def handle_hplip_clean(self): log.debug("handle_hplip_clean ") home_dir = sys_conf.get('dirs', 'home') cmd = 'sh %s/hplip_clean.sh'%home_dir os.system(cmd) def handle_hplip_updation(self): log.debug("handle_hplip_updation upgrade_notify =%d"%(self.user_settings.upgrade_notify)) path = utils.which('hp-upgrade') if self.user_settings.upgrade_notify is False: log.debug("upgrade notification is disabled in systray ") if path: path = os.path.join(path, 'hp-upgrade') log.debug("Running hp-upgrade: %s " % (path)) # this just updates the available version in conf file. But won't notify os.spawnlp(os.P_NOWAIT, path, 'hp-upgrade', '--check') time.sleep(5) try: os.waitpid(0, os.WNOHANG) except OSError: pass return current_time = time.time() if int(current_time) > self.user_settings.upgrade_pending_update_time: path = utils.which('hp-upgrade') if path: path = os.path.join(path, 'hp-upgrade') log.debug("Running hp-upgrade: %s " % (path)) os.spawnlp(os.P_NOWAIT, path, 'hp-upgrade', '--notify') time.sleep(5) else: log.error("Unable to find hp-upgrade --notify on PATH.") else: log.debug("upgrade schedule time is not yet completed. schedule time =%d current time =%d " %(self.user_settings.upgrade_pending_update_time, current_time)) try: os.waitpid(0, os.WNOHANG) except OSError: pass def __tr(self,s,c = None): return qApp.translate("SystemTrayApp",s,c) def run(read_pipe): log.set_module("hp-systray(qt3)") app = SystemTrayApp(sys.argv, read_pipe) notifier = QSocketNotifier(read_pipe, QSocketNotifier.Read) QObject.connect(notifier, SIGNAL("activated(int)"), app.notifier_activated) try: app.exec_loop() except KeyboardInterrupt: log.debug("Ctrl-C: Exiting...")