From 8ea1a6c6cbbb7ee8180daf1b76a3b343f8ca18b1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 15 Jun 2014 18:23:53 +0530 Subject: [PATCH] Workaround for broken handling of menus on OSX in Qt 5 --- src/calibre/gui2/actions/choose_library.py | 3 + src/calibre/gui2/bars.py | 236 +++++++++++++++++---- src/calibre/gui2/init.py | 13 +- src/calibre/gui2/main_window.py | 8 +- 4 files changed, 202 insertions(+), 58 deletions(-) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 6ed21ff75a..7f02b88176 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -359,6 +359,9 @@ class ChooseLibraryAction(InterfaceAction): self.gui.location_manager.set_switch_actions(quick_actions, rename_actions, delete_actions, qs_actions, self.action_choose) + # Allow the cloned actions in the OS X global menubar to update + for a in (self.qaction, self.menuless_qaction): + a.changed.emit() def location_selected(self, loc): enabled = loc == 'library' diff --git a/src/calibre/gui2/bars.py b/src/calibre/gui2/bars.py index 93ea2a1e96..2871845057 100644 --- a/src/calibre/gui2/bars.py +++ b/src/calibre/gui2/bars.py @@ -7,10 +7,11 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' - +import sip from PyQt5.Qt import (Qt, QAction, QMenu, QMenuBar, QObject, - QToolBar, QToolButton, QSize) + QToolBar, QToolButton, QSize, pyqtSignal, QTimer) +from calibre.constants import isosx from calibre.gui2.throbber import create_donate_widget from calibre.gui2 import gprefs @@ -204,61 +205,203 @@ class MenuAction(QAction): # {{{ self.setText(self.clone.text()) # }}} -class MenuBar(QMenuBar): # {{{ +# MenuBar {{{ - def __init__(self, location_manager, parent): - QMenuBar.__init__(self, parent) - self.gui = parent - self.setNativeMenuBar(True) +if isosx: + # On OS X we need special handling for the application global menu bar and + # the context menus, since Qt does not handle dynamic menus or menus in + # which the same action occurs in more than one place. - self.location_manager = location_manager - self.added_actions = [] + class CloneAction(QAction): - self.donate_action = QAction(_('Donate'), self) - self.donate_menu = QMenu() - self.donate_menu.addAction(self.gui.donate_action) - self.donate_action.setMenu(self.donate_menu) + text_changed = pyqtSignal() + visibility_changed = pyqtSignal() - def update_lm_actions(self): - for ac in self.added_actions: - clone = getattr(ac, 'clone', None) - if clone is not None and clone in self.location_manager.all_actions: - ac.setVisible(clone in self.location_manager.available_actions) + def __init__(self, clone, parent, is_top_level=False, clone_shortcuts=True): + QAction.__init__(self, clone.text(), parent) + self.is_top_level = is_top_level + self.clone_shortcuts = clone_shortcuts + self.clone = clone + clone.changed.connect(self.clone_changed) + self.clone_changed() + self.triggered.connect(self.do_trigger) - def init_bar(self, actions): - for ac in self.added_actions: - m = ac.menu() - if m is not None: - m.setVisible(False) + def clone_changed(self): + otext = self.text() + self.setText(self.clone.text()) + if otext != self.text: + self.text_changed.emit() + ov = self.isVisible() + self.setVisible(self.clone.isVisible()) + if ov != self.isVisible(): + self.visibility_changed.emit() + self.setEnabled(self.clone.isEnabled()) + self.setCheckable(self.clone.isCheckable()) + self.setChecked(self.clone.isChecked()) + self.setIcon(self.clone.icon()) + if self.clone_shortcuts: + self.setShortcuts(self.clone.shortcuts()) + if self.clone.menu() is None: + if not self.is_top_level: + self.setMenu(None) + else: + m = QMenu(self.text(), self.parent()) + for ac in QMenu.actions(self.clone.menu()): + if ac.isSeparator(): + m.addSeparator() + else: + m.addAction(CloneAction(ac, self.parent(), clone_shortcuts=self.clone_shortcuts)) + self.setMenu(m) - self.clear() - self.added_actions = [] + def do_trigger(self, checked=False): + if not sip.isdeleted(self.clone): + self.clone.trigger() - for what in actions: + def populate_menu(m, items, iactions): + for what in items: if what is None: - continue - elif what == 'Location Manager': - for ac in self.location_manager.all_actions: - ac = self.build_menu(ac) + m.addSeparator() + elif what in iactions: + m.addAction(CloneAction(iactions[what].qaction, m)) + + class MenuBar(QObject): + + @property + def native_menubar(self): + return self.gui.native_menubar + + def __init__(self, location_manager, parent): + QObject.__init__(self, parent) + self.gui = parent + + self.location_manager = location_manager + self.added_actions = [] + self.last_actions = [] + + self.donate_action = QAction(_('Donate'), self) + self.donate_menu = QMenu() + self.donate_menu.addAction(self.gui.donate_action) + self.donate_action.setMenu(self.donate_menu) + self.refresh_timer = t = QTimer(self) + t.setInterval(200), t.setSingleShot(True), t.timeout.connect(self.refresh_bar) + + def init_bar(self, actions): + self.last_actions = actions + for ac in self.added_actions: + m = ac.menu() + if m is not None: + m.setVisible(False) + + mb = self.native_menubar + for ac in self.added_actions: + mb.removeAction(ac) + if ac is not self.donate_action: + ac.setMenu(None) + ac.deleteLater() + self.added_actions = [] + + for what in actions: + if what is None: + continue + elif what == 'Location Manager': + for ac in self.location_manager.available_actions: + self.build_menu(ac, ac.isVisible()) + elif what == 'Donate': + mb.addAction(self.donate_action) + elif what in self.gui.iactions: + action = self.gui.iactions[what] + self.build_menu(action.qaction) + + def build_menu(self, ac, visible=True): + ans = CloneAction(ac, self.native_menubar, is_top_level=True) + ans.setVisible(visible) + if ans.menu() is None: + m = QMenu() + m.addAction(CloneAction(ac, self.native_menubar)) + ans.setMenu(m) + # Qt (as of 5.3.0) does not update global menubar entries + # correctly, so we have to rebuild the global menubar. + # Without this the Choose Library action shows the text + # 'Untitled' and the Location Manager items do not work. + ans.text_changed.connect(self.refresh_timer.start) + ans.visibility_changed.connect(self.refresh_timer.start) + self.native_menubar.addAction(ans) + self.added_actions.append(ans) + return ans + + def setVisible(self, yes): + pass # no-op on OS X since menu bar is always visible + + def update_lm_actions(self): + pass # no-op as this is taken care of by init_bar() + + def refresh_bar(self): + self.init_bar(self.last_actions) + +else: + + def populate_menu(m, items, iactions): + for what in items: + if what is None: + m.addSeparator() + elif what in iactions: + m.addAction(iactions[what].qaction) + + class MenuBar(QMenuBar): + + def __init__(self, location_manager, parent): + QMenuBar.__init__(self, parent) + parent.setMenuBar(self) + self.gui = parent + + self.location_manager = location_manager + self.added_actions = [] + + self.donate_action = QAction(_('Donate'), self) + self.donate_menu = QMenu() + self.donate_menu.addAction(self.gui.donate_action) + self.donate_action.setMenu(self.donate_menu) + + def init_bar(self, actions): + for ac in self.added_actions: + m = ac.menu() + if m is not None: + m.setVisible(False) + + self.clear() + self.added_actions = [] + + for what in actions: + if what is None: + continue + elif what == 'Location Manager': + for ac in self.location_manager.all_actions: + ac = self.build_menu(ac) + self.addAction(ac) + self.added_actions.append(ac) + ac.setVisible(False) + elif what == 'Donate': + self.addAction(self.donate_action) + elif what in self.gui.iactions: + action = self.gui.iactions[what] + ac = self.build_menu(action.qaction) self.addAction(ac) self.added_actions.append(ac) - ac.setVisible(False) - elif what == 'Donate': - self.addAction(self.donate_action) - elif what in self.gui.iactions: - action = self.gui.iactions[what] - ac = self.build_menu(action.qaction) - self.addAction(ac) - self.added_actions.append(ac) - def build_menu(self, action): - m = action.menu() - ac = MenuAction(action, self) - if m is None: - m = QMenu() - m.addAction(action) - ac.setMenu(m) - return ac + def build_menu(self, action): + m = action.menu() + ac = MenuAction(action, self) + if m is None: + m = QMenu() + m.addAction(action) + ac.setMenu(m) + return ac + + def update_lm_actions(self): + for ac in self.added_actions: + clone = getattr(ac, 'clone', None) + if clone is not None and clone in self.location_manager.all_actions: + ac.setVisible(clone in self.location_manager.available_actions) # }}} @@ -275,7 +418,6 @@ class BarsManager(QObject): self.child_bars = tuple(bars[2:]) self.menu_bar = MenuBar(self.location_manager, self.parent()) - self.parent().setMenuBar(self.menu_bar) self.apply_settings() self.init_bars() diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 632c38cd7b..ecf6cdda86 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -70,16 +70,11 @@ class LibraryViewMixin(object): # {{{ self.library_view.model().set_highlight_only(config['highlight_search_matches']) def build_context_menus(self): + from calibre.gui2.bars import populate_menu lm = QMenu(self) - def populate_menu(m, items): - for what in items: - if what is None: - m.addSeparator() - elif what in self.iactions: - m.addAction(self.iactions[what].qaction) - populate_menu(lm, gprefs['action-layout-context-menu']) + populate_menu(lm, gprefs['action-layout-context-menu'], self.iactions) dm = QMenu(self) - populate_menu(dm, gprefs['action-layout-context-menu-device']) + populate_menu(dm, gprefs['action-layout-context-menu-device'], self.iactions) ec = self.iactions['Edit Collections'].qaction self.library_view.set_context_menu(lm, ec) for v in (self.memory_view, self.card_a_view, self.card_b_view): @@ -88,7 +83,7 @@ class LibraryViewMixin(object): # {{{ if self.cover_flow is not None: cm = QMenu(self.cover_flow) populate_menu(cm, - gprefs['action-layout-context-menu-cover-browser']) + gprefs['action-layout-context-menu-cover-browser'], self.iactions) self.cover_flow.set_context_menu(cm) def search_done(self, view, ok): diff --git a/src/calibre/gui2/main_window.py b/src/calibre/gui2/main_window.py index 8bd466dede..e446a9bf8f 100644 --- a/src/calibre/gui2/main_window.py +++ b/src/calibre/gui2/main_window.py @@ -43,10 +43,10 @@ class GarbageCollector(QObject): self.threshold = gc.get_threshold() gc.disable() self.timer.start(self.INTERVAL) - #gc.set_debug(gc.DEBUG_SAVEALL) + # gc.set_debug(gc.DEBUG_SAVEALL) def check(self): - #return self.debug_cycles() + # return self.debug_cycles() l0, l1, l2 = gc.get_count() if self.debug: print ('gc_check called:', l0, l1, l2) @@ -97,6 +97,10 @@ class MainWindow(QMainWindow): quit_action.setMenuRole(QAction.QuitRole) return preferences_action, quit_action + @property + def native_menubar(self): + return self.___menu_bar + def __init__(self, opts, parent=None, disable_automatic_gc=False): QMainWindow.__init__(self, parent) if disable_automatic_gc: