diff --git a/src/calibre/gui2/dbus_export/tray.py b/src/calibre/gui2/dbus_export/tray.py index 14ffa15f10..dba64e959d 100644 --- a/src/calibre/gui2/dbus_export/tray.py +++ b/src/calibre/gui2/dbus_export/tray.py @@ -19,112 +19,28 @@ from PyQt5.Qt import ( QApplication, QObject, pyqtSignal, Qt, QPoint, QRect, QMenu, QIcon) from calibre.gui2.dbus_export.menu import DBusMenu -from calibre.gui2.dbus_export.utils import log, qicon_to_sni_image_list, setup_for_cli_run +from calibre.gui2.dbus_export.utils import qicon_to_sni_image_list from calibre.utils.dbus_service import Object, method as dbus_method, BusName, dbus_property, signal as dbus_signal -class Factory(QObject): - - 'See http://www.notmart.org/misc/statusnotifieritem/statusnotifierwatcher.html' - - SERVICE = "org.kde.StatusNotifierWatcher" - PATH = "/StatusNotifierWatcher" - IFACE = "org.kde.StatusNotifierWatcher" - - available_changed = pyqtSignal(bool) - - def __init__(self, parent=None): - QObject.__init__(self, parent) - self.count = 0 - self.items = [] - self.is_available = False - self._bus = None - self.connect_to_snw() - - @property - def bus(self): - if self._bus is None: - self._bus = dbus.SessionBus() - return self._bus - - def bus_disconnected(self, conn): - self._bus = None - - def init_bus(self): - bus = self.bus - bus.call_on_disconnection(self.bus_disconnected) - bus.watch_name_owner(self.SERVICE, self.owner_changed) - self.connect_to_snw() - - def owner_changed(self, new_owner): - old = self.is_available - if new_owner: - self.connect_to_snw() - else: - self.is_available = False - if old != self.is_available: - self.available_changed.emit(self.is_available) - - def connect_to_snw(self): - self.is_available = False - try: - self.bus.add_signal_receiver(self.host_registered, 'StatusNotifierHostRegistered', self.IFACE, self.SERVICE, self.PATH) - except dbus.DBusException as err: - log('Failed to connect to StatusNotifierHostRegistered, with error:', str(err)) - - self.update_availability() - if self.is_available: - for item in self.items: - self.register(item) - - def update_availability(self): - try: - self.is_available = bool(self.bus.call_blocking( - self.SERVICE, self.PATH, dbus.PROPERTIES_IFACE, 'Get', 'ss', (self.IFACE, 'IsStatusNotifierHostRegistered'), timeout=0.1)) - except dbus.DBusException as err: - self.is_available = False - log('Failed to get StatusNotifier host availability with error:', str(err)) - - def host_registered(self, *args): - if not self.is_available: - self.is_available = True - self.available_changed.emit(self.is_available) - - def create_indicator(self, **kw): - if not self.is_available: - raise RuntimeError('StatusNotifier services are not available on this system') - self.count += 1 - kw['bus'] = self.bus - item = StatusNotifierItem(self.count, **kw) - self.items.append(item) - item.destroyed.connect(self.items.remove) - self.register(item) - return item - - def register(self, item): - self.bus.call_blocking( - self.SERVICE, self.PATH, self.IFACE, 'RegisterStatusNotifierItem', 's', (item.dbus.name,), timeout=0.1) +_sni_count = 0 class StatusNotifierItem(QObject): IFACE = 'org.kde.StatusNotifierItem' - NewTitle = pyqtSignal() - NewIcon = pyqtSignal() - NewAttentionIcon = pyqtSignal() - NewOverlayIcon = pyqtSignal() - NewToolTip = pyqtSignal() - NewStatus = pyqtSignal(str) activated = pyqtSignal() show_menu = pyqtSignal(int, int) - def __init__(self, num, **kw): + def __init__(self, **kw): + global _sni_count QObject.__init__(self, parent=kw.get('parent')) self.context_menu = None self.is_visible = True self.tool_tip = '' self._icon = QIcon() self.show_menu.connect(self._show_menu, type=Qt.QueuedConnection) - kw['num'] = num - self.dbus = StatusNotifierItemAPI(self, **kw) + _sni_count += 1 + kw['num'] = _sni_count + self.dbus_api = StatusNotifierItemAPI(self, **kw) def _show_menu(self, x, y): m = self.contextMenu() @@ -137,7 +53,7 @@ class StatusNotifierItem(QObject): def setVisible(self, visible): if self.is_visible != visible: self.is_visible = visible - self.NewStatus.emit(self.dbus.Status) + self.dbus_api.NewStatus(self.dbus_api.Status) def show(self): self.setVisible(True) @@ -150,7 +66,7 @@ class StatusNotifierItem(QObject): def setContextMenu(self, menu): self.context_menu = menu - self.dbus.publish_new_menu() + self.dbus_api.publish_new_menu() def geometry(self): return QRect() @@ -160,11 +76,11 @@ class StatusNotifierItem(QObject): def setToolTip(self, val): self.tool_tip = val or '' - self.NewToolTip.emit() + self.dbus_api.NewToolTip() def setIcon(self, icon): self._icon = icon - self.NewIcon.emit() + self.dbus_api.NewIcon() def icon(self): return self._icon @@ -182,15 +98,12 @@ class StatusNotifierItemAPI(Object): bus = kw['bus'] = dbus.SessionBus() self.name = '%s-%s-%s' % (self.IFACE, os.getpid(), kw.get('num', 1)) self.dbus_name = BusName(self.name, bus=bus, do_not_queue=True) - self.app_id = kw.get('app_id', QApplication.instance().applicationName()) or 'unknown_application' - self.category = kw.get('category', 'ApplicationStatus') - self.title = kw.get('title', self.app_id) + self.app_id = kw.get('app_id') or QApplication.instance().applicationName() or 'unknown_application' + self.category = kw.get('category') or 'ApplicationStatus' + self.title = kw.get('title') or self.app_id self.icon_serialization = qicon_to_sni_image_list(notifier.icon()) Object.__init__(self, bus, '/' + self.IFACE.split('.')[-1]) self.dbus_menu = DBusMenu('/StatusItemMenu', **kw) - for name, val in vars(self.__class__).iteritems(): - if getattr(val, '_dbus_is_signal', False): - getattr(notifier, name).connect(getattr(self, name)) def publish_new_menu(self): menu = self.notifier.contextMenu() @@ -305,24 +218,3 @@ class StatusNotifierItemAPI(Object): pass -_factory = None -def factory(): - global _factory - if _factory is None: - _factory = Factory() - return _factory - -def test(): - setup_for_cli_run() - app = QApplication([]) - app.setApplicationName('Testing SNI Interface') - tray_icon = factory().create_indicator() - tray_icon.setToolTip('A test tooltip') - tray_icon.setIcon(QIcon(I('debug.png'))) - m = QMenu() - m.addAction('Quit this application', app.quit) - tray_icon.setContextMenu(m) - app.exec_() - -if __name__ == '__main__': - test() diff --git a/src/calibre/gui2/dbus_export/widgets.py b/src/calibre/gui2/dbus_export/widgets.py index eb718beb2d..3ebefad0f9 100644 --- a/src/calibre/gui2/dbus_export/widgets.py +++ b/src/calibre/gui2/dbus_export/widgets.py @@ -8,9 +8,12 @@ __copyright__ = '2014, Kovid Goyal ' import time, sys -from PyQt5.Qt import QObject, QMenuBar, QAction, QEvent +from PyQt5.Qt import QObject, QMenuBar, QAction, QEvent, QSystemTrayIcon + +from calibre.constants import iswindows, isosx UNITY_WINDOW_REGISTRAR = ('com.canonical.AppMenu.Registrar', '/com/canonical/AppMenu/Registrar', 'com.canonical.AppMenu.Registrar') +STATUS_NOTIFIER = ("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher", "org.kde.StatusNotifierWatcher") def log(*args, **kw): kw['file'] = sys.stderr @@ -81,13 +84,18 @@ class Factory(QObject): def __init__(self): QObject.__init__(self) - try: - import dbus - self.dbus = dbus - except ImportError: + if iswindows or isosx: self.dbus = None + else: + try: + import dbus + self.dbus = dbus + except ImportError as err: + log('Failed to import dbus, with error:', str(err)) + self.dbus = None self.menu_registrar = None + self.status_notifier = None self._bus = None @property @@ -119,11 +127,41 @@ class Factory(QObject): if self.bus.name_has_owner(UNITY_WINDOW_REGISTRAR[0]): self.menu_registrar = UNITY_WINDOW_REGISTRAR + @property + def has_status_notifier(self): + if self.status_notifier is None: + if self.dbus is None: + self.status_notifier = False + else: + try: + self.detect_status_notifier() + except Exception as err: + self.status_notifier = False + log('Failed to detect window status notifier, with error:', str(err)) + return bool(self.status_notifier) + + def detect_status_notifier(self): + 'See http://www.notmart.org/misc/statusnotifieritem/statusnotifierwatcher.html' + self.status_notifier = False + if self.bus.name_has_owner(STATUS_NOTIFIER[0]): + args = STATUS_NOTIFIER + ('Get', 'ss', (STATUS_NOTIFIER[-1], 'IsStatusNotifierHostRegistered')) + self.status_notifier = bool(self.bus.call_blocking(*args, timeout=0.1)) + def create_window_menubar(self, parent): if self.has_global_menu: return ExportedMenuBar(parent, self.menu_registrar, self.bus) return QMenuBar(parent) + def create_system_tray_icon(self, parent=None, title=None, app_id=None, category=None): + if self.has_status_notifier: + from calibre.gui2.dbus_export.tray import StatusNotifierItem + ans = StatusNotifierItem(parent=parent, title=title, app_id=app_id, category=category) + self.bus.call_blocking( + self.SERVICE, self.PATH, self.IFACE, 'RegisterStatusNotifierItem', 's', (ans.dbus_api.name,), timeout=1) + return ans + if iswindows or isosx: + return QSystemTrayIcon(parent) + def bus_disconnected(self): self._bus = None for i in xrange(5):