From efdf6614575bc768590558d3fee427965947e6e9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 24 Jun 2021 10:55:12 +0530 Subject: [PATCH] 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. --- setup/test.py | 11 +- src/calibre/gui2/bars.py | 10 +- src/calibre/gui2/dbus_export/__init__.py | 9 - src/calibre/gui2/dbus_export/demo.py | 170 ---- src/calibre/gui2/dbus_export/gtk.py | 314 ------- src/calibre/gui2/dbus_export/menu.py | 393 -------- src/calibre/gui2/dbus_export/tray.py | 241 ----- src/calibre/gui2/dbus_export/utils.py | 159 ---- src/calibre/gui2/dbus_export/widgets.py | 255 ------ src/calibre/gui2/tweak_book/ui.py | 8 +- src/calibre/gui2/ui.py | 9 +- src/calibre/utils/dbus_service.py | 1052 ---------------------- 12 files changed, 15 insertions(+), 2616 deletions(-) delete mode 100644 src/calibre/gui2/dbus_export/__init__.py delete mode 100644 src/calibre/gui2/dbus_export/demo.py delete mode 100644 src/calibre/gui2/dbus_export/gtk.py delete mode 100644 src/calibre/gui2/dbus_export/menu.py delete mode 100644 src/calibre/gui2/dbus_export/tray.py delete mode 100644 src/calibre/gui2/dbus_export/utils.py delete mode 100644 src/calibre/gui2/dbus_export/widgets.py delete mode 100644 src/calibre/utils/dbus_service.py diff --git a/setup/test.py b/setup/test.py index f9fe9d569b..29773994cb 100644 --- a/setup/test.py +++ b/setup/test.py @@ -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) diff --git a/src/calibre/gui2/bars.py b/src/calibre/gui2/bars.py index e2a9efda61..7f6de54be4 100644 --- a/src/calibre/gui2/bars.py +++ b/src/calibre/gui2/bars.py @@ -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 diff --git a/src/calibre/gui2/dbus_export/__init__.py b/src/calibre/gui2/dbus_export/__init__.py deleted file mode 100644 index a33a31c735..0000000000 --- a/src/calibre/gui2/dbus_export/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=utf-8 - - -__license__ = 'GPL v3' -__copyright__ = '2014, Kovid Goyal ' - - - diff --git a/src/calibre/gui2/dbus_export/demo.py b/src/calibre/gui2/dbus_export/demo.py deleted file mode 100644 index b8274f7b04..0000000000 --- a/src/calibre/gui2/dbus_export/demo.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=utf-8 - - -__license__ = 'GPL v3' -__copyright__ = '2014, Kovid Goyal ' - -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_() diff --git a/src/calibre/gui2/dbus_export/gtk.py b/src/calibre/gui2/dbus_export/gtk.py deleted file mode 100644 index 2b7c387f40..0000000000 --- a/src/calibre/gui2/dbus_export/gtk.py +++ /dev/null @@ -1,314 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=utf-8 - - -__license__ = 'GPL v3' -__copyright__ = '2014, Kovid Goyal ' - -# 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 = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -""" - - -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, '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, '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", "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)) diff --git a/src/calibre/gui2/dbus_export/menu.py b/src/calibre/gui2/dbus_export/menu.py deleted file mode 100644 index 91c3ae3606..0000000000 --- a/src/calibre/gui2/dbus_export/menu.py +++ /dev/null @@ -1,393 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=utf-8 - - -__license__ = 'GPL v3' -__copyright__ = '2014, Kovid Goyal ' - -# 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--"''' - 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() diff --git a/src/calibre/gui2/dbus_export/tray.py b/src/calibre/gui2/dbus_export/tray.py deleted file mode 100644 index cc19790c08..0000000000 --- a/src/calibre/gui2/dbus_export/tray.py +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=utf-8 - - -__license__ = 'GPL v3' -__copyright__ = '2014, Kovid Goyal ' - -# 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 diff --git a/src/calibre/gui2/dbus_export/utils.py b/src/calibre/gui2/dbus_export/utils.py deleted file mode 100644 index 3aeb1eac80..0000000000 --- a/src/calibre/gui2/dbus_export/utils.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=utf-8 - - -__license__ = 'GPL v3' -__copyright__ = '2014, Kovid Goyal ' - -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'(?' - -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 diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index f6a7b9b595..05f7bc351c 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -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) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 3353a02e72..f4bfa5150e 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -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) diff --git a/src/calibre/utils/dbus_service.py b/src/calibre/utils/dbus_service.py deleted file mode 100644 index fb38136daf..0000000000 --- a/src/calibre/utils/dbus_service.py +++ /dev/null @@ -1,1052 +0,0 @@ - - -# Copyright (C) 2003-2006 Red Hat Inc. -# Copyright (C) 2003 David Zeuthen -# Copyright (C) 2004 Rob Taylor -# Copyright (C) 2005-2006 Collabora Ltd. -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, copy, -# modify, merge, publish, distribute, sublicense, and/or sell copies -# of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -# - -__all__ = ('BusName', 'Object', 'PropertiesInterface', 'method', 'dbus_property', 'signal') -__docformat__ = 'restructuredtext' - -import sys -import logging -import threading -import traceback -from collections.abc import Sequence - -import _dbus_bindings -from dbus import ( - INTROSPECTABLE_IFACE, ObjectPath, PROPERTIES_IFACE, SessionBus, Signature, - Struct, validate_bus_name, validate_object_path) -from dbus.decorators import method, signal, validate_interface_name, validate_member_name -from dbus.exceptions import ( - DBusException, NameExistsException, UnknownMethodException) -from dbus.lowlevel import ErrorMessage, MethodReturnMessage, MethodCallMessage -from dbus.proxies import LOCAL_PATH - -from polyglot.builtins import itervalues, zip, native_string_type - - -class dbus_property(object): - """A decorator used to mark properties of a `dbus.service.Object`. - """ - - def __init__(self, dbus_interface=None, signature=None, - property_name=None, emits_changed_signal=None, - fget=None, fset=None, doc=None): - """Initialize the decorator used to mark properties of a - `dbus.service.Object`. - - :Parameters: - `dbus_interface` : str - The D-Bus interface owning the property - - `signature` : str - The signature of the property in the usual D-Bus notation. The - signature must be suitable to be carried in a variant. - - `property_name` : str - A name for the property. Defaults to the name of the getter or - setter function. - - `emits_changed_signal` : True, False, "invalidates", or None - Tells for introspection if the object emits PropertiesChanged - signal. - - `fget` : func - Getter function taking the instance from which to read the - property. - - `fset` : func - Setter function taking the instance to which set the property - and the property value. - - `doc` : str - Documentation string for the property. Defaults to documentation - string of getter function. - - :Since: 1.3.0 - """ - validate_interface_name(dbus_interface) - self._dbus_interface = dbus_interface - - self._init_property_name = property_name - if property_name is None: - if fget is not None: - property_name = fget.__name__ - elif fset is not None: - property_name = fset.__name__ - if property_name: - validate_member_name(property_name) - self.__name__ = property_name - - self._init_doc = doc - if doc is None and fget is not None: - doc = getattr(fget, "__doc__", None) - self.fget = fget - self.fset = fset - self.__doc__ = doc - - self._emits_changed_signal = emits_changed_signal - if len(tuple(Signature(signature))) != 1: - raise ValueError('signature must have only one item') - self._dbus_signature = signature - - def __get__(self, inst, type=None): - if inst is None: - return self - if self.fget is None: - raise AttributeError("unreadable attribute") - return self.fget(inst) - - def __set__(self, inst, value): - if self.fset is None: - raise AttributeError("can't set attribute") - self.fset(inst, value) - - def __call__(self, fget): - return self.getter(fget) - - def _copy(self, fget=None, fset=None): - return dbus_property(dbus_interface=self._dbus_interface, - signature=self._dbus_signature, - property_name=self._init_property_name, - emits_changed_signal=self._emits_changed_signal, - fget=fget or self.fget, fset=fset or self.fset, - doc=self._init_doc) - - def getter(self, fget): - return self._copy(fget=fget) - - def setter(self, fset): - return self._copy(fset=fset) - - -_logger = logging.getLogger('dbus.service') - - -class _VariantSignature(object): - """A fake method signature which, when iterated, yields an endless stream - of 'v' characters representing variants (handy with zip()). - - It has no string representation. - """ - - def __iter__(self): - """Return self.""" - return self - - def __next__(self): - """Return 'v' whenever called.""" - return 'v' - - -class BusName(object): - """A base class for exporting your own Named Services across the Bus. - - When instantiated, objects of this class attempt to claim the given - well-known name on the given bus for the current process. The name is - released when the BusName object becomes unreferenced. - - If a well-known name is requested multiple times, multiple references - to the same BusName object will be returned. - - Caveats - ------- - - Assumes that named services are only ever requested using this class - - if you request names from the bus directly, confusion may occur. - - Does not handle queueing. - """ - def __new__(cls, name, bus=None, allow_replacement=False , replace_existing=False, do_not_queue=False): - """Constructor, which may either return an existing cached object - or a new object. - - :Parameters: - `name` : str - The well-known name to be advertised - `bus` : dbus.Bus - A Bus on which this service will be advertised. - - Omitting this parameter or setting it to None has been - deprecated since version 0.82.1. For backwards compatibility, - if this is done, the global shared connection to the session - bus will be used. - - `allow_replacement` : bool - If True, other processes trying to claim the same well-known - name will take precedence over this one. - `replace_existing` : bool - If True, this process can take over the well-known name - from other processes already holding it. - `do_not_queue` : bool - If True, this service will not be placed in the queue of - services waiting for the requested name if another service - already holds it. - """ - validate_bus_name(name, allow_well_known=True, allow_unique=False) - - # if necessary, get default bus (deprecated) - if bus is None: - import warnings - warnings.warn('Omitting the "bus" parameter to ' - 'dbus.service.BusName.__init__ is deprecated', - DeprecationWarning, stacklevel=2) - bus = SessionBus() - - # see if this name is already defined, return it if so - # FIXME: accessing internals of Bus - if name in bus._bus_names: - return bus._bus_names[name] - - # otherwise register the name - name_flags = ( - (allow_replacement and _dbus_bindings.NAME_FLAG_ALLOW_REPLACEMENT or 0) | - (replace_existing and _dbus_bindings.NAME_FLAG_REPLACE_EXISTING or 0) | - (do_not_queue and _dbus_bindings.NAME_FLAG_DO_NOT_QUEUE or 0)) - - retval = bus.request_name(name, name_flags) - - # TODO: more intelligent tracking of bus name states? - if retval == _dbus_bindings.REQUEST_NAME_REPLY_PRIMARY_OWNER: - pass - elif retval == _dbus_bindings.REQUEST_NAME_REPLY_IN_QUEUE: - # queueing can happen by default, maybe we should - # track this better or let the user know if they're - # queued or not? - pass - elif retval == _dbus_bindings.REQUEST_NAME_REPLY_EXISTS: - raise NameExistsException(name) - elif retval == _dbus_bindings.REQUEST_NAME_REPLY_ALREADY_OWNER: - # if this is a shared bus which is being used by someone - # else in this process, this can happen legitimately - pass - else: - raise RuntimeError('requesting bus name %s returned unexpected value %s' % (name, retval)) - - # and create the object - bus_name = object.__new__(cls) - bus_name._bus = bus - bus_name._name = name - - # cache instance (weak ref only) - # FIXME: accessing Bus internals again - bus._bus_names[name] = bus_name - - return bus_name - - # do nothing because this is called whether or not the bus name - # object was retrieved from the cache or created new - def __init__(self, *args, **keywords): - pass - - # we can delete the low-level name here because these objects - # are guaranteed to exist only once for each bus name - def __del__(self): - self._bus.release_name(self._name) - pass - - def get_bus(self): - """Get the Bus this Service is on""" - return self._bus - - def get_name(self): - """Get the name of this service""" - return self._name - - def __repr__(self): - return '' % (self._name, self._bus, id(self)) - __str__ = __repr__ - - -def _method_lookup(self, method_name, dbus_interface): - """Walks the Python MRO of the given class to find the method to invoke. - - Returns two methods, the one to call, and the one it inherits from which - defines its D-Bus interface name, signature, and attributes. - """ - parent_method = None - candidate_class = None - successful = False - - # split up the cases when we do and don't have an interface because the - # latter is much simpler - if dbus_interface: - # search through the class hierarchy in python MRO order - for cls in self.__class__.__mro__: - # if we haven't got a candidate class yet, and we find a class with a - # suitably named member, save this as a candidate class - if (not candidate_class and method_name in cls.__dict__): - if ("_dbus_is_method" in cls.__dict__[method_name].__dict__ and - "_dbus_interface" in cls.__dict__[method_name].__dict__): - # however if it is annotated for a different interface - # than we are looking for, it cannot be a candidate - if cls.__dict__[method_name]._dbus_interface == dbus_interface: - candidate_class = cls - parent_method = cls.__dict__[method_name] - successful = True - break - else: - pass - else: - candidate_class = cls - - # if we have a candidate class, carry on checking this and all - # superclasses for a method annoated as a dbus method - # on the correct interface - if (candidate_class and method_name in cls.__dict__ and - "_dbus_is_method" in cls.__dict__[method_name].__dict__ and - "_dbus_interface" in cls.__dict__[method_name].__dict__ and - cls.__dict__[method_name]._dbus_interface == dbus_interface): - # the candidate class has a dbus method on the correct interface, - # or overrides a method that is, success! - parent_method = cls.__dict__[method_name] - successful = True - break - - else: - # simpler version of above - for cls in self.__class__.__mro__: - if (not candidate_class and method_name in cls.__dict__): - candidate_class = cls - - if (candidate_class and method_name in cls.__dict__ and - "_dbus_is_method" in cls.__dict__[method_name].__dict__): - parent_method = cls.__dict__[method_name] - successful = True - break - - if successful: - return (candidate_class.__dict__[method_name], parent_method) - else: - if dbus_interface: - raise UnknownMethodException('%s is not a valid method of interface %s' % (method_name, dbus_interface)) - else: - raise UnknownMethodException('%s is not a valid method' % method_name) - - -def _method_reply_return(connection, message, method_name, signature, *retval): - reply = MethodReturnMessage(message) - try: - reply.append(signature=signature, *retval) - except Exception as e: - logging.basicConfig() - if signature is None: - try: - signature = reply.guess_signature(retval) + ' (guessed)' - except Exception as e: - _logger.error('Unable to guess signature for arguments %r: ' - '%s: %s', retval, e.__class__, e) - raise - _logger.error('Unable to append %r to message with signature %s: ' - '%s: %s', retval, signature, e.__class__, e) - raise - - connection.send_message(reply) - - -def _method_reply_error(connection, message, exception): - name = getattr(exception, '_dbus_error_name', None) - - if name is not None: - pass - elif getattr(exception, '__module__', '') in ('', '__main__'): - name = 'org.freedesktop.DBus.Python.%s' % exception.__class__.__name__ - else: - name = 'org.freedesktop.DBus.Python.%s.%s' % (exception.__module__, exception.__class__.__name__) - - et, ev, etb = sys.exc_info() - if isinstance(exception, DBusException) and not exception.include_traceback: - # We don't actually want the traceback anyway - contents = exception.get_dbus_message() - elif ev is exception: - # The exception was actually thrown, so we can get a traceback - contents = ''.join(traceback.format_exception(et, ev, etb)) - else: - # We don't have any traceback for it, e.g. - # async_err_cb(MyException('Failed to badger the mushroom')) - # see also https://bugs.freedesktop.org/show_bug.cgi?id=12403 - contents = ''.join(traceback.format_exception_only(exception.__class__, - exception)) - reply = ErrorMessage(message, name, contents) - - connection.send_message(reply) - - -class InterfaceType(type): - - def __new__(cls, name, bases, dct): - # Properties require the PropertiesInterface base. - for func in dct.values(): - if isinstance(func, dbus_property): - for b in bases: - if issubclass(b, PropertiesInterface): - break - else: - bases += (PropertiesInterface,) - break - - interface_table = dct.setdefault('_dbus_interface_table', {}) - - # merge all the name -> method tables for all the interfaces - # implemented by our base classes into our own - for b in bases: - base_interface_table = getattr(b, '_dbus_interface_table', False) - if base_interface_table: - for (interface, method_table) in base_interface_table.items(): - our_method_table = interface_table.setdefault(interface, {}) - our_method_table.update(method_table) - - # add in all the name -> method entries for our own methods/signals - for func in dct.values(): - if getattr(func, '_dbus_interface', False): - method_table = interface_table.setdefault(func._dbus_interface, {}) - method_table[func.__name__] = func - - return type.__new__(cls, name, bases, dct) - - # methods are different to signals and properties, so we have three functions... :) - def _reflect_on_method(cls, func): - args = func._dbus_args - - if func._dbus_in_signature: - # convert signature into a tuple so length refers to number of - # types, not number of characters. the length is checked by - # the decorator to make sure it matches the length of args. - in_sig = tuple(Signature(func._dbus_in_signature)) - else: - # magic iterator which returns as many v's as we need - in_sig = _VariantSignature() - - if func._dbus_out_signature: - out_sig = Signature(func._dbus_out_signature) - else: - # its tempting to default to Signature('v'), but - # for methods that return nothing, providing incorrect - # introspection data is worse than providing none at all - out_sig = [] - - reflection_data = ' \n' % (func.__name__) - for pair in zip(in_sig, args): - reflection_data += ' \n' % pair - for type in out_sig: - reflection_data += ' \n' % type - reflection_data += ' \n' - - return reflection_data - - def _reflect_on_signal(cls, func): - args = func._dbus_args - - if func._dbus_signature: - # convert signature into a tuple so length refers to number of - # types, not number of characters - sig = tuple(Signature(func._dbus_signature)) - else: - # magic iterator which returns as many v's as we need - sig = _VariantSignature() - - reflection_data = ' \n' % (func.__name__) - for pair in zip(sig, args): - reflection_data = reflection_data + ' \n' % pair - reflection_data = reflection_data + ' \n' - - return reflection_data - - def _reflect_on_property(cls, descriptor): - signature = descriptor._dbus_signature - if signature is None: - signature = 'v' - - if descriptor.fget: - if descriptor.fset: - access = "readwrite" - else: - access = "read" - elif descriptor.fset: - access = "write" - else: - return "" - reflection_data = ' ' % (self.__class__.__module__, - self.__class__.__name__, where, - id(self)) - __str__ = __repr__ - - -class FallbackObject(Object): - """An object that implements an entire subtree of the object-path - tree. - - :Since: 0.82.0 - """ - - SUPPORTS_MULTIPLE_OBJECT_PATHS = True - - def __init__(self, conn=None, object_path=None): - """Constructor. - - Note that the superclass' ``bus_name`` __init__ argument is not - supported here. - - :Parameters: - `conn` : dbus.connection.Connection or None - The connection on which to export this object. If this is not - None, an `object_path` must also be provided. - - If None, the object is not initially available on any - Connection. - - `object_path` : str or None - A D-Bus object path at which to make this Object available - immediately. If this is not None, a `conn` must also be - provided. - - This object will implements all object-paths in the subtree - starting at this object-path, except where a more specific - object has been added. - """ - super(FallbackObject, self).__init__() - self._fallback = True - - if conn is None: - if object_path is not None: - raise TypeError('If object_path is given, conn is required') - elif object_path is None: - raise TypeError('If conn is given, object_path is required') - else: - self.add_to_connection(conn, object_path)