A demo application using Unity's global menu bar

This commit is contained in:
Kovid Goyal 2014-10-29 07:42:03 +05:30
parent d3e7c92e70
commit 2f00102174
3 changed files with 212 additions and 21 deletions

View File

@ -0,0 +1,43 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt5.Qt import (
QApplication, QMainWindow, QVBoxLayout, Qt, QKeySequence)
from calibre.gui2.dbus_export.utils import setup_for_cli_run
from calibre.gui2.dbus_export.widgets import factory
setup_for_cli_run()
class MainWindow(QMainWindow):
def action_triggered(self, checked=False):
self.statusBar().showMessage('Action triggered: %s' % self.sender().text())
app = QApplication([])
f = factory()
mw = MainWindow()
mw.setWindowTitle('Demo of DBUS menu exporter and systray integration')
mw.statusBar().showMessage(mw.windowTitle())
w = mw.centralWidget()
mw.l = l = QVBoxLayout(w)
mb = f.create_window_menubar(mw)
mw.setMenuBar(mb)
m = mb.addMenu('&One')
s = mw.style()
for i, icon in zip(xrange(3), map(s.standardIcon, (s.SP_DialogOkButton, s.SP_DialogCancelButton, s.SP_ArrowUp))):
ac = m.addAction('One - &%d' % (i + 1))
ac.triggered.connect(mw.action_triggered)
k = getattr(Qt, 'Key_%d' % (i + 1))
ac.setShortcut(QKeySequence(Qt.CTRL | (Qt.Key_1 + i), Qt.SHIFT | (Qt.Key_1 + i)))
ac.setIcon(icon)
m.addSeparator()
m.addAction('&Disabled action').setEnabled(False)
mw.show()
print ('DBUS connection unique name:', f.bus.get_unique_name())
app.exec_()

View File

@ -11,8 +11,7 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import dbus
from PyQt5.Qt import (
QApplication, QMenu, QIcon, QKeySequence, QObject, QAction, QMenuBar,
QEvent, QTimer)
QApplication, QMenu, QIcon, QKeySequence, QObject, QEvent, QTimer)
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 (
@ -23,14 +22,6 @@ null = object()
def PropDict(mapping=()):
return dbus.Dictionary(mapping, signature='sv')
class MenuBarAction(QAction):
def __init__(self, mb):
QAction.__init__(self, mb)
def menu(self):
return self.parent()
def create_properties_for_action(ac, previous=None):
ans = PropDict()
if ac.isSeparator():
@ -53,9 +44,13 @@ def create_properties_for_action(ac, previous=None):
ans['toggle-state'] = int(ac.isChecked())
shortcuts = ac.shortcuts()
if shortcuts:
ans['shortcut'] = sc = []
sc = dbus.Array(signature='as')
for s in shortcuts:
sc.extend(key_sequence_to_dbus_shortcut(s))
if not s.isEmpty():
for x in key_sequence_to_dbus_shortcut(s):
sc.append(dbus.Array(x, signature='s'))
if sc:
ans['shortcut'] = sc[:1] # Unity fails to display the shortcuts at all if more than one is specified
if ac.isIconVisibleInMenu():
icon = ac.icon()
if previous and previous.get('x-qt-icon-cache-key') == icon.cacheKey():
@ -115,11 +110,10 @@ class DBusMenu(QObject):
def publish_new_menu(self, qmenu=None):
self.init_maps(qmenu)
if qmenu is not None:
qmenu.destroyed.connect(self.publish_new_menu)
ac = MenuBarAction(qmenu) if isinstance(qmenu, QMenuBar) else qmenu.menuAction()
if isinstance(qmenu, QMenuBar):
qmenu.menuAction = lambda : ac
qmenu.destroyed.connect(lambda obj=None:self.publish_new_menu())
ac = qmenu.menuAction()
self.add_action(ac)
self.dbus_api.LayoutUpdated(self.dbus_api.revision, 0)
def add_action(self, ac):
ac_id = 0 if ac.menu() is self.qmenu else self.next_id
@ -222,6 +216,12 @@ class DBusMenu(QObject):
ans.append((action_id, self.action_properties(action_id, property_names)))
return ans
def handle_event(self, action_id, event, data, timestamp):
ac = self.id_to_action(action_id)
if event == 'clicked':
# TODO: Handle checkable actions
ac.triggered.emit()
class DBusMenuAPI(Object):
IFACE = 'com.canonical.dbusmenu'
@ -266,18 +266,19 @@ class DBusMenuAPI(Object):
@dbus_method(IFACE, in_signature='is', out_signature='v')
def GetProperty(self, id, name):
return self.menu.action_properties(id).get(name)
return self.menu.action_properties(id).get(name, '')
@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::
menu item. eventId can be one of the following::
* "clicked"
* "hovered"
* "opened"
* "closed"
Vendor specific events can be added by prefixing them with "x-<vendor>-"'''
pass
if self.menu.id_to_action(id) is not None:
self.menu.handle_event(id, eventId, data, timestamp)
@dbus_method(IFACE, in_signature='a(isvu)', out_signature='ai')
def EventGroup(self, events):
@ -285,7 +286,13 @@ class DBusMenuAPI(Object):
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')
missing = dbus.Array(signature='u')
for id, eventId, data, timestamp in events:
if self.menu.id_to_action(id) is not None:
self.menu.handle_event(id, eventId, data, timestamp)
else:
missing.append(id)
return missing
@dbus_method(IFACE, in_signature='i', out_signature='b')
def AboutToShow(self, id):
@ -313,7 +320,8 @@ def test():
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))
ac = m.addAction(QIcon(I('window-close.png')), 'Quit', app.quit)
ac.setShortcut(QKeySequence('Ctrl+Q'))
menu = DBusMenu('/Menu', bus=bus)
menu.publish_new_menu(m)
app.exec_()

View File

@ -0,0 +1,140 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import time
from PyQt5.Qt import QObject, QMenuBar, QAction, QEvent
UNITY_WINDOW_REGISTRAR = ('com.canonical.AppMenu.Registrar', '/com/canonical/AppMenu/Registrar', 'com.canonical.AppMenu.Registrar')
class MenuBarAction(QAction):
def __init__(self, mb):
QAction.__init__(self, mb)
def menu(self):
return self.parent()
menu_counter = 0
class ExportedMenuBar(QMenuBar):
def __init__(self, parent, menu_registrar, bus):
global menu_counter
if not parent.isWindow():
raise ValueError('You must supply a top level window widget as the parent for an exported menu bar')
QMenuBar.__init__(self, parent)
QMenuBar.setVisible(self, False)
self.menu_action = MenuBarAction(self)
self.menu_registrar = menu_registrar
self.registered_window_id = None
self.bus = bus
menu_counter += 1
import dbus
from calibre.gui2.dbus_export.menu import DBusMenu
self.object_path = dbus.ObjectPath('/MenuBar/%d' % menu_counter)
self.dbus_menu = DBusMenu(self.object_path)
self.dbus_menu.publish_new_menu(self)
self.register()
parent.installEventFilter(self)
def register(self):
wid = self.parent().effectiveWinId()
if wid is not None:
self.registered_window_id = int(wid)
args = self.menu_registrar + ('RegisterWindow', 'uo', (self.registered_window_id, self.object_path))
self.bus.call_blocking(*args)
def unregister(self):
if self.registered_window_id is not None:
args = self.menu_registrar + ('UnregisterWindow', 'u', (self.registered_window_id,))
self.registered_window_id = None
self.bus.call_blocking(*args)
def setVisible(self, visible):
pass # no-op
def isVisible(self):
return True
def menuAction(self):
return self.menu_action
def eventFilter(self, obj, ev):
etype = ev.type()
if etype == QEvent.WinIdChange:
self.unregister()
self.register()
return False
class Factory(QObject):
def __init__(self):
QObject.__init__(self)
try:
import dbus
self.dbus = dbus
except ImportError:
self.dbus = None
self.menu_registrar = None
self._bus = None
@property
def bus(self):
if self._bus is None:
try:
self._bus = self.dbus.SessionBus()
self._bus.call_on_disconnection(self.bus_disconnected)
except Exception as err:
print ('Failed to connect to DBUS session bus, with error:', str(err))
self._bus = False
return self._bus or None
@property
def has_global_menu(self):
if self.menu_registrar is None:
if self.dbus is None:
self.menu_registrar = False
else:
try:
self.detect_menu_registrar()
except Exception as err:
self.menu_registrar = False
print ('Failed to detect window menu registrar, with error:', str(err))
return bool(self.menu_registrar)
def detect_menu_registrar(self):
self.menu_registrar = False
if self.bus.name_has_owner(UNITY_WINDOW_REGISTRAR[0]):
self.menu_registrar = UNITY_WINDOW_REGISTRAR
def create_window_menubar(self, parent):
if self.has_global_menu:
return ExportedMenuBar(parent, self.menu_registrar, self.bus)
return QMenuBar(parent)
def bus_disconnected(self):
self._bus = None
for i in xrange(5):
try:
self.bus
except Exception:
time.sleep(1)
continue
break
else:
self.bus
# TODO: have the created widgets also handle bus disconnection
_factory = None
def factory():
global _factory
if _factory is None:
_factory = Factory()
return _factory