Remove DBUS based global menu and system tray code

This was mainly present to support GNOME and GNOME has decided to ditch
system trays and global menus. Since the python-dbus package this code
uses is not maintained, ditch it entirely.
This commit is contained in:
Kovid Goyal 2021-06-24 10:55:12 +05:30
parent 009e82c9df
commit efdf661457
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
12 changed files with 15 additions and 2616 deletions

View File

@ -34,17 +34,13 @@ class TestImports(unittest.TestCase):
full_module_name = full_module_name.rpartition('.')[0]
if full_module_name in exclude_modules or ('.' in full_module_name and full_module_name.rpartition('.')[0] in exclude_packages):
continue
try:
importlib.import_module(full_module_name)
except DeprecationWarning:
if 'dbus_export' not in full_module_name and 'dbus_service' not in full_module_name:
raise
importlib.import_module(full_module_name)
count += 1
return count
def test_import_of_all_python_modules(self):
exclude_modules = {'calibre.gui2.dbus_export.demo', 'calibre.gui2.dbus_export.gtk'}
exclude_packages = {'calibre.devices.mtp.unix.upstream'}
exclude_modules = set()
if not iswindows:
exclude_modules |= {'calibre.utils.iphlpapi', 'calibre.utils.open_with.windows', 'calibre.devices.winusb'}
exclude_packages |= {'calibre.utils.winreg', 'calibre.utils.windows'}
@ -52,11 +48,10 @@ class TestImports(unittest.TestCase):
exclude_modules.add('calibre.utils.open_with.osx')
if not islinux:
exclude_modules |= {
'calibre.utils.dbus_service', 'calibre.linux',
'calibre.linux',
'calibre.utils.linux_trash', 'calibre.utils.open_with.linux',
'calibre.gui2.linux_file_dialogs'
}
exclude_packages.add('calibre.gui2.dbus_export')
self.assertGreater(self.base_check(os.path.join(SRC, 'odf'), exclude_packages, exclude_modules), 10)
base = os.path.join(SRC, 'calibre')
self.assertGreater(self.base_check(base, exclude_packages, exclude_modules), 1000)

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
from functools import partial
from qt.core import (
Qt, QAction, QMenu, QObject, QToolBar, QToolButton, QSize, pyqtSignal, QKeySequence,
Qt, QAction, QMenu, QObject, QToolBar, QToolButton, QSize, pyqtSignal, QKeySequence, QMenuBar,
QTimer, QPropertyAnimation, QEasingCurve, pyqtProperty, QPainter, QWidget, QPalette, sip)
from calibre.constants import ismacos
@ -533,17 +533,15 @@ else:
ac = ia.shortcut_action_for_context_menu
m.addAction(ac)
from calibre.gui2.dbus_export.widgets import factory
class MenuBar(QObject):
is_native_menubar = False
def __init__(self, location_manager, parent):
QObject.__init__(self, parent)
f = factory(app_id='com.calibre-ebook.gui')
self.menu_bar = f.create_window_menubar(parent)
self.is_native_menubar = self.menu_bar.is_native_menubar
self.menu_bar = QMenuBar(parent)
self.menu_bar.is_native_menubar = False
parent.setMenuBar(self.menu_bar)
self.gui = parent
self.location_manager = location_manager

View File

@ -1,9 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'

View File

@ -1,170 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import time
from qt.core import (
QApplication, QMainWindow, QVBoxLayout, Qt, QKeySequence, QAction, QEvent, QStyle,
QActionGroup, QMenu, QPushButton, QWidget, QTimer, QMessageBox, pyqtSignal)
from calibre.gui2.dbus_export.utils import setup_for_cli_run
from calibre.gui2.dbus_export.widgets import factory
from polyglot.builtins import range
setup_for_cli_run()
def make_checkable(ac, checked=True):
ac.setCheckable(True), ac.setChecked(checked)
class MainWindow(QMainWindow):
window_blocked = pyqtSignal()
window_unblocked = pyqtSignal()
def __init__(self):
QMainWindow.__init__(self)
f = factory()
self.setMinimumWidth(400)
self.setWindowTitle('Demo of DBUS menu exporter and systray integration')
self.statusBar().showMessage(self.windowTitle())
w = QWidget(self)
self.setCentralWidget(w)
self.l = l = QVBoxLayout(w)
mb = self.menu_bar = f.create_window_menubar(self)
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.StandardKey.Quit), q.setIcon(s.standardIcon(QStyle.StandardPixmap.SP_DialogCancelButton))
q.triggered.connect(QApplication.quit)
self.addAction(q)
QApplication.instance().setWindowIcon(s.standardIcon(QStyle.StandardPixmap.SP_ComputerIcon))
for i, icon in zip(range(3), map(s.standardIcon, (
QStyle.StandardPixmap.SP_DialogOkButton, QStyle.StandardPixmap.SP_DialogHelpButton, QStyle.StandardPixmap.SP_ArrowUp))):
ac = m.addAction('One - &%d' % (i + 1))
ac.setShortcut(QKeySequence(Qt.Modifier.CTRL | (Qt.Key.Key_1 + i), Qt.Modifier.SHIFT | (Qt.Key.Key_1 + i)))
ac.setIcon(icon)
m.addSeparator()
self.menu_two = m2 = m.addMenu('A &submenu')
for i, icon in zip(range(3), map(s.standardIcon, (
QStyle.StandardPixmap.SP_DialogOkButton, QStyle.StandardPixmap.SP_DialogCancelButton, QStyle.StandardPixmap.SP_ArrowUp))):
ac = m2.addAction('Two - &%d' % (i + 1))
ac.setShortcut(QKeySequence(Qt.Modifier.CTRL | (Qt.Key.Key_A + i)))
ac.setIcon(icon)
m2.aboutToShow.connect(self.about_to_show_two)
m2.addSeparator(), m.addSeparator()
m.addAction('&Disabled action').setEnabled(False)
ac = m.addAction('A checkable action')
make_checkable(ac)
g = QActionGroup(self)
make_checkable(g.addAction(m.addAction('Exclusive 1')))
make_checkable(g.addAction(m.addAction('Exclusive 2')), False)
m.addSeparator()
self.about_to_show_sentinel = m.addAction('This action\'s text should change before menu is shown')
self.as_count = 0
for ac in mb.findChildren(QAction):
ac.triggered.connect(self.action_triggered)
for m in mb.findChildren(QMenu):
m.aboutToShow.connect(self.about_to_show)
self.systray = f.create_system_tray_icon(parent=self, title=self.windowTitle())
if self.systray is not None:
self.systray.activated.connect(self.tray_activated)
self.sm = m = QMenu()
m.addAction('Show/hide main window').triggered.connect(self.tray_activated)
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)
self.update_tooltip_timer = t = QTimer(self)
t.setInterval(1000), t.timeout.connect(self.update_tooltip), t.start()
self.ab = b = QPushButton('Add a new menu')
b.clicked.connect(self.add_menu), l.addWidget(b)
self.rb = b = QPushButton('Remove a created menu')
b.clicked.connect(self.remove_menu), l.addWidget(b)
self.sd = b = QPushButton('Show modal dialog')
b.clicked.connect(self.show_dialog), l.addWidget(b)
print('DBUS connection unique name:', f.bus.get_unique_name())
def update_tooltip(self):
self.systray.setToolTip(time.strftime('A dynamically updated tooltip [%H:%M:%S]'))
def add_menu(self):
mb = self.menu_bar
m = mb.addMenu('Created menu %d' % len(mb.actions()))
for i in range(3):
m.addAction('Some action %d' % i)
for ac in m.findChildren(QAction):
ac.triggered.connect(self.action_triggered)
m.aboutToShow.connect(self.about_to_show)
def remove_menu(self):
mb = self.menu_bar
if len(mb.actions()) > 1:
mb.removeAction(mb.actions()[-1])
def change_icon(self):
import random
num = QStyle.StandardPixmap.SP_ComputerIcon
while num == QStyle.StandardPixmap.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')
def hideEvent(self, ev):
if not ev.spontaneous():
self.update_tray_toggle_action()
return QMainWindow.hideEvent(self, ev)
def showEvent(self, ev):
if not ev.spontaneous():
self.update_tray_toggle_action()
return QMainWindow.showEvent(self, ev)
def tray_activated(self, reason):
self.setVisible(not self.isVisible())
def action_triggered(self, checked=False):
ac=self.sender()
text='Action triggered: %s' % ac.text()
self.statusBar().showMessage(text)
def about_to_show(self):
self.statusBar().showMessage('About to show menu: %s' % self.sender().title())
def about_to_show_one(self):
self.as_count += 1
self.about_to_show_sentinel.setText('About to show handled: %d' % self.as_count)
def about_to_show_two(self):
self.menu_two.addAction('Action added by about to show')
def show_dialog(self):
QMessageBox.information(self, 'A test dialog', 'While this dialog is shown, the global menu should be hidden')
def event(self, ev):
if ev.type() in (QEvent.Type.WindowBlocked, QEvent.Type.WindowUnblocked):
if ev.type() == QEvent.Type.WindowBlocked:
self.window_blocked.emit()
else:
self.window_unblocked.emit()
return QMainWindow.event(self, ev)
app=QApplication([])
app.setAttribute(Qt.ApplicationAttribute.AA_DontUseNativeMenuBar, False)
app.setApplicationName('com.calibre-ebook.DBusExportDemo')
mw=MainWindow()
mw.show()
app.exec_()

View File

