From 67ba10d3bf05711b53bd53766426a172076ab593 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 27 Oct 2014 22:16:57 +0530 Subject: [PATCH] Skeleton of the DBus API for dbusmenu --- src/calibre/gui2/dbus_export/menu.py | 99 +++++++++++++++++++++++++-- src/calibre/gui2/dbus_export/tray.py | 20 +++--- src/calibre/gui2/dbus_export/utils.py | 7 ++ 3 files changed, 113 insertions(+), 13 deletions(-) diff --git a/src/calibre/gui2/dbus_export/menu.py b/src/calibre/gui2/dbus_export/menu.py index 3b5d1acf43..42764c18e8 100644 --- a/src/calibre/gui2/dbus_export/menu.py +++ b/src/calibre/gui2/dbus_export/menu.py @@ -6,22 +6,111 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' -import dbus +# Support for excporting Qt's MenuBars/Menus over DBUS. The API is defined in +# dbus-menu.xml from the libdbusmenu project https://launchpad.net/libdbusmenu -from calibre.utils.dbus_service import Object +import dbus +from PyQt5.Qt import QApplication, QMenu, QIcon, QKeySequence + +from calibre.utils.dbus_service import Object, BusName, method as dbus_method, dbus_property, signal as dbus_signal +from calibre.gui2.dbus_export.utils import setup_for_cli_run class DBusMenu(Object): IFACE = 'com.canonical.dbusmenu' - def __init__(self, notifier, object_path, **kw): - self.notifier = notifier + def __init__(self, object_path, **kw): bus = kw.get('bus') if bus is None: bus = kw['bus'] = dbus.SessionBus() Object.__init__(self, bus, object_path) + self.status = 'normal' - def publish_new_menu(self): + def publish_new_menu(self, qmenu): + self.qmenu = qmenu + + @dbus_property(IFACE, signature='u') + def Version(self): + return 3 # GTK 3 uses 3, KDE 4 uses 2 + + @dbus_property(IFACE, signature='s', emits_changed_signal=True) + def Status(self): + return self.status + + def set_status(self, normal=True): + self.status = 'normal' if normal else 'notice' + self.PropertiesChanged(self.IFACE, {'Status': self.status}, []) + + @dbus_property(IFACE, signature='s') + def TextDirection(self): + return 'ltr' + + @dbus_property(IFACE, signature='as') + def IconThemePath(self): + return dbus.Array(signature='s') + + @dbus_method(IFACE, in_signature='iias', out_signature='u(ia{sv}av)') + def GetLayout(self, parentId, recursionDepth, propertyNames): pass + @dbus_method(IFACE, in_signature='aias', out_signature='a(ia{sv})') + def GetGroupProperties(self, ids, propertyNames): + pass + @dbus_method(IFACE, in_signature='is', out_signature='v') + def GetProperty(self, id, name): + pass + + @dbus_method(IFACE, in_signature='isvu', out_signature='') + def Event(self, id, eventId, data, timestamp): + ''' This is called by the applet to notify the application an event happened on a + menu item. type can be one of the following:: + * "clicked" + * "hovered" + * "opened" + * "closed" + Vendor specific events can be added by prefixing them with "x--"''' + pass + + @dbus_method(IFACE, in_signature='a(isvu)', out_signature='ai') + def EventGroup(self, events): + ''' Used to pass a set of events as a single message for possibily + several different menuitems. This is done to optimize DBus traffic. + Should return a list of ids that are not found. events is a list of + events in the same format as used for the Event method.''' + return dbus.Array(signature='u') + + @dbus_method(IFACE, in_signature='i', out_signature='b') + def AboutToShow(self, id): + pass + + @dbus_method(IFACE, in_signature='ai', out_signature='aiai') + def AboutToShowGroup(self, ids): + pass + + @dbus_signal(IFACE, 'a(ia{sv})a(ias)') + def ItemsPropertiesUpdated(self, updatedProps, removedProps): + pass + + @dbus_signal(IFACE, 'ui') + def LayoutUpdated(self, revision, parent): + pass + + @dbus_signal(IFACE, 'iu') + def ItemActivationRequested(self, id, timestamp): + pass + +def test(): + setup_for_cli_run() + app = QApplication([]) + bus = dbus.SessionBus() + dbus_name = BusName('com.calibre-ebook.TestDBusMenu', bus=bus, do_not_queue=True) + m = QMenu() + m.addAction(QIcon(I('window-close.png')), 'Quit', app.quit).setShortcut(QKeySequence(QKeySequence.Quit)) + menu = DBusMenu('/Menu', bus=bus) + menu.publish_new_menu(m) + app.exec_() + del dbus_name + +if __name__ == '__main__': + test() diff --git a/src/calibre/gui2/dbus_export/tray.py b/src/calibre/gui2/dbus_export/tray.py index f3d9a697f8..14ffa15f10 100644 --- a/src/calibre/gui2/dbus_export/tray.py +++ b/src/calibre/gui2/dbus_export/tray.py @@ -19,7 +19,7 @@ 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 +from calibre.gui2.dbus_export.utils import log, qicon_to_sni_image_list, setup_for_cli_run from calibre.utils.dbus_service import Object, method as dbus_method, BusName, dbus_property, signal as dbus_signal class Factory(QObject): @@ -187,13 +187,21 @@ class StatusNotifierItemAPI(Object): self.title = kw.get('title', 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(notifier, '/StatusItemMenu', **kw) + 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): - self.dbus_menu.publish_new_menu() + menu = self.notifier.contextMenu() + if menu is None: + menu = QMenu() + if len(menu.actions()) == 0: + menu.addAction(self.notifier.icon(), _('Show/hide %s') % self.title, self.notifier.activated.emit) + # The menu must have at least one entry, namely the show/hide entry. + # This is necessary as Canonical in their infinite wisdom decided to + # force all tray icons to show their popup menus when clicked. + self.dbus_menu.publish_new_menu(menu) @dbus_property(IFACE, signature='s') def IconName(self): @@ -305,13 +313,9 @@ def factory(): return _factory def test(): - import signal - from dbus.mainloop.glib import DBusGMainLoop, threads_init - DBusGMainLoop(set_as_default=True) - threads_init() + setup_for_cli_run() app = QApplication([]) app.setApplicationName('Testing SNI Interface') - signal.signal(signal.SIGINT, signal.SIG_DFL) # quit on Ctrl-C tray_icon = factory().create_indicator() tray_icon.setToolTip('A test tooltip') tray_icon.setIcon(QIcon(I('debug.png'))) diff --git a/src/calibre/gui2/dbus_export/utils.py b/src/calibre/gui2/dbus_export/utils.py index dd3a3cd746..82291c32fa 100644 --- a/src/calibre/gui2/dbus_export/utils.py +++ b/src/calibre/gui2/dbus_export/utils.py @@ -38,3 +38,10 @@ def qicon_to_sni_image_list(qicon): ans.append((w, h, dbus.ByteArray(data))) return ans +def setup_for_cli_run(): + import signal + from dbus.mainloop.glib import DBusGMainLoop, threads_init + DBusGMainLoop(set_as_default=True) + threads_init() + signal.signal(signal.SIGINT, signal.SIG_DFL) # quit on Ctrl-C +