mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
009e82c9df
commit
efdf661457
@ -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
|
||||
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)
|
||||
|
@ -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
|
||||
|
@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
|
||||
|
@ -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_()
|
@ -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))
|
@ -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()
|
@ -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
|
@ -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()
|
@ -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
|
@ -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)
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user