@ -1,314 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
# Demo program to explore the GTK DBus interface, which is only partially documented
# at https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI
import sys, dbus, struct, time, signal
from threading import Thread
from pprint import pformat
from gi.repository import Gtk, Gdk, GdkX11 # noqa
from polyglot.builtins import unicode_type, iteritems
UI_INFO = """
<ui>
<menubar name='MenuBar'>
<menu action='FileMenu'>
<menu action='FileNew'>
<menuitem action='FileNewStandard' />
<menuitem action='FileNewFoo' />
<menuitem action='FileNewGoo' />
</menu>
<separator />
<menuitem action='FileQuit' />
</menu>
<menu action='EditMenu'>
<menuitem action='EditCopy' />
<menuitem action='EditPaste' />
<menuitem action='EditSomething' />
</menu>
<menu action='ChoicesMenu'>
<menuitem action='ChoiceOne'/>
<menuitem action='ChoiceTwo'/>
<separator />
<menuitem action='ChoiceThree'/>
<separator />
<menuitem action='DisabledAction'/>
<menuitem action='InvisibleAction'/>
<menuitem action='TooltipAction'/>
<menuitem action='IconAction'/>
</menu>
</menubar>
<toolbar name='ToolBar'>
<toolitem action='FileNewStandard' />
<toolitem action='FileQuit' />
</toolbar>
<popup name='PopupMenu'>
<menuitem action='EditCopy' />
<menuitem action='EditPaste' />
<menuitem action='EditSomething' />
</popup>
</ui>
"""
class MenuExampleWindow(Gtk.ApplicationWindow):
def __init__(self, app):
Gtk.Window.__init__(self, application=app, title="Menu Example")
self.set_default_size(800, 600)
self.scroll = s = Gtk.ScrolledWindow()
s.set_hexpand(True), s.set_vexpand(True)
s.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
s.set_min_content_height(450)
self.label = la = Gtk.TextView()
la.set_text = la.get_buffer().set_text
s.add(la)
action_group = Gtk.ActionGroup("my_actions")
self.add_file_menu_actions(action_group)
self.add_edit_menu_actions(action_group)
self.add_choices_menu_actions(action_group)
uimanager = self.create_ui_manager()
uimanager.insert_action_group(action_group)
menubar = uimanager.get_widget("/MenuBar")
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.pack_start(menubar, False, False, 0)
toolbar = uimanager.get_widget("/ToolBar")
box.pack_start(toolbar, False, False, 0)
eventbox = Gtk.EventBox()
eventbox.connect("button-press-event", self.on_button_press_event)
box.pack_start(s, False, False, 0)
box.pack_start(eventbox, True, True, 0)
label = Gtk.Label("Right-click to see the popup menu.")
eventbox.add(label)
self.popup = uimanager.get_widget("/PopupMenu")
self.add(box)
i = Gtk.Image.new_from_stock(Gtk.STOCK_OK, Gtk.IconSize.MENU)
# Currently the menu items image is not exported over DBus, so probably
# best to stick with using dbusmenu
uimanager.get_widget('/MenuBar/ChoicesMenu/IconAction')
uimanager.get_widget('/MenuBar/ChoicesMenu/IconAction').set_image(i)
uimanager.get_widget('/MenuBar/ChoicesMenu/IconAction').set_always_show_image(True)
def add_file_menu_actions(self, action_group):
action_filemenu = Gtk.Action("FileMenu", "File", None, None)
action_group.add_action(action_filemenu)
action_filenewmenu = Gtk.Action("FileNew", None, None, Gtk.STOCK_NEW)
action_group.add_action(action_filenewmenu)
action_new = Gtk.Action("FileNewStandard", "_New",
"Create a new file", Gtk.STOCK_NEW)
action_new.connect("activate", self.on_menu_file_new_generic)
action_group.add_action_with_accel(action_new, '<Ctrl>N')
action_group.add_actions([
("FileNewFoo", None, "New Foo", None, "Create new foo",
self.on_menu_file_new_generic),
("FileNewGoo", None, "_New Goo", None, "Create new goo",
self.on_menu_file_new_generic),
])
action_filequit = Gtk.Action("FileQuit", None, None, Gtk.STOCK_QUIT)
action_filequit.connect("activate", self.on_menu_file_quit)
action_group.add_action_with_accel(action_filequit, '<Ctrl>Q')
def add_edit_menu_actions(self, action_group):
action_group.add_actions([
("EditMenu", None, "Edit"),
("EditCopy", Gtk.STOCK_COPY, None, None, None,
self.on_menu_others),
("EditPaste", Gtk.STOCK_PASTE, None, None, None,
self.on_menu_others),
("EditSomething", None, "Something", "<control><alt>S", None,
self.on_menu_others)
])
def add_choices_menu_actions(self, action_group):
action_group.add_action(Gtk.Action("ChoicesMenu", "Choices", None,
None))
action_group.add_radio_actions([
("ChoiceOne", None, "One", None, None, 1),
("ChoiceTwo", None, "Two", None, None, 2)
], 1, self.on_menu_choices_changed)
three = Gtk.ToggleAction("ChoiceThree", "Three", None, None)
three.connect("toggled", self.on_menu_choices_toggled)
action_group.add_action(three)
ad = Gtk.Action('DisabledAction', 'Disabled Action', None, None)
ad.set_sensitive(False)
action_group.add_action(ad)
ia = Gtk.Action('InvisibleAction', 'Invisible Action', None, None)
ia.set_visible(False)
action_group.add_action(ia)
ta = Gtk.Action('TooltipAction', 'Tooltip Action', 'A tooltip', None)
action_group.add_action(ta)
action_group.add_action(Gtk.Action('IconAction', 'Icon Action', None, None))
def create_ui_manager(self):
uimanager = Gtk.UIManager()
# Throws exception if something went wrong
uimanager.add_ui_from_string(UI_INFO)
# Add the accelerator group to the toplevel window
accelgroup = uimanager.get_accel_group()
self.add_accel_group(accelgroup)
return uimanager
def on_menu_file_new_generic(self, widget):
print("A File|New menu item was selected.")
def on_menu_file_quit(self, widget):
app.quit()
def on_menu_others(self, widget):
print("Menu item " + widget.get_name() + " was selected")
def on_menu_choices_changed(self, widget, current):
print(current.get_name() + " was selected.")
def on_menu_choices_toggled(self, widget):
if widget.get_active():
print(widget.get_name() + " activated")
else:
print(widget.get_name() + " deactivated")
def on_button_press_event(self, widget, event):
# Check if right mouse button was preseed
if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
self.popup.popup(None, None, None, None, event.button, event.time)
return True # event has been handled
def convert(v):
if isinstance(v, (unicode_type, bytes)):
return unicode_type(v)
if isinstance(v, dbus.Struct):
return tuple(convert(val) for val in v)
if isinstance(v, list):
return [convert(val) for val in v]
if isinstance(v, dict):
return {convert(k):convert(val) for k, val in iteritems(v)}
if isinstance(v, dbus.Boolean):
return bool(v)
if isinstance(v, (dbus.UInt32, dbus.UInt16)):
return int(v)
return v
class MyApplication(Gtk.Application):
def do_activate(self):
win = self.window = MenuExampleWindow(self)
win.show_all()
self.get_xprop_data()
Thread(target=self.print_dbus_data).start()
def get_xprop_data(self):
win_id = self.window.get_window().get_xid()
try:
import xcb, xcb.xproto
except ImportError:
raise SystemExit('You must install the python-xpyb XCB bindings')
conn = xcb.Connection()
atoms = conn.core.ListProperties(win_id).reply().atoms
atom_names = {atom:conn.core.GetAtomNameUnchecked(atom) for atom in atoms}
atom_names = {k:bytes(a.reply().name.buf()) for k, a in iteritems(atom_names)}
property_names = {name:atom for atom, name in iteritems(atom_names) if
name.startswith('_GTK') or name.startswith('_UNITY') or name.startswith('_GNOME')}
replies = {name:conn.core.GetProperty(False, win_id, atom, xcb.xproto.GetPropertyType.Any, 0, 2 ** 32 - 1) for name, atom in iteritems(property_names)}
type_atom_cache = {}
def get_property_value(property_reply):
if property_reply.format == 8:
is_list_of_strings = 0 in property_reply.value[:-1]
ans = bytes(property_reply.value.buf())
if property_reply.type not in type_atom_cache:
type_atom_cache[property_reply.type] = bytes(conn.core.GetAtomNameUnchecked(property_reply.type).reply().name.buf())
if type_atom_cache[property_reply.type] == b'UTF8_STRING':
ans = ans.decode('utf-8')
if is_list_of_strings:
ans = ans.split('\0')
return ans
elif property_reply.format in (16, 32):
return list(struct.unpack(b'I' * property_reply.value_len,
property_reply.value.buf()))
return None
props = {name:get_property_value(r.reply()) for name, r in iteritems(replies)}
ans = ['\nX Window properties:']
for name in sorted(props):
ans.append('%s: %r' % (name, props[name]))
self.xprop_data = '\n'.join(ans)
self.object_path = props['_UNITY_OBJECT_PATH']
self.bus_name = props['_GTK_UNIQUE_BUS_NAME']
def print(self, *args):
self.data.append(' '.join(map(str, args)))
def print_menu_start(self, bus, group=0, seen=None):
groups = set()
seen = seen or set()
seen.add(group)
print = self.print
print('\nMenu description (Group %d)' % group)
for item in bus.call_blocking(self.bus_name, self.object_path, 'org.gtk.Menus', 'Start', 'au', ([group],)):
print('Subscription group:', item[0])
print('Menu number:', item[1])
for menu_item in item[2]:
menu_item = {unicode_type(k):convert(v) for k, v in iteritems(menu_item)}
if ':submenu' in menu_item:
groups.add(menu_item[':submenu'][0])
if ':section' in menu_item:
groups.add(menu_item[':section'][0])
print(pformat(menu_item))
for other_group in sorted(groups - seen):
self.print_menu_start(bus, other_group, seen)
def print_dbus_data(self):
bus = dbus.SessionBus()
time.sleep(0.5)
self.data = []
self.get_actions_description(bus)
self.print_menu_start(bus)
self.data.append(self.xprop_data)
self.window.label.set_text('\n'.join(self.data))
def get_actions_description(self, bus):
print = self.print
print('\nActions description')
self.actions_desc = d = {}
adata = bus.call_blocking(self.bus_name, self.object_path, 'org.gtk.Actions', 'DescribeAll', '', ())
for name in sorted(adata):
data = adata[name]
d[name] = {'enabled':convert(data[0]), 'param type': convert(data[1]), 'state':convert(data[2])}
print('Name:', name)
print(pformat(d[name]))
def do_startup(self):
Gtk.Application.do_startup(self)
app = MyApplication(application_id='com.calibre-ebook.test-gtk')
signal.signal(signal.SIGINT, signal.SIG_DFL)
sys.exit(app.run(sys.argv))

View File

@ -1,393 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
# 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
import dbus, sip
from qt.core import (
QApplication, QMenu, QIcon, QKeySequence, QObject, QEvent, QTimer, pyqtSignal, Qt)
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, swap_mnemonic_char, key_sequence_to_dbus_shortcut, icon_to_dbus_menu_icon)
from polyglot.builtins import iteritems
null = object()
def PropDict(mapping=()):
return dbus.Dictionary(mapping, signature='sv')
def create_properties_for_action(ac, previous=None):
ans = PropDict()
if ac.isSeparator():
ans['type'] = 'separator'
if not ac.isVisible():
ans['visible'] = False
return ans
text = ac.text() or ac.iconText()
if text:
ans['label'] = swap_mnemonic_char(text)
if not ac.isEnabled():
ans['enabled'] = False
if not ac.isVisible() or ac.property('blocked') is True:
ans['visible'] = False
if ac.menu() is not None:
ans['children-display'] = 'submenu'
if ac.isCheckable():
exclusive = ac.actionGroup() is not None and ac.actionGroup().isExclusive()
ans['toggle-type'] = 'radio' if exclusive else 'checkmark'
ans['toggle-state'] = int(ac.isChecked())
shortcuts = ac.shortcuts()
if shortcuts:
sc = dbus.Array(signature='as')
for s in shortcuts:
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():
for x in 'icon-data x-qt-icon-cache-key'.split():
ans[x] = previous[x]
else:
data = icon_to_dbus_menu_icon(ac.icon())
if data is not None:
ans['icon-data'] = data
ans['x-qt-icon-cache-key'] = dbus.UInt64(icon.cacheKey())
return ans
def menu_actions(menu):
try:
return menu.actions()
except TypeError:
if isinstance(menu, QMenu):
return QMenu.actions(menu)
raise
class DBusMenu(QObject):
handle_event_signal = pyqtSignal(object, object, object, object)
def __init__(self, object_path, parent=None, bus=None):
QObject.__init__(self, parent)
# Unity barfs is the Event DBUS method does not return immediately, so
# handle it asynchronously
self.handle_event_signal.connect(self.handle_event, type=Qt.ConnectionType.QueuedConnection)
self.dbus_api = DBusMenuAPI(self, object_path, bus=bus)
self.set_status = self.dbus_api.set_status
self._next_id = 0
self.action_changed_timer = t = QTimer(self)
t.setInterval(0), t.setSingleShot(True), t.timeout.connect(self.actions_changed)
self.layout_changed_timer = t = QTimer(self)
t.setInterval(0), t.setSingleShot(True), t.timeout.connect(self.layouts_changed)
self.init_maps()
@property
def object_path(self):
return self.dbus_api._object_path
def init_maps(self, qmenu=None):
self.action_changes = set()
self.layout_changes = set()
self.qmenu = qmenu
self._id_to_action, self._action_to_id = {}, {}
self._action_properties = {}
@property
def next_id(self):
self._next_id += 1
return self._next_id
def id_to_action(self, action_id):
if self.qmenu is None:
return None
return self._id_to_action.get(action_id)
def action_to_id(self, action):
if self.qmenu is None:
return None
return self._action_to_id.get(action)
def action_properties(self, action_id, restrict_to=None):
if self.qmenu is None:
return {}
ans = self._action_properties.get(action_id, PropDict())
if restrict_to:
ans = PropDict({k:v for k, v in iteritems(ans) if k in restrict_to})
return ans
def publish_new_menu(self, qmenu=None):
self.init_maps(qmenu)
if qmenu is not None:
connect_lambda(qmenu.destroyed, self, lambda self:self.publish_new_menu())
ac = qmenu.menuAction()
self.add_action(ac)
self.dbus_api.LayoutUpdated(self.dbus_api.revision, 0)
def set_visible(self, visible):
ac = self.id_to_action(0)
if ac is not None and self.qmenu is not None:
changed = False
blocked = not visible
for ac in menu_actions(ac.menu()):
ac_id = self.action_to_id(ac)
if ac_id is not None:
old = ac.property('blocked')
if old is not blocked:
ac.setProperty('blocked', blocked)
self.action_changes.add(ac_id)
changed = True
if changed:
self.action_changed_timer.start()
def add_action(self, ac):
ac_id = 0 if ac.menu() is self.qmenu else self.next_id
self._id_to_action[ac_id] = ac
self._action_to_id[ac] = ac_id
self._action_properties[ac_id] = create_properties_for_action(ac)
if ac.menu() is not None:
self.add_menu(ac.menu())
def add_menu(self, menu):
menu.installEventFilter(self)
for ac in menu_actions(menu):
self.add_action(ac)
def eventFilter(self, obj, ev):
ac = getattr(obj, 'menuAction', lambda : None)()
ac_id = self.action_to_id(ac)
if ac_id is not None and hasattr(ev, 'action'):
etype = ev.type()
if etype == QEvent.Type.ActionChanged:
ac_id = self.action_to_id(ev.action())
self.action_changes.add(ac_id)
self.action_changed_timer.start()
elif etype == QEvent.Type.ActionAdded:
self.layout_changes.add(ac_id)
self.layout_changed_timer.start()
self.add_action(ev.action())
elif etype == QEvent.Type.ActionRemoved:
self.layout_changes.add(ac_id)
self.layout_changed_timer.start()
self.action_removed(ev.action())
return False
def actions_changed(self):
updated_props = dbus.Array(signature='(ia{sv})')
removed_props = dbus.Array(signature='(ias)')
for ac_id in self.action_changes:
ac = self.id_to_action(ac_id)
if ac is None:
continue
old_props = self.action_properties(ac_id)
new_props = self._action_properties[ac_id] = create_properties_for_action(ac, old_props)
removed = set(old_props) - set(new_props)
if removed:
removed_props.append((ac_id, dbus.Array(removed, signature='as')))
updated = PropDict({k:v for k, v in iteritems(new_props) if v != old_props.get(k, null)})
if updated:
updated_props.append((ac_id, updated))
self.action_changes = set()
if updated_props or removed_props:
self.dbus_api.ItemsPropertiesUpdated(updated_props, removed_props)
return updated_props, removed_props
def layouts_changed(self):
changes = set()
for ac_id in self.layout_changes:
if ac_id in self._id_to_action:
changes.add(ac_id)
self.layout_changes = set()
if changes:
self.dbus_api.revision += 1
for change in changes:
self.dbus_api.LayoutUpdated(self.dbus_api.revision, change)
return changes
def action_is_in_a_menu(self, ac):
if sip.isdeleted(ac):
return False
all_menus = {a.menu() for a in self._action_to_id if not sip.isdeleted(a)}
all_menus.discard(None)
return bool(set(ac.associatedWidgets()).intersection(all_menus))
def action_removed(self, ac):
if not self.action_is_in_a_menu(ac):
ac_id = self._action_to_id.pop(ac, None)
self._id_to_action.pop(ac_id, None)
self._action_properties.pop(ac_id, None)
def get_layout(self, parent_id, depth, property_names):
# Ensure any pending updates are done, as they are needed now
self.actions_changed()
self.layouts_changed()
property_names = property_names or None
props = self.action_properties(parent_id, property_names)
return parent_id, props, self.get_layout_children(parent_id, depth, property_names)
def get_layout_children(self, parent_id, depth, property_names):
ans = dbus.Array(signature='v')
ac = self.id_to_action(parent_id)
if ac is not None and depth != 0 and ac.menu() is not None:
for child in menu_actions(ac.menu()):
child_id = self.action_to_id(child)
if child_id is not None:
props = self.action_properties(child_id, property_names)
ans.append((child_id, props, self.get_layout_children(child_id, depth - 1, property_names)))
return ans
def get_properties(self, ids=None, property_names=None):
property_names = property_names or None
ans = dbus.Array(signature='(ia{sv})')
for action_id in (ids or self._id_to_action):
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':
if ac.isCheckable():
ac.toggle()
ac.triggered.emit(ac.isCheckable() and ac.isChecked())
def handle_about_to_show(self, ac):
child_ids = {self.action_to_id(x) for x in menu_actions(ac.menu())}
child_ids.discard(None)
ac_id = self.action_to_id(ac)
ac.menu().aboutToShow.emit()
if ac_id in self.layout_changes or child_ids.intersection(self.action_changes):
return True
return False
class DBusMenuAPI(Object):
IFACE = 'com.canonical.dbusmenu'
def __init__(self, menu, object_path, bus=None):
if bus is None:
bus = dbus.SessionBus()
Object.__init__(self, bus, object_path)
self.status = 'normal'
self.menu = menu
self.revision = 0
@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' if QApplication.instance().isLeftToRight() else 'rtl'
@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):
layout = self.menu.get_layout(parentId, recursionDepth, propertyNames)
return self.revision, layout
@dbus_method(IFACE, in_signature='aias', out_signature='a(ia{sv})')
def GetGroupProperties(self, ids, propertyNames):
return self.menu.get_properties(ids, propertyNames)
@dbus_method(IFACE, in_signature='is', out_signature='v')
def GetProperty(self, id, 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. eventId can be one of the following::
* "clicked"
* "hovered"
* "opened"
* "closed"
Vendor specific events can be added by prefixing them with "x-<vendor>-"'''
if self.menu.id_to_action(id) is not None:
self.menu.handle_event_signal.emit(id, eventId, data, timestamp)
@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.'''
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_signal.emit(id, eventId, data, timestamp)
else:
missing.append(id)
return missing
@dbus_method(IFACE, in_signature='i', out_signature='b')
def AboutToShow(self, id):
ac = self.menu.id_to_action(id)
if ac is not None and ac.menu() is not None:
return self.menu.handle_about_to_show(ac)
return False
@dbus_method(IFACE, in_signature='ai', out_signature='aiai')
def AboutToShowGroup(self, ids):
updates_needed = dbus.Array(signature='i')
id_errors = dbus.Array(signature='i')
for ac_id in ids:
ac = self.menu.id_to_action(id)
if ac is not None and ac.menu() is not None:
if self.menu.handle_about_to_show(ac):
updates_needed.append(ac_id)
else:
id_errors.append(ac_id)
return updates_needed, id_errors
@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()
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_()
del dbus_name
if __name__ == '__main__':
test()

View File

@ -1,241 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
# Implement the StatusNotifierItem spec for creating a system tray icon in
# modern linux desktop environments. See
# http://www.notmart.org/misc/statusnotifieritem/index.html#introduction
# This is not an actual standard, but is apparently used by GNOME, KDE and
# Unity, which makes it necessary enough to implement.
import os
import dbus
from qt.core import (
QApplication, QObject, pyqtSignal, Qt, QPoint, QRect, QMenu,
QSystemTrayIcon, QIcon)
from calibre.gui2.dbus_export.menu import DBusMenu
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
class StatusNotifierItem(QObject):
IFACE = 'org.kde.StatusNotifierItem'
activated = pyqtSignal(object)
show_menu = pyqtSignal(int, int)
def __init__(self, **kw):
global _sni_count
QObject.__init__(self, parent=kw.get('parent'))
self.context_menu = None
self.is_visible = True
self.tool_tip = ''
path = I('calibre-tray.png')
if path and os.path.exists(path):
self._icon = QIcon(path)
else:
self._icon = QApplication.instance().windowIcon()
self.show_menu.connect(self._show_menu, type=Qt.ConnectionType.QueuedConnection)
_sni_count += 1
kw['num'] = _sni_count
self.dbus_api = StatusNotifierItemAPI(self, **kw)
def _show_menu(self, x, y):
m = self.contextMenu()
if m is not None:
m.exec_(QPoint(x, y))
def isVisible(self):
return self.is_visible
def setVisible(self, visible):
if self.is_visible != visible:
self.is_visible = visible
self.dbus_api.NewStatus(self.dbus_api.Status)
def show(self):
self.setVisible(True)
def hide(self):
self.setVisible(False)
def toggle(self):
self.setVisible(not self.isVisible())
def contextMenu(self):
return self.context_menu
def setContextMenu(self, menu):
self.context_menu = menu
self.dbus_api.publish_new_menu()
def geometry(self):
return QRect()
def toolTip(self):
return self.tool_tip
def setToolTip(self, val):
self.tool_tip = val or ''
self.dbus_api.NewToolTip()
def setIcon(self, icon):
self._icon = icon
self.dbus_api.NewIcon()
def icon(self):
return self._icon
@classmethod
def supportsMessages(cls):
return False
def emit_activated(self):
self.activated.emit(QSystemTrayIcon.ActivationReason.Trigger)
_status_item_menu_count = 0
class StatusNotifierItemAPI(Object):
'See http://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html'
IFACE = 'org.kde.StatusNotifierItem'
def __init__(self, notifier, **kw):
global _status_item_menu_count
self.notifier = notifier
bus = kw.get('bus')
if bus is None:
bus = kw['bus'] = dbus.SessionBus()
self.name = '%s-%s-%s' % (self.IFACE, os.getpid(), kw.get('num', 1))
self.dbus_name = BusName(self.name, bus=bus, do_not_queue=True)
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
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'))
def publish_new_menu(self):
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.emit_activated)
# 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):
return icon_cache().name_for_icon(self.notifier.icon())
@dbus_property(IFACE, signature='s')
def IconThemePath(self):
return icon_cache().icon_theme_path
@dbus_property(IFACE, signature='a(iiay)')
def IconPixmap(self):
return dbus.Array(signature='(iiay)')
@dbus_property(IFACE, signature='s')
def OverlayIconName(self):
return ''
@dbus_property(IFACE, signature='(sa(iiay)ss)')
def ToolTip(self):
# This is ignored on Unity, Canonical believes in user interfaces
# that are so functionality free that they dont need tooltips
return self.IconName, self.IconPixmap, self.Title, self.notifier.toolTip()
@dbus_property(IFACE, signature='a(iiay)')
def OverlayIconPixmap(self):
return dbus.Array(signature='(iiay)')
@dbus_property(IFACE, signature='s')
def AttentionIconName(self):
return ''
@dbus_property(IFACE, signature='a(iiay)')
def AttentionIconPixmap(self):
return dbus.Array(signature='(iiay)')
@dbus_property(IFACE, signature='s')
def Category(self):
return self.category
@dbus_property(IFACE, signature='s')
def Id(self):
return self.app_id
@dbus_property(IFACE, signature='s')
def Title(self):
return self.title
@dbus_property(IFACE, signature='s')
def Status(self):
return 'Active' if self.notifier.isVisible() else 'Passive'
@dbus_property(IFACE, signature='o')
def Menu(self):
return dbus.ObjectPath(self.dbus_menu.object_path)
@dbus_property(IFACE, signature='u')
def WindowId(self):
return 0
@dbus_method(IFACE, in_signature='ii', out_signature='')
def ContextMenu(self, x, y):
self.notifier.show_menu.emit(x, y)
@dbus_method(IFACE, in_signature='ii', out_signature='')
def Activate(self, x, y):
self.notifier.activated.emit(QSystemTrayIcon.ActivationReason.Trigger)
@dbus_method(IFACE, in_signature='u', out_signature='')
def XAyatanaSecondaryActivate(self, timestamp):
# This is called when the user middle clicks the icon in Unity
self.notifier.activated.emit(QSystemTrayIcon.ActivationReason.MiddleClick)
@dbus_method(IFACE, in_signature='ii', out_signature='')
def SecondaryActivate(self, x, y):
self.notifier.activated.emit(QSystemTrayIcon.ActivationReason.MiddleClick)
@dbus_method(IFACE, in_signature='is', out_signature='')
def Scroll(self, delta, orientation):
pass
@dbus_signal(IFACE, '')
def NewTitle(self):
pass
@dbus_signal(IFACE, '')
def NewIcon(self):
pass
@dbus_signal(IFACE, '')
def NewAttentionIcon(self):
pass
@dbus_signal(IFACE, '')
def NewOverlayIcon(self):
pass
@dbus_signal(IFACE, '')
def NewToolTip(self):
pass
@dbus_signal(IFACE, 's')
def NewStatus(self, status):
pass

