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
|
||||
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_()
|
||||
|
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