diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 9f3122750e..297169a7e5 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1097,6 +1097,12 @@ class ActionBooklistContextMenu(InterfaceActionBase): description = _('Open the context menu for the column') +class ActionAllActions(InterfaceActionBase): + name = 'All GUI actions' + author = 'Charles Haley' + actual_plugin = 'calibre.gui2.actions.all_actions:AllGUIActions' + description = _('Open a menu showing all installed GUI actions') + class ActionVirtualLibrary(InterfaceActionBase): name = 'Virtual Library' actual_plugin = 'calibre.gui2.actions.virtual_library:VirtualLibraryAction' @@ -1128,7 +1134,7 @@ class ActionPluginUpdater(InterfaceActionBase): actual_plugin = 'calibre.gui2.actions.plugin_updates:PluginUpdaterAction' -plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, +plugins += [ActionAdd, ActionAllActions, ActionFetchAnnotations, ActionGenerateCatalog, ActionConvert, ActionDelete, ActionEditMetadata, ActionView, ActionFetchNews, ActionSaveToDisk, ActionQuickview, ActionPolish, ActionShowBookDetails,ActionRestart, ActionOpenFolder, ActionConnectShare, diff --git a/src/calibre/gui2/actions/all_actions.py b/src/calibre/gui2/actions/all_actions.py new file mode 100644 index 0000000000..2c436cc48a --- /dev/null +++ b/src/calibre/gui2/actions/all_actions.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2022, Charles Haley + +from functools import partial +from math import ceil + +from qt.core import QIcon, QMenu, QToolButton, Qt + +from calibre.gui2.actions import InterfaceAction, show_menu_under_widget +from calibre.gui2.preferences.toolbar import AllModel, CurrentModel +from calibre.utils.icu import sort_key + + +class AllGUIActions(InterfaceAction): + + name = 'All GUI actions' + action_spec = (_('All GUI actions'), 'wizard.png', + _("Show a menu of all available GUI and plugin actions.\nThis menu " + "is not available when looking at books on a device"), None) + + action_type = 'current' + popup_type = QToolButton.ToolButtonPopupMode.InstantPopup + action_add_menu = True + dont_add_to = frozenset() + + def genesis(self): + self.layout_icon = QIcon.ic('wizard.png') + self.menu = m = self.qaction.menu() + m.aboutToShow.connect(self.about_to_show_menu) + + # Create a "hidden" menu that can have a shortcut. + self.hidden_menu = QMenu() + self.shortcut_action = self.create_menu_action( + menu=self.hidden_menu, + unique_name='Main window layout', + shortcut=None, + text=_("Save and restore layout item sizes, and add/remove/toggle " + "layout items such as the search bar, tag browser, etc. "), + icon='layout.png', + triggered=self.show_menu) + + # We want to show the menu when a shortcut is used. Apparently the only way + # to do that is to scan the toolbar(s) for the action button then exec the + # associated menu. The search is done here to take adding and removing the + # action from toolbars into account. + # + # If a shortcut is triggered and there isn't a toolbar button visible then + # 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): + show_menu_under_widget(self.gui, self.menu, self.qaction, self.name) + + def gui_layout_complete(self): + self.qaction.menu().aboutToShow.connect(self.about_to_show_menu) + + def initialization_complete(self): + self.populate_menu() + + def about_to_show_menu(self): + self.populate_menu() + + def location_selected(self, loc): + self.qaction.setEnabled(loc == 'library') + + def populate_menu(self): + # Need to do this on every invocation because shortcuts can change + m = self.qaction.menu() + m.clear() + + name_data = {} # A dict of display names to actions data + + # Use model data from Preferences / Toolbars, with location 'toolbar' or + # 'toolbar-device' depending on whether a device is connected. + location = 'toolbar' + ('-device' if self.gui.location_manager.has_device else '') + for model in (AllModel(location, self.gui), CurrentModel(location, self.gui)): + for i in range(0, model.rowCount(None)): + dex = model.index(i) + name = model.names((dex,))[0] # this is the action name + if name is not None and not name.startswith('---'): + name_data[model.data(dex, Qt.ItemDataRole.DisplayRole)] = { + 'action': model.name_to_action(name, self.gui), + 'action_name':name, + 'icon': model.data(dex, Qt.ItemDataRole.DecorationRole)} + # The loop leaves the variable 'model' set to a valid value + + # Get display names of builtin and user plugins. We tell the difference + # using the class full module name. Plugins start with 'calibre_plugins' + builtin_actions = list() + user_plugins = list() + for display_name, act_data in name_data.items(): + act = model.name_to_action(act_data['action_name'], self.gui) + if act is not None: + module = f'{act.__module__}.{act.__class__ .__name__}' + if module.startswith('calibre_plugins'): + user_plugins.append(display_name) + else: + builtin_actions.append(display_name) + + builtin_actions = sorted(builtin_actions, key=sort_key) + user_plugins = sorted(user_plugins, key=sort_key) + + # Build the map of action shortcuts so we can display the shortcuts for + # the actions. For shortcuts sometimes the action name and sometimes the + # display name is used. Test both. Unfortunately, there are case + # differences to deal with so we use lower case keys. + lower_names = ({n['action_name'].lower() for n in name_data.values()} | + {n.lower() for n in name_data.keys()}) + kbd = self.gui.keyboard + shortcut_map = {} + for n,v in kbd.keys_map.items(): + act_name = kbd.shortcuts[n]['name'].lower() + if act_name in lower_names: + shortcuts = list((sc.toString() for sc in v)) + shortcut_map[act_name] = f'\t{", ".join(shortcuts)}' + + # This function constructs a menu action, dealing with the action being + # either a popup menu or a dialog. It adds the shortcuts to the menu + # line. Happily, a tab causes the shortcuts to be right-aligned. The tab + # is added when the shortcut map is built. + def add_action(menu, display_name): + shortcuts = shortcut_map.get(display_name.lower(), '') + act = name_data[display_name]['action'] + if not hasattr(act, 'popup_type'): # FakeAction + return + menu_text = f'{display_name}{shortcuts}' + icon = name_data[display_name]['icon'] + if act.popup_type == QToolButton.ToolButtonPopupMode.MenuButtonPopup: + if act.action_add_menu: + # The action offers both a 'click' and a menu. Use the menu. + menu.addAction(icon, menu_text, partial(self._do_menu, display_name, act)) + else: + # The action is a dialog. + menu.addAction(act.qaction.icon(), menu_text, partial(self._do_action, act)) + else: + # The action is a menu. + menu.addAction(icon, menu_text, partial(self._do_menu, display_name, act)) + + # Finally the real work, building the action menu. Partition long lists + # of actions into sublists of some arbitrary length. + def partition(names): + count_in_partition = 10 # arbitrary + if len(names) >= count_in_partition: + partition_count = len(names) // (count_in_partition - 1) + step = int(ceil(len(names) / partition_count)) + for first in range(0, len(names), step): + last = min(first + step - 1, len(names) - 1) + dnf = names[first] + dnl = names[last] + if dnf != dnl: + sm = m.addMenu(QIcon.ic('wizard.png'), f'{dnf} - {dnl}') + else: + sm = m.addMenu(QIcon.ic('wizard.png'), f'{dnf}') + for name in names[first:last+1]: + add_action(sm, name) + else: + for name in names: + act = self.gui.iactions[name] + add_action(m, name) + + # Add a named section for builtin actions if user plugins are installed. + if user_plugins: + m.addSection(_('Built-in calibre actions') + ' ') + partition(builtin_actions) + m.addSection(_('Plugins') + ' ') + partition(user_plugins) + else: + partition(builtin_actions) + # Add access to the toolbars and keyboard shortcuts preferences dialogs + m.addSection(_('Preferences') + ' ') + m.addAction(_('Toolbars'), self._do_pref_toolbar) + m.addAction(_('Keyboard shortcuts'), self._do_pref_shortcuts) + + def _do_pref_toolbar(self): + self.gui.iactions['Preferences'].do_config(initial_plugin=('Interface', 'Toolbar'), close_after_initial=True) + + def _do_pref_shortcuts(self): + self.gui.iactions['Preferences'].do_config(initial_plugin=('Advanced', 'Keyboard'), close_after_initial=True) + + def _do_action(self, action): + action.qaction.trigger() + + def _do_menu(self, name, action): + # This method clones and shows the action's menu. If the action is in a + # context menu, the action menu is displayed there without using this + # method. If not it is displayed underneath a toolbar or menu button. If + # neither is true then the menu is displayed in the upper left corner. + menu = QMenu() + menu.addSection(name) + action.qaction.menu().aboutToShow.emit() + for m in action.qaction.menu().actions(): + menu.addAction(m) + show_menu_under_widget(self.gui, menu, self.qaction, self.name) + diff --git a/src/calibre/gui2/actions/sort.py b/src/calibre/gui2/actions/sort.py index ff9a68cf18..864bc29ed0 100644 --- a/src/calibre/gui2/actions/sort.py +++ b/src/calibre/gui2/actions/sort.py @@ -40,13 +40,15 @@ 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.MenuButtonPopup + popup_type = QToolButton.ToolButtonPopupMode.InstantPopup action_add_menu = True dont_add_to = frozenset(('context-menu-cover-browser', )) def genesis(self): self.sorted_icon = QIcon.ic('ok.png') - self.qaction.triggered.connect(self.show_menu) + self.menu = m = self.qaction.menu() + m.aboutToShow.connect(self.about_to_show_menu) + # 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 @@ -69,10 +71,10 @@ 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. + def about_to_show_menu(self): self.update_menu() + + def show_menu(self): show_menu_under_widget(self.gui, self.qaction.menu(), self.qaction, self.name) def reverse_sort(self): @@ -91,12 +93,6 @@ class SortByAction(InterfaceAction): 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 = menu or self.qaction.menu()