mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
A demo application using Unity's global menu bar
This commit is contained in:
parent
d3e7c92e70
commit
2f00102174
43
src/calibre/gui2/dbus_export/demo.py
Normal file
43
src/calibre/gui2/dbus_export/demo.py
Normal 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_()
|
@ -11,8 +11,7 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import dbus
|
import dbus
|
||||||
from PyQt5.Qt import (
|
from PyQt5.Qt import (
|
||||||
QApplication, QMenu, QIcon, QKeySequence, QObject, QAction, QMenuBar,
|
QApplication, QMenu, QIcon, QKeySequence, QObject, QEvent, QTimer)
|
||||||
QEvent, QTimer)
|
|
||||||
|
|
||||||
from calibre.utils.dbus_service import Object, BusName, method as dbus_method, dbus_property, signal as dbus_signal
|
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 (
|
from calibre.gui2.dbus_export.utils import (
|
||||||
@ -23,14 +22,6 @@ null = object()
|
|||||||
def PropDict(mapping=()):
|
def PropDict(mapping=()):
|
||||||
return dbus.Dictionary(mapping, signature='sv')
|
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):
|
def create_properties_for_action(ac, previous=None):
|
||||||
ans = PropDict()
|
ans = PropDict()
|
||||||
if ac.isSeparator():
|
if ac.isSeparator():
|
||||||
@ -53,9 +44,13 @@ def create_properties_for_action(ac, previous=None):
|
|||||||
ans['toggle-state'] = int(ac.isChecked())
|
ans['toggle-state'] = int(ac.isChecked())
|
||||||
shortcuts = ac.shortcuts()
|
shortcuts = ac.shortcuts()
|
||||||
if shortcuts:
|
if shortcuts:
|
||||||
ans['shortcut'] = sc = []
|
sc = dbus.Array(signature='as')
|
||||||
for s in shortcuts:
|
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():
|
if ac.isIconVisibleInMenu():
|
||||||
icon = ac.icon()
|
icon = ac.icon()
|
||||||
if previous and previous.get('x-qt-icon-cache-key') == icon.cacheKey():
|
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):
|
def publish_new_menu(self, qmenu=None):
|
||||||
self.init_maps(qmenu)
|
self.init_maps(qmenu)
|
||||||
if qmenu is not None:
|
if qmenu is not None:
|
||||||
qmenu.destroyed.connect(self.publish_new_menu)
|
qmenu.destroyed.connect(lambda obj=None:self.publish_new_menu())
|
||||||
ac = MenuBarAction(qmenu) if isinstance(qmenu, QMenuBar) else qmenu.menuAction()
|
ac = qmenu.menuAction()
|
||||||
if isinstance(qmenu, QMenuBar):
|
|
||||||
qmenu.menuAction = lambda : ac
|
|
||||||
self.add_action(ac)
|
self.add_action(ac)
|
||||||
|
self.dbus_api.LayoutUpdated(self.dbus_api.revision, 0)
|
||||||
|
|
||||||
def add_action(self, ac):
|
def add_action(self, ac):
|
||||||
ac_id = 0 if ac.menu() is self.qmenu else self.next_id
|
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)))
|
ans.append((action_id, self.action_properties(action_id, property_names)))
|
||||||
return ans
|
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):
|
class DBusMenuAPI(Object):
|
||||||
|
|
||||||
IFACE = 'com.canonical.dbusmenu'
|
IFACE = 'com.canonical.dbusmenu'
|
||||||
@ -266,18 +266,19 @@ class DBusMenuAPI(Object):
|
|||||||
|
|
||||||
@dbus_method(IFACE, in_signature='is', out_signature='v')
|
@dbus_method(IFACE, in_signature='is', out_signature='v')
|
||||||
def GetProperty(self, id, name):
|
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='')
|
@dbus_method(IFACE, in_signature='isvu', out_signature='')
|
||||||
def Event(self, id, eventId, data, timestamp):
|
def Event(self, id, eventId, data, timestamp):
|
||||||
''' This is called by the applet to notify the application an event happened on a
|
''' 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"
|
* "clicked"
|
||||||
* "hovered"
|
* "hovered"
|
||||||
* "opened"
|
* "opened"
|
||||||
* "closed"
|
* "closed"
|
||||||
Vendor specific events can be added by prefixing them with "x-<vendor>-"'''
|
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')
|
@dbus_method(IFACE, in_signature='a(isvu)', out_signature='ai')
|
||||||
def EventGroup(self, events):
|
def EventGroup(self, events):
|
||||||
@ -285,7 +286,13 @@ class DBusMenuAPI(Object):
|
|||||||
several different menuitems. This is done to optimize DBus traffic.
|
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
|
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.'''
|
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')
|
@dbus_method(IFACE, in_signature='i', out_signature='b')
|
||||||
def AboutToShow(self, id):
|
def AboutToShow(self, id):
|
||||||
@ -313,7 +320,8 @@ def test():
|
|||||||
bus = dbus.SessionBus()
|
bus = dbus.SessionBus()
|
||||||
dbus_name = BusName('com.calibre-ebook.TestDBusMenu', bus=bus, do_not_queue=True)
|
dbus_name = BusName('com.calibre-ebook.TestDBusMenu', bus=bus, do_not_queue=True)
|
||||||
m = QMenu()
|
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 = DBusMenu('/Menu', bus=bus)
|
||||||
menu.publish_new_menu(m)
|
menu.publish_new_menu(m)
|
||||||
app.exec_()
|
app.exec_()
|
||||||
|
140
src/calibre/gui2/dbus_export/widgets.py
Normal file
140
src/calibre/gui2/dbus_export/widgets.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user