View File

@ -1,159 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, array, re, os, errno
import dbus
from qt.core import QSize, QImage, Qt, QKeySequence, QBuffer, QByteArray, QIODevice
from polyglot.builtins import unicode_type, iteritems
def log(*args, **kw):
kw['file'] = sys.stderr
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))
tc = b'L' if array.array(b'I').itemsize < 4 else b'I'
for size in sizes:
# Convert to DBUS struct of width, height, and image data in ARGB32
# in network endianness
i = qicon.pixmap(size).toImage().convertToFormat(QImage.Format.Format_ARGB32)
w, h = i.width(), i.height()
data = i.constBits().asstring(4 * w * h)
if socket.htonl(1) != 1:
# Host endianness != Network Endiannes
data = array.array(tc, i.constBits().asstring(4 * i.width() * i.height()))
data.byteswap()
data = data.tostring()
ans.append((w, h, dbus.ByteArray(data)))
return ans
def swap_mnemonic_char(text, from_char='&', to_char='_'):
text = text.replace(to_char, to_char * 2) # Escape to_char
# Replace the first occurence of an unescaped from_char with to_char
text = re.sub(r'(?<!{0}){0}(?!$)'.format(from_char), to_char, text, count=1)
# Remove any remaining unescaped from_char
text = re.sub(r'(?<!{0}){0}(?!$)'.format(from_char), '', text)
# Unescape from_char
text = text.replace(from_char * 2, from_char)
return text
def key_sequence_to_dbus_shortcut(qks):
for key in qks:
if key == -1 or key == Qt.Key.Key_unknown:
continue
items = []
for mod, name in iteritems({Qt.Modifier.META:'Super', Qt.Modifier.CTRL:'Control', Qt.Modifier.ALT:'Alt', Qt.Modifier.SHIFT:'Shift'}):
if key & mod == mod:
items.append(name)
key &= int(~(Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier |
Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.MetaModifier | Qt.KeyboardModifier.KeypadModifier))
text = QKeySequence(key).toString()
if text:
text = {'+':'plus', '-':'minus'}.get(text, text)
items.append(text)
if items:
yield items
def icon_to_dbus_menu_icon(icon, size=32):
if icon.isNull():
return None
ba = QByteArray()
buf = QBuffer(ba)
buf.open(QIODevice.OpenModeFlag.WriteOnly)
icon.pixmap(32).save(buf, 'PNG')
return dbus.ByteArray(ba)
def setup_for_cli_run():
import signal
from dbus.mainloop.glib import DBusGMainLoop, threads_init
threads_init()
DBusGMainLoop(set_as_default=True)
signal.signal(signal.SIGINT, signal.SIG_DFL) # quit on Ctrl-C
def set_X_window_properties(win_id, **properties):
' Set X Window properties on the window with the specified id. Only string values are supported. '
import xcb, xcb.xproto
conn = xcb.connect()
atoms = {name:conn.core.InternAtom(False, len(name), name) for name in properties}
utf8_string_atom = None
for name, val in iteritems(properties):
atom = atoms[name].reply().atom
type_atom = xcb.xproto.Atom.STRING
if isinstance(val, unicode_type):
if utf8_string_atom is None:
utf8_string_atom = conn.core.InternAtom(True, len(b'UTF8_STRING'), b'UTF8_STRING').reply().atom
type_atom = utf8_string_atom
val = val.encode('utf-8')
conn.core.ChangePropertyChecked(xcb.xproto.PropMode.Replace, win_id, atom, type_atom, 8, len(val), val)
conn.flush()
conn.disconnect()

View File

@ -1,255 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import time, sys, weakref
from qt.core import (
QObject, QMenuBar, QAction, QEvent, QSystemTrayIcon, QApplication, Qt)
from calibre.constants import iswindows, ismacos
from polyglot.builtins import range, unicode_type
UNITY_WINDOW_REGISTRAR = ('com.canonical.AppMenu.Registrar', '/com/canonical/AppMenu/Registrar', 'com.canonical.AppMenu.Registrar')
STATUS_NOTIFIER = ("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher", "org.kde.StatusNotifierWatcher")
def log(*args, **kw):
kw['file'] = sys.stderr
print('DBusExport:', *args, **kw)
kw['file'].flush()
class MenuBarAction(QAction):
def __init__(self, mb):
QAction.__init__(self, mb)
def menu(self):
return self.parent()
menu_counter = 0
class ExportedMenuBar(QMenuBar): # {{{
is_native_menubar = True
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')
self._blocked = False
self.is_visible = True
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, menu_registrar=None):
self.menu_registrar = menu_registrar or self.menu_registrar
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):
self.is_visible = visible
self.dbus_menu.set_visible(self.is_visible and not self._blocked)
def isVisible(self):
return self.is_visible
def show(self):
self.setVisible(True)
def hide(self):
self.setVisible(False)
def menuAction(self):
return self.menu_action
def _block(self):
self._blocked = True
self.setVisible(self.is_visible)
def _unblock(self):
self._blocked = False
self.setVisible(self.is_visible)
def eventFilter(self, obj, ev):
etype = ev.type()
if etype == QEvent.Type.Show:
# Hiding a window causes the registrar to auto-unregister it, so we
# have to re-register it on show events.
self.register()
elif etype == QEvent.Type.WinIdChange:
self.unregister()
self.register()
return False
# }}}
class Factory(QObject):
def __init__(self, app_id=None):
QObject.__init__(self)
self.app_id = app_id or QApplication.instance().applicationName() or 'unknown_application'
if iswindows or ismacos:
self.dbus = None
else:
try:
import dbus
self.dbus = dbus
except ImportError as err:
log('Failed to import dbus, with error:', unicode_type(err))
self.dbus = None
self.menu_registrar = None
self.status_notifier = None
self._bus = None
self.status_notifiers, self.window_menus = [], []
def prune_dead_refs(self):
self.status_notifiers = [ref for ref in self.status_notifiers if ref() is not None]
self.window_menus = [ref for ref in self.window_menus if ref() is not None]
def window_registrar_changed(self, new_owner):
if new_owner:
self.menu_registrar = None
if self.has_global_menu:
for ref in self.window_menus:
w = ref()
if w is not None:
w.register(self.menu_registrar)
def status_notifier_registrar_changed(self, new_owner):
if new_owner:
self.status_notifier = None
if self.has_status_notifier:
for ref in self.status_notifiers:
w = ref()
if w is not None:
self.register_status_notifier(w)
@property
def bus(self):
if self._bus is None:
try:
self._bus = self.dbus.SessionBus()
self._bus.call_on_disconnection(self.bus_disconnected)
self._bus.watch_name_owner(UNITY_WINDOW_REGISTRAR[0], self.window_registrar_changed)
self._bus.watch_name_owner(STATUS_NOTIFIER[0], self.status_notifier_registrar_changed)
except Exception as err:
log('Failed to connect to DBUS session bus, with error:', unicode_type(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
log('Failed to detect window menu registrar, with error:', unicode_type(err))
return bool(self.menu_registrar)
def detect_menu_registrar(self):
self.menu_registrar = False
if self.bus is not None and self.bus.name_has_owner(UNITY_WINDOW_REGISTRAR[0]):
self.menu_registrar = UNITY_WINDOW_REGISTRAR
@property
def has_status_notifier(self):
if self.status_notifier is None:
if self.dbus is None:
self.status_notifier = False
else:
try:
self.detect_status_notifier()
except Exception as err:
self.status_notifier = False
log('Failed to detect window status notifier, with error:', unicode_type(err))
return bool(self.status_notifier)
def detect_status_notifier(self):
'See http://www.notmart.org/misc/statusnotifieritem/statusnotifierwatcher.html'
self.status_notifier = False
if self.bus is not None and self.bus.name_has_owner(STATUS_NOTIFIER[0]):
args = STATUS_NOTIFIER[:2] + (self.dbus.PROPERTIES_IFACE, 'Get', 'ss', (STATUS_NOTIFIER[-1], 'IsStatusNotifierHostRegistered'))
self.status_notifier = bool(self.bus.call_blocking(*args, timeout=0.1))
def create_window_menubar(self, parent):
if not QApplication.instance().testAttribute(Qt.ApplicationAttribute.AA_DontUseNativeMenuBar) and self.has_global_menu:
ans = ExportedMenuBar(parent, self.menu_registrar, self.bus)
self.prune_dead_refs()
self.window_menus.append(weakref.ref(ans))
return ans
ans = QMenuBar(parent)
parent.setMenuBar(ans)
ans.is_native_menubar = False
return ans
def register_status_notifier(self, item):
args = STATUS_NOTIFIER + ('RegisterStatusNotifierItem', 's', (item.dbus_api.name,))
self.bus.call_blocking(*args, timeout=1)
def create_system_tray_icon(self, parent=None, title=None, category=None):
if self.has_status_notifier:
from calibre.gui2.dbus_export.tray import StatusNotifierItem
ans = StatusNotifierItem(parent=parent, title=title, app_id=self.app_id, category=category)
self.register_status_notifier(ans)
self.prune_dead_refs()
self.status_notifiers.append(weakref.ref(ans))
return ans
if iswindows or ismacos:
return QSystemTrayIcon(parent)
def bus_disconnected(self):
self._bus = None
for i in range(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(app_id=None):
global _factory
if _factory is None:
_factory = Factory(app_id=app_id)
return _factory

View File

@ -11,7 +11,7 @@ from itertools import product
from qt.core import (
QAction, QApplication, QColor, QDockWidget, QEvent, QHBoxLayout, QIcon, QImage,
QLabel, QMenu, QPalette, QPixmap, QSize, QStackedWidget, Qt, QTabWidget, QTimer,
QUrl, QVBoxLayout, QWidget, pyqtSignal
QUrl, QVBoxLayout, QWidget, pyqtSignal, QMenuBar
)
from calibre import prepare_string_for_xml, prints
@ -21,7 +21,6 @@ from calibre.constants import (
)
from calibre.customize.ui import find_plugin
from calibre.gui2 import elided_text, open_url
from calibre.gui2.dbus_export.widgets import factory
from calibre.gui2.keyboard import Manager as KeyboardManager
from calibre.gui2.main_window import MainWindow
from calibre.gui2.throbber import ThrobbingButton
@ -618,8 +617,9 @@ class Main(MainWindow):
p, q = self.create_application_menubar()
q.triggered.connect(self.action_quit.trigger)
p.triggered.connect(self.action_preferences.trigger)
f = factory(app_id='com.calibre-ebook.EditBook-%d' % os.getpid())
b = f.create_window_menubar(self)
b = QMenuBar(self)
self.setMenuBar(b)
b.is_native_menubar = False
f = b.addMenu(_('&File'))
f.addAction(self.action_new_file)

View File

@ -38,7 +38,6 @@ from calibre.gui2 import (
from calibre.gui2.auto_add import AutoAdder
from calibre.gui2.changes import handle_changes
from calibre.gui2.cover_flow import CoverFlowMixin
from calibre.gui2.dbus_export.widgets import factory
from calibre.gui2.device import DeviceMixin
from calibre.gui2.dialogs.message_box import JobError
from calibre.gui2.ebook_download import EbookDownloadMixin
@ -248,9 +247,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.viewers = deque()
self.system_tray_icon = None
do_systray = config['systray_icon'] or opts.start_in_tray
if do_systray:
self.system_tray_icon = factory(app_id='com.calibre-ebook.gui').create_system_tray_icon(parent=self, title='calibre')
if self.system_tray_icon is not None:
if do_systray and QSystemTrayIcon.isSystemTrayAvailable():
self.system_tray_icon = QSystemTrayIcon(self)
self.system_tray_icon.setIcon(QIcon(I('lt.png', allow_user_override=False)))
if not (iswindows or ismacos):
self.system_tray_icon.setIcon(QIcon.fromTheme('calibre-tray', self.system_tray_icon.icon()))
@ -259,7 +257,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.jobs_button.tray_tooltip_updated.connect(self.system_tray_icon.setToolTip)
elif do_systray:
prints('Failed to create system tray icon, your desktop environment probably'
' does not support the StatusNotifier spec https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/')
' does not support the StatusNotifier spec https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/',
file=sys.stderr, flush=True)
self.system_tray_menu = QMenu(self)
self.toggle_to_tray_action = self.system_tray_menu.addAction(QIcon(I('page.png')), '')
self.toggle_to_tray_action.triggered.connect(self.system_tray_icon_activated)

File diff suppressed because it is too large Load Diff