Workaround for broken handling of menus on OSX in Qt 5

This commit is contained in:
Kovid Goyal 2014-06-15 18:23:53 +05:30
parent 500b05da83
commit 8ea1a6c6cb
4 changed files with 202 additions and 58 deletions

View File

@ -359,6 +359,9 @@ class ChooseLibraryAction(InterfaceAction):
self.gui.location_manager.set_switch_actions(quick_actions, self.gui.location_manager.set_switch_actions(quick_actions,
rename_actions, delete_actions, qs_actions, rename_actions, delete_actions, qs_actions,
self.action_choose) 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): def location_selected(self, loc):
enabled = loc == 'library' enabled = loc == 'library'

View File

@ -7,10 +7,11 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import sip
from PyQt5.Qt import (Qt, QAction, QMenu, QMenuBar, QObject, 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.throbber import create_donate_widget
from calibre.gui2 import gprefs from calibre.gui2 import gprefs
@ -204,12 +205,154 @@ class MenuAction(QAction): # {{{
self.setText(self.clone.text()) self.setText(self.clone.text())
# }}} # }}}
class MenuBar(QMenuBar): # {{{ # MenuBar {{{
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.
class CloneAction(QAction):
text_changed = pyqtSignal()
visibility_changed = pyqtSignal()
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 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)
def do_trigger(self, checked=False):
if not sip.isdeleted(self.clone):
self.clone.trigger()
def populate_menu(m, items, iactions):
for what in items:
if what is None:
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): def __init__(self, location_manager, parent):
QMenuBar.__init__(self, parent) QMenuBar.__init__(self, parent)
parent.setMenuBar(self)
self.gui = parent self.gui = parent
self.setNativeMenuBar(True)
self.location_manager = location_manager self.location_manager = location_manager
self.added_actions = [] self.added_actions = []
@ -219,12 +362,6 @@ class MenuBar(QMenuBar): # {{{
self.donate_menu.addAction(self.gui.donate_action) self.donate_menu.addAction(self.gui.donate_action)
self.donate_action.setMenu(self.donate_menu) self.donate_action.setMenu(self.donate_menu)
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_bar(self, actions): def init_bar(self, actions):
for ac in self.added_actions: for ac in self.added_actions:
m = ac.menu() m = ac.menu()
@ -260,6 +397,12 @@ class MenuBar(QMenuBar): # {{{
ac.setMenu(m) ac.setMenu(m)
return ac 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)
# }}} # }}}
class BarsManager(QObject): class BarsManager(QObject):
@ -275,7 +418,6 @@ class BarsManager(QObject):
self.child_bars = tuple(bars[2:]) self.child_bars = tuple(bars[2:])
self.menu_bar = MenuBar(self.location_manager, self.parent()) self.menu_bar = MenuBar(self.location_manager, self.parent())
self.parent().setMenuBar(self.menu_bar)
self.apply_settings() self.apply_settings()
self.init_bars() self.init_bars()

View File

@ -70,16 +70,11 @@ class LibraryViewMixin(object): # {{{
self.library_view.model().set_highlight_only(config['highlight_search_matches']) self.library_view.model().set_highlight_only(config['highlight_search_matches'])
def build_context_menus(self): def build_context_menus(self):
from calibre.gui2.bars import populate_menu
lm = QMenu(self) lm = QMenu(self)
def populate_menu(m, items): populate_menu(lm, gprefs['action-layout-context-menu'], self.iactions)
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'])
dm = QMenu(self) 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 ec = self.iactions['Edit Collections'].qaction
self.library_view.set_context_menu(lm, ec) self.library_view.set_context_menu(lm, ec)
for v in (self.memory_view, self.card_a_view, self.card_b_view): 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: if self.cover_flow is not None:
cm = QMenu(self.cover_flow) cm = QMenu(self.cover_flow)
populate_menu(cm, 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) self.cover_flow.set_context_menu(cm)
def search_done(self, view, ok): def search_done(self, view, ok):

View File

@ -97,6 +97,10 @@ class MainWindow(QMainWindow):
quit_action.setMenuRole(QAction.QuitRole) quit_action.setMenuRole(QAction.QuitRole)
return preferences_action, quit_action return preferences_action, quit_action
@property
def native_menubar(self):
return self.___menu_bar
def __init__(self, opts, parent=None, disable_automatic_gc=False): def __init__(self, opts, parent=None, disable_automatic_gc=False):
QMainWindow.__init__(self, parent) QMainWindow.__init__(self, parent)
if disable_automatic_gc: if disable_automatic_gc: