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]
|
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):
|
if full_module_name in exclude_modules or ('.' in full_module_name and full_module_name.rpartition('.')[0] in exclude_packages):
|
||||||
continue
|
continue
|
||||||
try:
|
|
||||||
importlib.import_module(full_module_name)
|
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
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
def test_import_of_all_python_modules(self):
|
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_packages = {'calibre.devices.mtp.unix.upstream'}
|
||||||
|
exclude_modules = set()
|
||||||
if not iswindows:
|
if not iswindows:
|
||||||
exclude_modules |= {'calibre.utils.iphlpapi', 'calibre.utils.open_with.windows', 'calibre.devices.winusb'}
|
exclude_modules |= {'calibre.utils.iphlpapi', 'calibre.utils.open_with.windows', 'calibre.devices.winusb'}
|
||||||
exclude_packages |= {'calibre.utils.winreg', 'calibre.utils.windows'}
|
exclude_packages |= {'calibre.utils.winreg', 'calibre.utils.windows'}
|
||||||
@ -52,11 +48,10 @@ class TestImports(unittest.TestCase):
|
|||||||
exclude_modules.add('calibre.utils.open_with.osx')
|
exclude_modules.add('calibre.utils.open_with.osx')
|
||||||
if not islinux:
|
if not islinux:
|
||||||
exclude_modules |= {
|
exclude_modules |= {
|
||||||
'calibre.utils.dbus_service', 'calibre.linux',
|
'calibre.linux',
|
||||||
'calibre.utils.linux_trash', 'calibre.utils.open_with.linux',
|
'calibre.utils.linux_trash', 'calibre.utils.open_with.linux',
|
||||||
'calibre.gui2.linux_file_dialogs'
|
'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)
|
self.assertGreater(self.base_check(os.path.join(SRC, 'odf'), exclude_packages, exclude_modules), 10)
|
||||||
base = os.path.join(SRC, 'calibre')
|
base = os.path.join(SRC, 'calibre')
|
||||||
self.assertGreater(self.base_check(base, exclude_packages, exclude_modules), 1000)
|
self.assertGreater(self.base_check(base, exclude_packages, exclude_modules), 1000)
|
||||||
|
@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from qt.core import (
|
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)
|
QTimer, QPropertyAnimation, QEasingCurve, pyqtProperty, QPainter, QWidget, QPalette, sip)
|
||||||
|
|
||||||
from calibre.constants import ismacos
|
from calibre.constants import ismacos
|
||||||
@ -533,17 +533,15 @@ else:
|
|||||||
ac = ia.shortcut_action_for_context_menu
|
ac = ia.shortcut_action_for_context_menu
|
||||||
m.addAction(ac)
|
m.addAction(ac)
|
||||||
|
|
||||||
from calibre.gui2.dbus_export.widgets import factory
|
|
||||||
|
|
||||||
class MenuBar(QObject):
|
class MenuBar(QObject):
|
||||||
|
|
||||||
is_native_menubar = False
|
is_native_menubar = False
|
||||||
|
|
||||||
def __init__(self, location_manager, parent):
|
def __init__(self, location_manager, parent):
|
||||||
QObject.__init__(self, parent)
|
QObject.__init__(self, parent)
|
||||||
f = factory(app_id='com.calibre-ebook.gui')
|
self.menu_bar = QMenuBar(parent)
|
||||||
self.menu_bar = f.create_window_menubar(parent)
|
self.menu_bar.is_native_menubar = False
|
||||||
self.is_native_menubar = self.menu_bar.is_native_menubar
|
parent.setMenuBar(self.menu_bar)
|
||||||
self.gui = parent
|
self.gui = parent
|
||||||
|
|
||||||
self.location_manager = location_manager
|
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 (
|
from qt.core import (
|
||||||
QAction, QApplication, QColor, QDockWidget, QEvent, QHBoxLayout, QIcon, QImage,
|
QAction, QApplication, QColor, QDockWidget, QEvent, QHBoxLayout, QIcon, QImage,
|
||||||
QLabel, QMenu, QPalette, QPixmap, QSize, QStackedWidget, Qt, QTabWidget, QTimer,
|
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
|
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.customize.ui import find_plugin
|
||||||
from calibre.gui2 import elided_text, open_url
|
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.keyboard import Manager as KeyboardManager
|
||||||
from calibre.gui2.main_window import MainWindow
|
from calibre.gui2.main_window import MainWindow
|
||||||
from calibre.gui2.throbber import ThrobbingButton
|
from calibre.gui2.throbber import ThrobbingButton
|
||||||
@ -618,8 +617,9 @@ class Main(MainWindow):
|
|||||||
p, q = self.create_application_menubar()
|
p, q = self.create_application_menubar()
|
||||||
q.triggered.connect(self.action_quit.trigger)
|
q.triggered.connect(self.action_quit.trigger)
|
||||||
p.triggered.connect(self.action_preferences.trigger)
|
p.triggered.connect(self.action_preferences.trigger)
|
||||||
f = factory(app_id='com.calibre-ebook.EditBook-%d' % os.getpid())
|
b = QMenuBar(self)
|
||||||
b = f.create_window_menubar(self)
|
self.setMenuBar(b)
|
||||||
|
b.is_native_menubar = False
|
||||||
|
|
||||||
f = b.addMenu(_('&File'))
|
f = b.addMenu(_('&File'))
|
||||||
f.addAction(self.action_new_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.auto_add import AutoAdder
|
||||||
from calibre.gui2.changes import handle_changes
|
from calibre.gui2.changes import handle_changes
|
||||||
from calibre.gui2.cover_flow import CoverFlowMixin
|
from calibre.gui2.cover_flow import CoverFlowMixin
|
||||||
from calibre.gui2.dbus_export.widgets import factory
|
|
||||||
from calibre.gui2.device import DeviceMixin
|
from calibre.gui2.device import DeviceMixin
|
||||||
from calibre.gui2.dialogs.message_box import JobError
|
from calibre.gui2.dialogs.message_box import JobError
|
||||||
from calibre.gui2.ebook_download import EbookDownloadMixin
|
from calibre.gui2.ebook_download import EbookDownloadMixin
|
||||||
@ -248,9 +247,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
self.viewers = deque()
|
self.viewers = deque()
|
||||||
self.system_tray_icon = None
|
self.system_tray_icon = None
|
||||||
do_systray = config['systray_icon'] or opts.start_in_tray
|
do_systray = config['systray_icon'] or opts.start_in_tray
|
||||||
if do_systray:
|
if do_systray and QSystemTrayIcon.isSystemTrayAvailable():
|
||||||
self.system_tray_icon = factory(app_id='com.calibre-ebook.gui').create_system_tray_icon(parent=self, title='calibre')
|
self.system_tray_icon = QSystemTrayIcon(self)
|
||||||
if self.system_tray_icon is not None:
|
|
||||||
self.system_tray_icon.setIcon(QIcon(I('lt.png', allow_user_override=False)))
|
self.system_tray_icon.setIcon(QIcon(I('lt.png', allow_user_override=False)))
|
||||||
if not (iswindows or ismacos):
|
if not (iswindows or ismacos):
|
||||||
self.system_tray_icon.setIcon(QIcon.fromTheme('calibre-tray', self.system_tray_icon.icon()))
|
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)
|
self.jobs_button.tray_tooltip_updated.connect(self.system_tray_icon.setToolTip)
|
||||||
elif do_systray:
|
elif do_systray:
|
||||||
prints('Failed to create system tray icon, your desktop environment probably'
|
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.system_tray_menu = QMenu(self)
|
||||||
self.toggle_to_tray_action = self.system_tray_menu.addAction(QIcon(I('page.png')), '')
|
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)
|
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