This commit is contained in:
Kovid Goyal 2023-04-30 20:29:36 +05:30
commit 5d1e886cf9
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 98 additions and 63 deletions

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
from functools import partial
from zipfile import ZipFile
from qt.core import (QToolButton, QAction, QIcon, QObject, QMenu,
from qt.core import (QToolButton, QAction, QIcon, QObject, QMenu, QPoint,
QKeySequence)
from calibre import prints
@ -18,6 +18,62 @@ from calibre.gui2.keyboard import NameConflict
from polyglot.builtins import string_or_bytes
def toolbar_widgets_for_action(gui, action):
# Search the the toolbars for the widget associated with an action, passing
# them to the caller for further processing
for x in gui.bars_manager.bars:
try:
w = x.widgetForAction(action)
# It seems that multiple copies of the action can exist, such as
# when the device-connected menu is changed while the device is
# connected. Use the one that has an actual position.
if w is None or w.pos().x() == 0:
continue
# The button might be hidden
if not w.isVisible():
continue
yield(w)
except Exception:
continue
def show_menu_under_widget(gui, menu, action, name):
# First try the tool bar
for w in toolbar_widgets_for_action(gui, action):
try:
# The w.height() assures that the menu opens below the button.
menu.exec(w.mapToGlobal(QPoint(0, w.height())))
return
except Exception:
continue
# Now try the menu bar
for x in gui.bars_manager.menu_bar.added_actions:
# This depends on no two menus with the same name.
# I don't know if this works on a Mac
if x.text() == name:
try:
# The menu item might be hidden
if not x.isVisible():
continue
# We can't use x.trigger() because it doesn't put the menu
# in the right place. Instead get the position of the menu
# widget on the menu bar
p = x.parent().menu_bar
r = p.actionGeometry(x)
# Make sure that the menu item is actually displayed in the menu
# and not the overflow
if p.geometry().width() < (r.x() + r.width()):
continue
# Show the menu under the name in the menu bar
menu.exec(p.mapToGlobal(QPoint(r.x()+2, r.height()-2)))
return
except Exception:
continue
# No visible button found. Fall back to displaying in upper left corner
# of the library view.
menu.exec(gui.library_view.mapToGlobal(QPoint(10, 10)))
def menu_action_unique_name(plugin, unique_name):
return '%s : menu action : %s'%(plugin.unique_name, unique_name)

View File

@ -4,7 +4,7 @@
from qt.core import QMenu, QToolButton
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.actions import InterfaceAction, show_menu_under_widget
class ManageCategoriesAction(InterfaceAction):
@ -39,7 +39,6 @@ class ManageCategoriesAction(InterfaceAction):
# show the menu in the upper left corner of the library view pane. Yes, this
# is a bit weird but it works as well as a popping up a dialog.
def show_menu(self):
from calibre.gui2.actions.saved_searches import show_menu_under_widget
show_menu_under_widget(self.gui, self.menu, self.qaction, self.name)
def about_to_show_menu(self):

View File

@ -2,55 +2,9 @@
# License: GPLv3 Copyright: 2022, Charles Haley
#
from qt.core import QPoint, QMenu, QToolButton
from qt.core import QMenu, QToolButton
from calibre.gui2.actions import InterfaceAction
def show_menu_under_widget(gui, menu, action, name):
# First try the tool bar
for x in gui.bars_manager.bars:
try:
w = x.widgetForAction(action)
# It seems that multiple copies of the action can exist, such as
# when the device-connected menu is changed while the device is
# connected. Use the one that has an actual position.
if w is None or w.pos().x() == 0:
continue
# The button might be hidden
if not w.isVisible():
continue
# The w.height() assures that the menu opens below the button.
menu.exec(w.mapToGlobal(QPoint(0, w.height())))
return
except Exception:
continue
# Now try the menu bar
for x in gui.bars_manager.menu_bar.added_actions:
# This depends on no two menus with the same name.
# I don't know if this works on a Mac
if x.text() == name:
try:
# The menu item might be hidden
if not x.isVisible():
continue
# We can't use x.trigger() because it doesn't put the menu
# in the right place. Instead get the position of the menu
# widget on the menu bar
p = x.parent().menu_bar
r = p.actionGeometry(x)
# Make sure that the menu item is actually displayed in the menu
# and not the overflow
if p.geometry().width() < (r.x() + r.width()):
continue
# Show the menu under the name in the menu bar
menu.exec(p.mapToGlobal(QPoint(r.x()+2, r.height()-2)))
return
except Exception:
continue
# No visible button found. Fall back to displaying in upper left corner
# of the library view.
menu.exec(gui.library_view.mapToGlobal(QPoint(10, 10)))
from calibre.gui2.actions import InterfaceAction, show_menu_under_widget
class SavedSearchesAction(InterfaceAction):

View File

@ -8,10 +8,10 @@ from contextlib import suppress
from functools import partial
from qt.core import (
QAbstractItemView, QAction, QDialog, QDialogButtonBox, QIcon, QListWidget,
QListWidgetItem, QSize, Qt, QToolButton, QVBoxLayout, pyqtSignal,
QListWidgetItem, QMenu, QSize, Qt, QToolButton, QVBoxLayout, pyqtSignal,
)
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.actions import InterfaceAction, show_menu_under_widget, toolbar_widgets_for_action
from calibre.library.field_metadata import category_icon_map
from calibre.utils.icu import primary_sort_key
from polyglot.builtins import iteritems
@ -40,13 +40,25 @@ class SortByAction(InterfaceAction):
name = 'Sort By'
action_spec = (_('Sort by'), 'sort.png', _('Sort the list of books'), None)
action_type = 'current'
popup_type = QToolButton.ToolButtonPopupMode.InstantPopup
popup_type = QToolButton.ToolButtonPopupMode.MenuButtonPopup
action_add_menu = True
dont_add_to = frozenset(('context-menu-cover-browser', ))
def genesis(self):
self.sorted_icon = QIcon.ic('ok.png')
self.qaction.menu().aboutToShow.connect(self.about_to_show)
self.qaction.triggered.connect(self.show_menu)
# Create a "hidden" menu that can have a shortcut. This also lets us
# manually show the menu instead of letting Qt do it to work around a
# problem where Qt can show the menu on the wrong screen.
self.hidden_menu = QMenu()
self.shortcut_action = self.create_menu_action(
menu=self.hidden_menu,
unique_name=_('Sort by'),
text=_('Show the Sort by menu'),
icon=None,
shortcut='Ctrl+F5',
triggered=self.show_menu)
def c(attr, title, tooltip, callback, keys=()):
ac = self.create_action(spec=(title, None, tooltip, keys), attr=attr)
@ -57,6 +69,12 @@ class SortByAction(InterfaceAction):
c('reverse_sort_action', _('Reverse current sort'), _('Reverse the current sort order'), self.reverse_sort, 'shift+f5')
c('reapply_sort_action', _('Re-apply current sort'), _('Re-apply the current sort'), self.reapply_sort, 'f5')
def show_menu(self):
# We manually show the menu instead of letting Qt do it to work around a
# problem where the menu can show on the wrong screen.
self.update_menu()
show_menu_under_widget(self.gui, self.qaction.menu(), self.qaction, self.name)
def reverse_sort(self):
self.gui.current_view().reverse_sort()
@ -68,17 +86,20 @@ class SortByAction(InterfaceAction):
self.qaction.setEnabled(enabled)
self.menuless_qaction.setEnabled(enabled)
def about_to_show(self):
self.update_menu()
def library_changed(self, db):
self.update_menu()
def initialization_complete(self):
self.update_menu()
# Remove the down arrow from the buttons as they serve no purpose. They
# show exactly what clicking the button shows
for w in toolbar_widgets_for_action(self.gui, self.qaction):
# Not sure why both styles are necessary, but they are
w.setStyleSheet('QToolButton::menu-button {image: none; }'
'QToolButton::menu-arrow {image: none; }')
def update_menu(self, menu=None):
menu = self.qaction.menu() if menu is None else menu
def update_menu(self):
menu = self.qaction.menu()
for action in menu.actions():
if hasattr(action, 'sort_requested'):
action.sort_requested.disconnect()
@ -96,17 +117,22 @@ class SortByAction(InterfaceAction):
menu.addAction(name, partial(self.named_sort_selected, saved_sorts[name]))
menu.addSeparator()
# Note the current sort column so it can be specially handled below
try:
sort_col, order = m.sorted_on
except TypeError:
sort_col, order = 'date', True
fm = db.field_metadata
name_map = {v:k for k, v in iteritems(fm.ui_sortable_field_keys())}
hidden = frozenset(db.new_api.pref(SORT_HIDDEN_PREF, default=()) or ())
# The operations to choose which columns to display and to create saved sorts
menu.addAction(_('Select sortable columns')).triggered.connect(self.select_sortable_columns)
menu.addAction(_('Sort on multiple columns'), self.choose_multisort)
menu.addSeparator()
# Add the columns to the menu
fm = db.field_metadata
name_map = {v:k for k, v in iteritems(fm.ui_sortable_field_keys())}
all_names = sorted(name_map, key=primary_sort_key)
hidden = frozenset(db.new_api.pref(SORT_HIDDEN_PREF, default=()) or ())
for name in all_names:
key = name_map[name]