diff --git a/src/calibre/gui2/dbus_export/demo.py b/src/calibre/gui2/dbus_export/demo.py index 32b16fed94..a2c8bd1e58 100644 --- a/src/calibre/gui2/dbus_export/demo.py +++ b/src/calibre/gui2/dbus_export/demo.py @@ -8,7 +8,7 @@ __copyright__ = '2014, Kovid Goyal ' from PyQt5.Qt import ( QApplication, QMainWindow, QVBoxLayout, Qt, QKeySequence, QAction, - QActionGroup, QMenu, QIcon) + QActionGroup, QMenu, QPushButton, QWidget) from calibre.gui2.dbus_export.utils import setup_for_cli_run from calibre.gui2.dbus_export.widgets import factory @@ -26,19 +26,20 @@ class MainWindow(QMainWindow): self.setMinimumWidth(400) self.setWindowTitle('Demo of DBUS menu exporter and systray integration') self.statusBar().showMessage(self.windowTitle()) - w = self.centralWidget() - self.l = QVBoxLayout(w) + w = QWidget(self) + self.setCentralWidget(w) + self.l = l = QVBoxLayout(w) mb = f.create_window_menubar(self) self.setMenuBar(mb) m = self.menu_one = mb.addMenu('&One') m.aboutToShow.connect(self.about_to_show_one) s = self.style() self.q = q = QAction('&Quit', self) - q.setShortcut(QKeySequence.Quit) + q.setShortcut(QKeySequence.Quit), q.setIcon(s.standardIcon(s.SP_DialogCancelButton)) q.triggered.connect(QApplication.quit) self.addAction(q) - QApplication.instance().setWindowIcon(QIcon(I('debug.png'))) - for i, icon in zip(xrange(3), map(s.standardIcon, (s.SP_DialogOkButton, s.SP_DialogCancelButton, s.SP_ArrowUp))): + QApplication.instance().setWindowIcon(s.standardIcon(s.SP_ComputerIcon)) + for i, icon in zip(xrange(3), map(s.standardIcon, (s.SP_DialogOkButton, s.SP_DialogHelpButton, s.SP_ArrowUp))): ac = m.addAction('One - &%d' % (i + 1)) ac.setShortcut(QKeySequence(Qt.CTRL | (Qt.Key_1 + i), Qt.SHIFT | (Qt.Key_1 + i))) ac.setIcon(icon) @@ -71,8 +72,20 @@ class MainWindow(QMainWindow): m.addAction(q) self.systray.setContextMenu(m) self.update_tray_toggle_action() + self.cib = b = QPushButton('Change system tray icon') + l.addWidget(b), b.clicked.connect(self.change_icon) + self.hib = b = QPushButton('Show/Hide system tray icon') + l.addWidget(b), b.clicked.connect(self.systray.toggle) print ('DBUS connection unique name:', f.bus.get_unique_name()) + def change_icon(self): + import random + s = self.style() + num = s.SP_ComputerIcon + while num == s.SP_ComputerIcon: + num = random.choice(range(20)) + self.systray.setIcon(self.style().standardIcon(num)) + def update_tray_toggle_action(self): if hasattr(self, 'sm'): self.sm.actions()[0].setText('Hide main window' if self.isVisible() else 'Show main window') @@ -91,8 +104,8 @@ class MainWindow(QMainWindow): self.setVisible(not self.isVisible()) def action_triggered(self, checked=False): - ac = self.sender() - text = 'Action triggered: %s' % ac.text() + ac=self.sender() + text='Action triggered: %s' % ac.text() self.statusBar().showMessage(text) def about_to_show(self): @@ -105,8 +118,8 @@ class MainWindow(QMainWindow): def about_to_show_two(self): self.menu_two.addAction('Action added by about to show') -app = QApplication([]) +app=QApplication([]) app.setApplicationName('com.calibre-ebook.DBusExportDemo') -mw = MainWindow() +mw=MainWindow() mw.show() app.exec_() diff --git a/src/calibre/gui2/dbus_export/tray.py b/src/calibre/gui2/dbus_export/tray.py index 954d1615b6..16f356e293 100644 --- a/src/calibre/gui2/dbus_export/tray.py +++ b/src/calibre/gui2/dbus_export/tray.py @@ -19,8 +19,9 @@ from PyQt5.Qt import ( QApplication, QObject, pyqtSignal, Qt, QPoint, QRect, QMenu) from calibre.gui2.dbus_export.menu import DBusMenu -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 +from calibre.gui2.dbus_export.utils import icon_cache +from calibre.utils.dbus_service import ( + Object, method as dbus_method, BusName, dbus_property, signal as dbus_signal) _sni_count = 0 @@ -61,6 +62,9 @@ class StatusNotifierItem(QObject): def hide(self): self.setVisible(False) + def toggle(self): + self.setVisible(not self.isVisible()) + def contextMenu(self): return self.context_menu @@ -104,7 +108,6 @@ class StatusNotifierItemAPI(Object): 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]) _status_item_menu_count += 1 self.dbus_menu = DBusMenu('/StatusItemMenu/%d' % _status_item_menu_count, bus=bus, parent=kw.get('parent')) @@ -122,15 +125,15 @@ class StatusNotifierItemAPI(Object): @dbus_property(IFACE, signature='s') def IconName(self): - return '' + return icon_cache().name_for_icon(self.notifier.icon()) @dbus_property(IFACE, signature='s') def IconThemePath(self): - return '' + return icon_cache().icon_theme_path @dbus_property(IFACE, signature='a(iiay)') def IconPixmap(self): - return self.icon_serialization + return dbus.Array(signature='(iiay)') @dbus_property(IFACE, signature='s') def OverlayIconName(self): @@ -203,7 +206,7 @@ class StatusNotifierItemAPI(Object): @dbus_signal(IFACE, '') def NewIcon(self): - self.icon_serialization = qicon_to_sni_image_list(self.notifier.icon()) + pass @dbus_signal(IFACE, '') def NewAttentionIcon(self): diff --git a/src/calibre/gui2/dbus_export/utils.py b/src/calibre/gui2/dbus_export/utils.py index 6b1ab1c731..4e193130f4 100644 --- a/src/calibre/gui2/dbus_export/utils.py +++ b/src/calibre/gui2/dbus_export/utils.py @@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' -import sys, array, socket, re +import sys, array, re, os, errno import dbus @@ -17,9 +17,57 @@ def log(*args, **kw): print('DBusExport:', *args, **kw) kw['file'].flush() +from calibre.ptempfile import PersistentTemporaryDirectory + +class IconCache(object): + + # Avoiding sending status notifier icon data over DBus, makes dbus-monitor + # easier to read. Also Canonical's StatusNotifier implementation cannot + # handle icon data over DBus, so we have to do this anyway. + + def __init__(self): + self.icon_theme_path = os.path.join(PersistentTemporaryDirectory(prefix='dbus-export-icons-'), 'icons') + self.theme_dir = os.path.join(self.icon_theme_path, 'hicolor') + os.makedirs(self.theme_dir) + self.cached = set() + + def name_for_icon(self, qicon): + if qicon.isNull(): + return '' + key = qicon.cacheKey() + ans = 'dbus-icon-cache-%d' % key + if key not in self.cached: + self.write_icon(qicon, ans) + self.cached.add(key) + return ans + + def write_icon(self, qicon, name): + sizes = qicon.availableSizes() or [QSize(x, x) for x in (16, 32, 64, 128, 256)] + for size in sizes: + sdir = os.path.join(self.theme_dir, '%dx%d' % (size.width(), size.height()), 'apps') + try: + os.makedirs(sdir) + except EnvironmentError as err: + if err.errno != errno.EEXIST: + raise + fname = os.path.join(sdir, '%s.png' % name) + qicon.pixmap(size).save(fname, 'PNG') + # Touch the theme path: GTK icon loading system checks the mtime of the + # dir to decide whether it should look for new icons in the theme dir. + os.utime(self.icon_theme_path, None) + +_icon_cache = None + +def icon_cache(): + global _icon_cache + if _icon_cache is None: + _icon_cache = IconCache() + return _icon_cache + def qicon_to_sni_image_list(qicon): 'See http://www.notmart.org/misc/statusnotifieritem/icons.html' + import socket ans = dbus.Array(signature='(iiay)') if not qicon.isNull(): sizes = qicon.availableSizes() or (QSize(x, x) for x in (32, 64, 128, 256))