From d9c18c86d2a46ed4b6e9b95b729d400f0a156d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Andr=C3=A9s=20Margffoy=20Tuay?= Date: Wed, 11 Aug 2021 16:23:30 -0500 Subject: [PATCH 1/6] Main menu: Use menu and item identifiers to add actions and menus --- spyder/api/widgets/menus.py | 37 ++++++++++++----- spyder/plugins/mainmenu/plugin.py | 68 +++++++++++++++++++------------ 2 files changed, 68 insertions(+), 37 deletions(-) diff --git a/spyder/api/widgets/menus.py b/spyder/api/widgets/menus.py index 864f4ddd23b..2c56aaf229b 100644 --- a/spyder/api/widgets/menus.py +++ b/spyder/api/widgets/menus.py @@ -10,12 +10,13 @@ # Standard library imports import sys +from typing import Optional, Union, TypeVar # Third party imports from qtpy.QtWidgets import QAction, QMenu # Local imports -from spyder.utils.qthelpers import add_actions +from spyder.utils.qthelpers import add_actions, SpyderAction # --- Constants @@ -23,6 +24,10 @@ MENU_SEPARATOR = None +# Generic type annotations +T = TypeVar('T', bound='SpyderMenu') + + class OptionsMenuSections: Top = 'top_section' Bottom = 'bottom_section' @@ -46,6 +51,7 @@ def __init__(self, parent=None, title=None, dynamic=True): self._title = title self._sections = [] self._actions = [] + self._actions_map = {} self.unintroduced_actions = {} self.unintroduced_sections = [] self._dirty = False @@ -74,11 +80,15 @@ def clear_actions(self): self.clear() self._sections = [] self._actions = [] + self._actions_map = {} self.unintroduced_actions = {} self.unintroduced_sections = [] - def add_action(self, action, section=None, before=None, - before_section=None, check_before=True): + def add_action(self: T, action: Union[SpyderAction, T], + action_id: str, section: Optional[str] = None, + before: Optional[str] = None, + before_section: Optional[str] = None, + check_before: bool = True): """ Add action to a given menu section. @@ -86,11 +96,13 @@ def add_action(self, action, section=None, before=None, ---------- action: SpyderAction The action to add. + action_id: str + An unique identifier to store the action under the current menu. section: str or None The section id in which to insert the `action`. - before: SpyderAction or None - Make the action appear before another given action. - before_section: Section or None + before: str + Make the action appear before the given action identifier. + before_section: str or None Make the item section (if provided) appear before another given section. check_before: bool @@ -103,8 +115,10 @@ def add_action(self, action, section=None, before=None, else: new_actions = [] added = False + before_item = self._actions_map.get(before, None) + for sec, act in self._actions: - if act == before: + if before_item is not None and act == before_item: added = True new_actions.append((section, action)) @@ -116,7 +130,7 @@ def add_action(self, action, section=None, before=None, # the menu is rendered. if not added and check_before: before_actions = self.unintroduced_actions.get(before, []) - before_actions.append((section, action)) + before_actions.append((section, action, action_id)) self.unintroduced_actions[before] = before_actions self._actions = new_actions @@ -136,6 +150,7 @@ def add_action(self, action, section=None, before=None, # Track state of menu to avoid re-rendering if menu has not changed self._dirty = True + self._actions_map[action_id] = action def get_title(self): """ @@ -190,9 +205,9 @@ def _render(self): # Update actions with those that were not introduced because # a `before` action they required was not part of the menu yet. for before, actions in self.unintroduced_actions.items(): - for section, action in actions: - self.add_action(action, section=section, before=before, - check_before=False) + for section, action, action_id in actions: + self.add_action(action, action_id, section=section, + before=before, check_before=False) actions = self.get_actions() add_actions(self, actions) diff --git a/spyder/plugins/mainmenu/plugin.py b/spyder/plugins/mainmenu/plugin.py index 66914fdd2e6..75bd8a622b5 100644 --- a/spyder/plugins/mainmenu/plugin.py +++ b/spyder/plugins/mainmenu/plugin.py @@ -11,6 +11,7 @@ # Standard library imports from collections import OrderedDict import sys +from typing import Dict, List, Tuple, Optional, Union # Third party imports from qtpy.QtGui import QKeySequence @@ -20,14 +21,20 @@ from spyder.api.exceptions import SpyderAPIError from spyder.api.plugins import Plugins, SpyderPluginV2, SpyderDockablePlugin from spyder.api.translations import get_translation -from spyder.api.widgets.menus import MENU_SEPARATOR +from spyder.api.widgets.menus import MENU_SEPARATOR, SpyderMenu from spyder.plugins.mainmenu.api import ( ApplicationMenu, ApplicationMenus, HelpMenuSections) -from spyder.utils.qthelpers import add_actions, set_menu_icons +from spyder.utils.qthelpers import add_actions, set_menu_icons, SpyderAction # Localization _ = get_translation('spyder') +# Extended typing definitions +ItemType = Union[SpyderAction, SpyderMenu] +ItemSectionBefore = Tuple[ + ItemType, str, Optional[str], Optional[str], Optional[str]] +ItemQueue = Dict[str, List[ItemSectionBefore]] + class MainMenu(SpyderPluginV2): NAME = 'mainmenu' @@ -46,6 +53,11 @@ def get_description(self): def on_initialize(self): # Reference holder dict for the menus self._APPLICATION_MENUS = OrderedDict() + + # Queue that contain items that are pending to add to a non-existing + # menu + self._ITEM_QUEUE = {} # type: ItemQueue + # Create Application menus using plugin public API # FIXME: Migrated menus need to have 'dynamic=True' (default value) to # work on Mac. Remove the 'dynamic' kwarg when migrating a menu! @@ -149,7 +161,8 @@ def _setup_menus(self): # ---- Public API # ------------------------------------------------------------------------ - def create_application_menu(self, menu_id, title, dynamic=True): + def create_application_menu(self, menu_id: str, title: str, + dynamic: bool = True): """ Create a Spyder application menu. @@ -180,11 +193,22 @@ def create_application_menu(self, menu_id, title, dynamic=True): lambda menu=menu: set_menu_icons(menu, False)) menu.aboutToShow.connect(self._hide_options_menus) + if menu_id in self._ITEM_QUEUE: + pending_items = self._ITEM_QUEUE.pop(menu_id) + for pending in pending_items: + (item, item_id, section, + before_item, before_section) = pending + self.add_item_to_application_menu( + item, item_id, menu_id=menu_id, section=section, + before=before_item, before_section=before_section) + return menu - def add_item_to_application_menu(self, item, menu=None, menu_id=None, - section=None, before=None, - before_section=None): + def add_item_to_application_menu(self, item: ItemType, item_id: str, + menu_id: Optional[str] = None, + section: Optional[str] = None, + before: Optional[str] = None, + before_section: Optional[str] = None): """ Add action or widget `item` to given application menu `section`. @@ -192,8 +216,8 @@ def add_item_to_application_menu(self, item, menu=None, menu_id=None, ---------- item: SpyderAction or SpyderMenu The item to add to the `menu`. - menu: ApplicationMenu or None - Instance of a Spyder application menu. + item_id: str + The identifier under which the item will be stored. menu_id: str or None The application menu unique string identifier. section: str or None @@ -208,18 +232,6 @@ def add_item_to_application_menu(self, item, menu=None, menu_id=None, ----- Must provide a `menu` or a `menu_id`. """ - if menu and menu_id: - raise SpyderAPIError('Must provide only menu or menu_id!') - - if menu is None and menu_id is None: - raise SpyderAPIError('Must provide at least menu or menu_id!') - - if menu and not isinstance(menu, ApplicationMenu): - raise SpyderAPIError('Not an `ApplicationMenu`!') - - if menu_id and menu_id not in self._APPLICATION_MENUS: - raise SpyderAPIError('{} is not a valid menu_id'.format(menu_id)) - # TODO: For now just add the item to the bottom for non-migrated menus. # Temporal solution while migration is complete app_menu_actions = { @@ -230,18 +242,22 @@ def add_item_to_application_menu(self, item, menu=None, menu_id=None, ApplicationMenus.Debug: self._main.debug_menu_actions, } - menu_id = menu_id if menu_id else menu.menu_id - menu = menu if menu else self.get_application_menu(menu_id) - if menu_id in app_menu_actions: actions = app_menu_actions[menu_id] actions.append(MENU_SEPARATOR) actions.append(item) else: - menu.add_action(item, section=section, before=before, - before_section=before_section) + if menu_id not in self._APPLICATION_MENUS: + pending_menu_items = self._ITEM_QUEUE.get(menu_id, []) + pending_menu_items.append((item, item_id, section, before, + before_section)) + self._ITEM_QUEUE[menu_id] = pending_menu_items + else: + menu = self.get_application_menu(menu_id) + menu.add_action(item, item_id, section=section, before=before, + before_section=before_section) - def get_application_menu(self, menu_id): + def get_application_menu(self, menu_id: str) -> SpyderMenu: """ Return an application menu by menu unique id. From 78968dea9fda823b42484b5399765df02e7812bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Andr=C3=A9s=20Margffoy=20Tuay?= Date: Thu, 12 Aug 2021 16:27:17 -0500 Subject: [PATCH 2/6] Add identifiers to menus and actions --- spyder/api/widgets/main_widget.py | 2 +- spyder/api/widgets/menus.py | 21 +++++++++++++++------ spyder/api/widgets/mixins.py | 2 +- spyder/app/mainwindow.py | 13 +++++++++---- spyder/plugins/mainmenu/plugin.py | 18 ++++++++++-------- spyder/utils/qthelpers.py | 5 +++-- 6 files changed, 39 insertions(+), 22 deletions(-) diff --git a/spyder/api/widgets/main_widget.py b/spyder/api/widgets/main_widget.py index 36aca314112..7db9e281f7e 100644 --- a/spyder/api/widgets/main_widget.py +++ b/spyder/api/widgets/main_widget.py @@ -555,7 +555,7 @@ def create_menu(self, menu_id, title='', icon=None): 'Menu name "{}" already in use!'.format(menu_id) ) - menu = MainWidgetMenu(parent=self, title=title) + menu = MainWidgetMenu(parent=self, title=title, menu_id=menu_id) menu.ID = menu_id MENU_REGISTRY.register_reference( diff --git a/spyder/api/widgets/menus.py b/spyder/api/widgets/menus.py index 2c56aaf229b..c7c1c377ab6 100644 --- a/spyder/api/widgets/menus.py +++ b/spyder/api/widgets/menus.py @@ -9,6 +9,7 @@ """ # Standard library imports +from spyder.api.exceptions import SpyderAPIError import sys from typing import Optional, Union, TypeVar @@ -46,7 +47,8 @@ class SpyderMenu(QMenu): """ MENUS = [] - def __init__(self, parent=None, title=None, dynamic=True): + def __init__(self, parent=None, title=None, dynamic=True, + menu_id=None): self._parent = parent self._title = title self._sections = [] @@ -55,6 +57,7 @@ def __init__(self, parent=None, title=None, dynamic=True): self.unintroduced_actions = {} self.unintroduced_sections = [] self._dirty = False + self.menu_id = menu_id if title is None: super().__init__(parent) @@ -85,7 +88,7 @@ def clear_actions(self): self.unintroduced_sections = [] def add_action(self: T, action: Union[SpyderAction, T], - action_id: str, section: Optional[str] = None, + section: Optional[str] = None, before: Optional[str] = None, before_section: Optional[str] = None, check_before: bool = True): @@ -96,8 +99,6 @@ def add_action(self: T, action: Union[SpyderAction, T], ---------- action: SpyderAction The action to add. - action_id: str - An unique identifier to store the action under the current menu. section: str or None The section id in which to insert the `action`. before: str @@ -110,6 +111,14 @@ def add_action(self: T, action: Union[SpyderAction, T], necessary to avoid an infinite recursion when adding unintroduced actions with this method again. """ + item_id = None + if isinstance(action, SpyderAction): + item_id = action.action_id + elif isinstance(action, SpyderMenu): + item_id = action.menu_id + + assert item_id is not None + if before is None: self._actions.append((section, action)) else: @@ -130,7 +139,7 @@ def add_action(self: T, action: Union[SpyderAction, T], # the menu is rendered. if not added and check_before: before_actions = self.unintroduced_actions.get(before, []) - before_actions.append((section, action, action_id)) + before_actions.append((section, action, item_id)) self.unintroduced_actions[before] = before_actions self._actions = new_actions @@ -150,7 +159,7 @@ def add_action(self: T, action: Union[SpyderAction, T], # Track state of menu to avoid re-rendering if menu has not changed self._dirty = True - self._actions_map[action_id] = action + self._actions_map[item_id] = action def get_title(self): """ diff --git a/spyder/api/widgets/mixins.py b/spyder/api/widgets/mixins.py index 26a7e13018b..44e09d0f9ed 100644 --- a/spyder/api/widgets/mixins.py +++ b/spyder/api/widgets/mixins.py @@ -254,7 +254,7 @@ def create_menu(self, name, text=None, icon=None): """ from spyder.api.widgets.menus import SpyderMenu - menu = SpyderMenu(parent=self, title=text) + menu = SpyderMenu(parent=self, title=text, menu_id=name) if icon is not None: menu.menuAction().setIconVisibleInMenu(True) menu.setIcon(icon) diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py index 9666a458bc4..46e36c5be6f 100644 --- a/spyder/app/mainwindow.py +++ b/spyder/app/mainwindow.py @@ -31,6 +31,7 @@ import signal import socket import glob +from spyder.api.widgets.menus import SpyderMenu import sys import threading import traceback @@ -975,7 +976,8 @@ def setup(self): icon=ima.icon('filelist'), tip=_('Fast switch between files'), triggered=self.open_switcher, - context=Qt.ApplicationShortcut) + context=Qt.ApplicationShortcut, + id_='file_switcher') self.register_shortcut(self.file_switcher_action, context="_", name="File switcher") self.symbol_finder_action = create_action( @@ -983,7 +985,8 @@ def setup(self): icon=ima.icon('symbol_find'), tip=_('Fast symbol search in file'), triggered=self.open_symbolfinder, - context=Qt.ApplicationShortcut) + context=Qt.ApplicationShortcut, + id_='symbol_finder') self.register_shortcut(self.symbol_finder_action, context="_", name="symbol finder", add_shortcut_to_tip=True) @@ -1046,7 +1049,8 @@ def create_edit_action(text, tr_text, icon): _("PYTHONPATH manager"), None, icon=ima.icon('pythonpath'), triggered=self.show_path_manager, - tip=_("PYTHONPATH manager")) + tip=_("PYTHONPATH manager"), + id_='spyder_path_action') from spyder.plugins.application.plugin import ( ApplicationActions, WinUserEnvDialog) winenv_action = None @@ -1060,7 +1064,8 @@ def create_edit_action(text, tr_text, icon): before=winenv_action ) if get_debug_level() >= 3: - self.menu_lsp_logs = QMenu(_("LSP logs")) + self.menu_lsp_logs = SpyderMenu( + title=_("LSP logs"), menu_id='lsp_logs_menu') self.menu_lsp_logs.aboutToShow.connect(self.update_lsp_logs) mainmenu.add_item_to_application_menu( self.menu_lsp_logs, diff --git a/spyder/plugins/mainmenu/plugin.py b/spyder/plugins/mainmenu/plugin.py index 75bd8a622b5..ea982c7f4d9 100644 --- a/spyder/plugins/mainmenu/plugin.py +++ b/spyder/plugins/mainmenu/plugin.py @@ -32,7 +32,7 @@ # Extended typing definitions ItemType = Union[SpyderAction, SpyderMenu] ItemSectionBefore = Tuple[ - ItemType, str, Optional[str], Optional[str], Optional[str]] + ItemType, Optional[str], Optional[str], Optional[str]] ItemQueue = Dict[str, List[ItemSectionBefore]] @@ -196,15 +196,15 @@ def create_application_menu(self, menu_id: str, title: str, if menu_id in self._ITEM_QUEUE: pending_items = self._ITEM_QUEUE.pop(menu_id) for pending in pending_items: - (item, item_id, section, + (item, section, before_item, before_section) = pending self.add_item_to_application_menu( - item, item_id, menu_id=menu_id, section=section, + item, menu_id=menu_id, section=section, before=before_item, before_section=before_section) return menu - def add_item_to_application_menu(self, item: ItemType, item_id: str, + def add_item_to_application_menu(self, item: ItemType, menu_id: Optional[str] = None, section: Optional[str] = None, before: Optional[str] = None, @@ -216,8 +216,6 @@ def add_item_to_application_menu(self, item: ItemType, item_id: str, ---------- item: SpyderAction or SpyderMenu The item to add to the `menu`. - item_id: str - The identifier under which the item will be stored. menu_id: str or None The application menu unique string identifier. section: str or None @@ -232,6 +230,10 @@ def add_item_to_application_menu(self, item: ItemType, item_id: str, ----- Must provide a `menu` or a `menu_id`. """ + if not isinstance(item, (SpyderAction, SpyderMenu)): + raise SpyderAPIError('A menu only accepts items objects of type ' + 'SpyderAction or SpyderMenu') + # TODO: For now just add the item to the bottom for non-migrated menus. # Temporal solution while migration is complete app_menu_actions = { @@ -249,12 +251,12 @@ def add_item_to_application_menu(self, item: ItemType, item_id: str, else: if menu_id not in self._APPLICATION_MENUS: pending_menu_items = self._ITEM_QUEUE.get(menu_id, []) - pending_menu_items.append((item, item_id, section, before, + pending_menu_items.append((item, section, before, before_section)) self._ITEM_QUEUE[menu_id] = pending_menu_items else: menu = self.get_application_menu(menu_id) - menu.add_action(item, item_id, section=section, before=before, + menu.add_action(item, section=section, before=before, before_section=before_section) def get_application_menu(self, menu_id: str) -> SpyderMenu: diff --git a/spyder/utils/qthelpers.py b/spyder/utils/qthelpers.py index 91a779940c0..1d87188c06a 100644 --- a/spyder/utils/qthelpers.py +++ b/spyder/utils/qthelpers.py @@ -311,7 +311,7 @@ def create_action(parent, text, shortcut=None, icon=None, tip=None, id_=None, plugin=None, context_name=None, register_action=False, overwrite=False): """Create a QAction""" - action = SpyderAction(text, parent) + action = SpyderAction(text, parent, action_id=id_) if triggered is not None: action.triggered.connect(triggered) if toggled is not None: @@ -552,9 +552,10 @@ def get_filetype_icon(fname): class SpyderAction(QAction): """Spyder QAction class wrapper to handle cross platform patches.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, action_id=None, **kwargs): """Spyder QAction class wrapper to handle cross platform patches.""" super(SpyderAction, self).__init__(*args, **kwargs) + self.action_id = action_id if sys.platform == "darwin": self.setIconVisibleInMenu(False) From 6b89a8d722d4c7ca6718be18a8882134e19d0bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Andr=C3=A9s=20Margffoy=20Tuay?= Date: Thu, 12 Aug 2021 19:12:47 -0500 Subject: [PATCH 3/6] Fix references --- spyder/api/widgets/menus.py | 20 +++++++++++++------- spyder/plugins/completion/api.py | 6 +++--- spyder/plugins/editor/plugin.py | 18 ++++++++++++------ spyder/plugins/findinfiles/plugin.py | 3 +-- spyder/plugins/ipythonconsole/plugin.py | 9 ++++++--- spyder/plugins/layout/plugin.py | 1 + spyder/plugins/mainmenu/plugin.py | 12 +++++++++--- spyder/plugins/profiler/plugin.py | 4 ++-- spyder/plugins/projects/plugin.py | 6 +++--- spyder/plugins/pylint/plugin.py | 5 ++--- spyder/plugins/toolbar/container.py | 12 ++++++++++++ 11 files changed, 64 insertions(+), 32 deletions(-) diff --git a/spyder/api/widgets/menus.py b/spyder/api/widgets/menus.py index c7c1c377ab6..291596c54a3 100644 --- a/spyder/api/widgets/menus.py +++ b/spyder/api/widgets/menus.py @@ -91,7 +91,8 @@ def add_action(self: T, action: Union[SpyderAction, T], section: Optional[str] = None, before: Optional[str] = None, before_section: Optional[str] = None, - check_before: bool = True): + check_before: bool = True, + omit_id: bool = False): """ Add action to a given menu section. @@ -110,14 +111,19 @@ def add_action(self: T, action: Union[SpyderAction, T], Check if the `before` action is part of the menu. This is necessary to avoid an infinite recursion when adding unintroduced actions with this method again. + omit_id: bool + If True, then the menu will check if the item to add declares an + id, False otherwise. This flag exists only for items added on + Spyder 4 plugins. Default: False """ item_id = None - if isinstance(action, SpyderAction): + if isinstance(action, SpyderAction) or hasattr(action, 'action_id'): item_id = action.action_id - elif isinstance(action, SpyderMenu): + elif isinstance(action, SpyderMenu) or hasattr(action, 'menu_id'): item_id = action.menu_id - assert item_id is not None + if not omit_id and item_id is None and action is not None: + raise AttributeError(f'Item {action} must declare an id.') if before is None: self._actions.append((section, action)) @@ -139,7 +145,7 @@ def add_action(self: T, action: Union[SpyderAction, T], # the menu is rendered. if not added and check_before: before_actions = self.unintroduced_actions.get(before, []) - before_actions.append((section, action, item_id)) + before_actions.append((section, action)) self.unintroduced_actions[before] = before_actions self._actions = new_actions @@ -214,8 +220,8 @@ def _render(self): # Update actions with those that were not introduced because # a `before` action they required was not part of the menu yet. for before, actions in self.unintroduced_actions.items(): - for section, action, action_id in actions: - self.add_action(action, action_id, section=section, + for section, action in actions: + self.add_action(action, section=section, before=before, check_before=False) actions = self.get_actions() diff --git a/spyder/plugins/completion/api.py b/spyder/plugins/completion/api.py index 21a1d724ab4..4e2b01b8bba 100644 --- a/spyder/plugins/completion/api.py +++ b/spyder/plugins/completion/api.py @@ -1332,7 +1332,7 @@ def add_item_to_menu(self, action_or_menu, menu, section=None, self.main.add_item_to_menu( action_or_menu, menu, section=section, before=before) - def add_item_to_application_menu(self, item, menu=None, menu_id=None, + def add_item_to_application_menu(self, item, menu_id=None, section=None, before=None, before_section=None): """ @@ -1348,7 +1348,7 @@ def add_item_to_application_menu(self, item, menu=None, menu_id=None, The application menu unique string identifier. section: str or None The section id in which to insert the `item` on the `menu`. - before: SpyderAction/SpyderMenu or None + before: str or None Make the item appear before another given item. before_section: Section or None Make the item section (if provided) appear before another @@ -1359,5 +1359,5 @@ def add_item_to_application_menu(self, item, menu=None, menu_id=None, Must provide a `menu` or a `menu_id`. """ self.main.add_item_to_application_menu( - item, menu=menu, menu_id=menu_id, section=section, + item, menu_id=menu_id, section=section, before=before, before_section=before_section) diff --git a/spyder/plugins/editor/plugin.py b/spyder/plugins/editor/plugin.py index 0603bd1285b..da3fe43f879 100644 --- a/spyder/plugins/editor/plugin.py +++ b/spyder/plugins/editor/plugin.py @@ -985,7 +985,8 @@ def get_plugin_actions(self): self.new_action, menu_id=ApplicationMenus.File, section=FileMenuSections.New, - before_section=FileMenuSections.Restart) + before_section=FileMenuSections.Restart, + omit_id=True) # Open section open_actions = [ self.open_action, @@ -997,7 +998,8 @@ def get_plugin_actions(self): open_action, menu_id=ApplicationMenus.File, section=FileMenuSections.Open, - before_section=FileMenuSections.Restart) + before_section=FileMenuSections.Restart, + omit_id=True) # Save section save_actions = [ self.save_action, @@ -1011,7 +1013,8 @@ def get_plugin_actions(self): save_action, menu_id=ApplicationMenus.File, section=FileMenuSections.Save, - before_section=FileMenuSections.Restart) + before_section=FileMenuSections.Restart, + omit_id=True) # Print print_actions = [ print_preview_action, @@ -1022,7 +1025,8 @@ def get_plugin_actions(self): print_action, menu_id=ApplicationMenus.File, section=FileMenuSections.Print, - before_section=FileMenuSections.Restart) + before_section=FileMenuSections.Restart, + omit_id=True) # Close close_actions = [ self.close_action, @@ -1033,14 +1037,16 @@ def get_plugin_actions(self): close_action, menu_id=ApplicationMenus.File, section=FileMenuSections.Close, - before_section=FileMenuSections.Restart) + before_section=FileMenuSections.Restart, + omit_id=True) # Navigation if sys.platform == 'darwin': self.main.mainmenu.add_item_to_application_menu( self.tab_navigation_actions, menu_id=ApplicationMenus.File, section=FileMenuSections.Navigation, - before_section=FileMenuSections.Restart) + before_section=FileMenuSections.Restart, + omit_id=True) file_toolbar_actions = ([self.new_action, self.open_action, self.save_action, self.save_all_action] + diff --git a/spyder/plugins/findinfiles/plugin.py b/spyder/plugins/findinfiles/plugin.py index af2e7d9768c..a6abf7b0557 100644 --- a/spyder/plugins/findinfiles/plugin.py +++ b/spyder/plugins/findinfiles/plugin.py @@ -87,10 +87,9 @@ def on_main_menu_available(self): mainmenu = self.get_plugin(Plugins.MainMenu) findinfiles_action = self.get_action(FindInFilesActions.FindInFiles) - menu = mainmenu.get_application_menu(ApplicationMenus.Search) mainmenu.add_item_to_application_menu( findinfiles_action, - menu=menu, + menu_id=ApplicationMenus.Search, ) def on_close(self, cancelable=False): diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py index 46f831002dc..1c9d56f25d1 100644 --- a/spyder/plugins/ipythonconsole/plugin.py +++ b/spyder/plugins/ipythonconsole/plugin.py @@ -673,12 +673,14 @@ def get_plugin_actions(self): self.main.mainmenu.add_item_to_application_menu( console_new_action, menu_id=ApplicationMenus.Consoles, - section=ConsolesMenuSections.New) + section=ConsolesMenuSections.New, + omit_id=True) for console_restart_connect_action in restart_connect_consoles_actions: self.main.mainmenu.add_item_to_application_menu( console_restart_connect_action, menu_id=ApplicationMenus.Consoles, - section=ConsolesMenuSections.Restart) + section=ConsolesMenuSections.Restart, + omit_id=True) # IPython documentation self.ipython_menu = SpyderMenu( @@ -703,7 +705,8 @@ def get_plugin_actions(self): self.ipython_menu, menu_id=ApplicationMenus.Help, section=HelpMenuSections.ExternalDocumentation, - before_section=HelpMenuSections.About) + before_section=HelpMenuSections.About, + omit_id=True) # Plugin actions self.menu_actions = [create_client_action, special_console_menu, diff --git a/spyder/plugins/layout/plugin.py b/spyder/plugins/layout/plugin.py index 9be3195fac8..09bddd4fe14 100644 --- a/spyder/plugins/layout/plugin.py +++ b/spyder/plugins/layout/plugin.py @@ -735,6 +735,7 @@ def create_plugins_menu(self): except AttributeError: # Old API action = plugin._toggle_view_action + action.action_id = f'switch to {plugin.CONF_SECTION}' if action: action.setChecked(plugin.dockwidget.isVisible()) diff --git a/spyder/plugins/mainmenu/plugin.py b/spyder/plugins/mainmenu/plugin.py index ea982c7f4d9..0ec17d0707c 100644 --- a/spyder/plugins/mainmenu/plugin.py +++ b/spyder/plugins/mainmenu/plugin.py @@ -10,6 +10,7 @@ # Standard library imports from collections import OrderedDict +from email.mime.nonmultipart import MIMENonMultipart import sys from typing import Dict, List, Tuple, Optional, Union @@ -208,7 +209,8 @@ def add_item_to_application_menu(self, item: ItemType, menu_id: Optional[str] = None, section: Optional[str] = None, before: Optional[str] = None, - before_section: Optional[str] = None): + before_section: Optional[str] = None, + omit_id: bool = False): """ Add action or widget `item` to given application menu `section`. @@ -225,12 +227,16 @@ def add_item_to_application_menu(self, item: ItemType, before_section: Section or None Make the item section (if provided) appear before another given section. + omit_id: bool + If True, then the menu will check if the item to add declares an + id, False otherwise. This flag exists only for items added on + Spyder 4 plugins. Default: False Notes ----- Must provide a `menu` or a `menu_id`. """ - if not isinstance(item, (SpyderAction, SpyderMenu)): + if not isinstance(item, (SpyderAction, SpyderMenu)) and not omit_id: raise SpyderAPIError('A menu only accepts items objects of type ' 'SpyderAction or SpyderMenu') @@ -257,7 +263,7 @@ def add_item_to_application_menu(self, item: ItemType, else: menu = self.get_application_menu(menu_id) menu.add_action(item, section=section, before=before, - before_section=before_section) + before_section=before_section, omit_id=omit_id) def get_application_menu(self, menu_id: str) -> SpyderMenu: """ diff --git a/spyder/plugins/profiler/plugin.py b/spyder/plugins/profiler/plugin.py index a054837e949..884328032be 100644 --- a/spyder/plugins/profiler/plugin.py +++ b/spyder/plugins/profiler/plugin.py @@ -102,8 +102,8 @@ def on_main_menu_available(self): mainmenu = self.get_plugin(Plugins.MainMenu) run_action = self.get_action(ProfilerActions.ProfileCurrentFile) - run_menu = mainmenu.get_application_menu(ApplicationMenus.Run) - mainmenu.add_item_to_application_menu(run_action, menu=run_menu) + mainmenu.add_item_to_application_menu( + run_action, menu_id=ApplicationMenus.Run) # --- Public API # ------------------------------------------------------------------------ diff --git a/spyder/plugins/projects/plugin.py b/spyder/plugins/projects/plugin.py index ad3546e4a90..8287ddd70e4 100644 --- a/spyder/plugins/projects/plugin.py +++ b/spyder/plugins/projects/plugin.py @@ -248,19 +248,19 @@ def on_main_menu_available(self): main_menu.add_item_to_application_menu( new_project_action, - menu=projects_menu, + menu_id=ApplicationMenus.Projects, section=ProjectsMenuSections.New) for item in [open_project_action, self.close_project_action, self.delete_project_action]: main_menu.add_item_to_application_menu( item, - menu=projects_menu, + menu_id=ApplicationMenus.Projects, section=ProjectsMenuSections.Open) main_menu.add_item_to_application_menu( self.recent_project_menu, - menu=projects_menu, + menu_id=ApplicationMenus.Projects, section=ProjectsMenuSections.Extras) def setup(self): diff --git a/spyder/plugins/pylint/plugin.py b/spyder/plugins/pylint/plugin.py index ad6449bd278..752f623d285 100644 --- a/spyder/plugins/pylint/plugin.py +++ b/spyder/plugins/pylint/plugin.py @@ -126,9 +126,8 @@ def on_main_menu_available(self): mainmenu = self.get_plugin(Plugins.MainMenu) pylint_act = self.get_action(PylintActions.AnalyzeCurrentFile) - source_menu = mainmenu.get_application_menu( - ApplicationMenus.Source) - mainmenu.add_item_to_application_menu(pylint_act, menu=source_menu) + mainmenu.add_item_to_application_menu( + pylint_act, menu_id=ApplicationMenus.Source) # --- Private API # ------------------------------------------------------------------------ diff --git a/spyder/plugins/toolbar/container.py b/spyder/plugins/toolbar/container.py index 1fb8ebb3a6b..cf96f650a81 100644 --- a/spyder/plugins/toolbar/container.py +++ b/spyder/plugins/toolbar/container.py @@ -13,6 +13,7 @@ # Third party imports from qtpy.QtCore import QSize, Slot +from qtpy.QtWidgets import QAction # Local imports from spyder.api.exceptions import SpyderAPIError @@ -41,6 +42,15 @@ class ToolbarActions: ShowToolbars = "show toolbars" +class QActionID(QAction): + @property + def action_id(self): + return self._action_id + + @action_id.setter + def action_id(self, act): + self._action_id = act + class ToolbarContainer(PluginMainContainer): def __init__(self, name, plugin, parent=None): super().__init__(name, plugin, parent=parent) @@ -296,6 +306,8 @@ def create_toolbars_menu(self): for toolbar_id, toolbar in self._ADDED_TOOLBARS.items(): if toolbar: action = toolbar.toggleViewAction() + action.__class__ = QActionID + action.action_id = f'toolbar_{toolbar_id}' section = ( main_section if toolbar_id in default_toolbars From a75e74bb327981efe9cbc21ea2d998068e65023c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Andr=C3=A9s=20Margffoy=20Tuay?= Date: Mon, 16 Aug 2021 12:39:07 -0500 Subject: [PATCH 4/6] Address review comments --- spyder/api/widgets/main_widget.py | 3 +-- spyder/api/widgets/menus.py | 1 - spyder/app/mainwindow.py | 2 +- spyder/plugins/application/plugin.py | 3 +-- spyder/plugins/mainmenu/plugin.py | 5 ++--- spyder/plugins/toolbar/container.py | 2 ++ spyder/plugins/tours/plugin.py | 6 +----- 7 files changed, 8 insertions(+), 14 deletions(-) diff --git a/spyder/api/widgets/main_widget.py b/spyder/api/widgets/main_widget.py index 7db9e281f7e..fad1c5410eb 100644 --- a/spyder/api/widgets/main_widget.py +++ b/spyder/api/widgets/main_widget.py @@ -556,10 +556,9 @@ def create_menu(self, menu_id, title='', icon=None): ) menu = MainWidgetMenu(parent=self, title=title, menu_id=menu_id) - menu.ID = menu_id MENU_REGISTRY.register_reference( - menu, menu.ID, self.PLUGIN_NAME, self.CONTEXT_NAME) + menu, menu_id, self.PLUGIN_NAME, self.CONTEXT_NAME) if icon is not None: menu.menuAction().setIconVisibleInMenu(True) diff --git a/spyder/api/widgets/menus.py b/spyder/api/widgets/menus.py index 291596c54a3..f747d93d246 100644 --- a/spyder/api/widgets/menus.py +++ b/spyder/api/widgets/menus.py @@ -9,7 +9,6 @@ """ # Standard library imports -from spyder.api.exceptions import SpyderAPIError import sys from typing import Optional, Union, TypeVar diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py index 46e36c5be6f..798c9f81a5e 100644 --- a/spyder/app/mainwindow.py +++ b/spyder/app/mainwindow.py @@ -31,7 +31,6 @@ import signal import socket import glob -from spyder.api.widgets.menus import SpyderMenu import sys import threading import traceback @@ -70,6 +69,7 @@ #============================================================================== from spyder import __version__ from spyder import dependencies +from spyder.api.widgets.menus import SpyderMenu from spyder.app.utils import ( create_application, create_splash_screen, create_window, delete_lsp_log_files, qt_message_handler, set_links_color, setup_logging, diff --git a/spyder/plugins/application/plugin.py b/spyder/plugins/application/plugin.py index c3818cb0cbf..baa806bbe3c 100644 --- a/spyder/plugins/application/plugin.py +++ b/spyder/plugins/application/plugin.py @@ -136,8 +136,7 @@ def _populate_help_menu_documentation_section(self): if shortcuts: from spyder.plugins.shortcuts.plugin import ShortcutActions - shortcuts_summary_action = shortcuts.get_action( - ShortcutActions.ShortcutSummaryAction) + shortcuts_summary_action = ShortcutActions.ShortcutSummaryAction for documentation_action in [ self.documentation_action, self.video_action]: mainmenu.add_item_to_application_menu( diff --git a/spyder/plugins/mainmenu/plugin.py b/spyder/plugins/mainmenu/plugin.py index 0ec17d0707c..42d3ca73f97 100644 --- a/spyder/plugins/mainmenu/plugin.py +++ b/spyder/plugins/mainmenu/plugin.py @@ -10,7 +10,6 @@ # Standard library imports from collections import OrderedDict -from email.mime.nonmultipart import MIMENonMultipart import sys from typing import Dict, List, Tuple, Optional, Union @@ -222,8 +221,8 @@ def add_item_to_application_menu(self, item: ItemType, The application menu unique string identifier. section: str or None The section id in which to insert the `item` on the `menu`. - before: SpyderAction/SpyderMenu or None - Make the item appear before another given item. + before: str + Make the item appear before the given object identifier. before_section: Section or None Make the item section (if provided) appear before another given section. diff --git a/spyder/plugins/toolbar/container.py b/spyder/plugins/toolbar/container.py index cf96f650a81..4f9673304db 100644 --- a/spyder/plugins/toolbar/container.py +++ b/spyder/plugins/toolbar/container.py @@ -43,6 +43,7 @@ class ToolbarActions: class QActionID(QAction): + """Wrapper class around QAction that allows to set/get an identifier.""" @property def action_id(self): return self._action_id @@ -51,6 +52,7 @@ def action_id(self): def action_id(self, act): self._action_id = act + class ToolbarContainer(PluginMainContainer): def __init__(self, name, plugin, parent=None): super().__init__(name, plugin, parent=parent) diff --git a/spyder/plugins/tours/plugin.py b/spyder/plugins/tours/plugin.py index 80de509716c..8b041e1ce5c 100644 --- a/spyder/plugins/tours/plugin.py +++ b/spyder/plugins/tours/plugin.py @@ -58,15 +58,11 @@ def on_initialize(self): def on_main_menu_available(self): mainmenu = self.get_plugin(Plugins.MainMenu) - docs_action = self.get_action( - ApplicationActions.SpyderDocumentationAction, - plugin=Plugins.Application) - mainmenu.add_item_to_application_menu( self.get_container().tour_action, menu_id=ApplicationMenus.Help, section=HelpMenuSections.Documentation, - before=docs_action) + before=ApplicationActions.SpyderDocumentationAction) def on_mainwindow_visible(self): self.show_tour_message() From 58a80dfe487e85f7b4f4c7fb2cd38203e0a81fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Andr=C3=A9s=20Margffoy=20Tuay?= Date: Mon, 16 Aug 2021 15:08:15 -0500 Subject: [PATCH 5/6] Fix missing before references --- spyder/app/mainwindow.py | 3 +-- spyder/plugins/help/plugin.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py index 798c9f81a5e..5d7592adbde 100644 --- a/spyder/app/mainwindow.py +++ b/spyder/app/mainwindow.py @@ -1055,8 +1055,7 @@ def create_edit_action(text, tr_text, icon): ApplicationActions, WinUserEnvDialog) winenv_action = None if WinUserEnvDialog: - winenv_action = self.application.get_action( - ApplicationActions.SpyderWindowsEnvVariables) + winenv_action = ApplicationActions.SpyderWindowsEnvVariables mainmenu.add_item_to_application_menu( spyder_path_action, menu_id=ApplicationMenus.Tools, diff --git a/spyder/plugins/help/plugin.py b/spyder/plugins/help/plugin.py index 73320a11749..c2411f0e0bc 100644 --- a/spyder/plugins/help/plugin.py +++ b/spyder/plugins/help/plugin.py @@ -177,8 +177,7 @@ def _setup_menus(self): shortcuts_summary_action = None if shortcuts: from spyder.plugins.shortcuts.plugin import ShortcutActions - shortcuts_summary_action = shortcuts.get_action( - ShortcutActions.ShortcutSummaryAction) + shortcuts_summary_action = ShortcutActions.ShortcutSummaryAction if mainmenu: from spyder.plugins.mainmenu.api import ( ApplicationMenus, HelpMenuSections) From 5b1133a17fc01ea4f9bcc196bc6d5953f24a34f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Andr=C3=A9s=20Margffoy=20Tuay?= Date: Tue, 17 Aug 2021 16:46:33 -0500 Subject: [PATCH 6/6] Fix missing menus --- spyder/plugins/breakpoints/plugin.py | 4 ++-- spyder/plugins/shortcuts/plugin.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/spyder/plugins/breakpoints/plugin.py b/spyder/plugins/breakpoints/plugin.py index 25b71d657b6..2c7a20a1146 100644 --- a/spyder/plugins/breakpoints/plugin.py +++ b/spyder/plugins/breakpoints/plugin.py @@ -137,8 +137,8 @@ def on_editor_available(self): def on_main_menu_available(self): mainmenu = self.get_plugin(Plugins.MainMenu) list_action = self.get_action(BreakpointsActions.ListBreakpoints) - debug_menu = mainmenu.get_application_menu(ApplicationMenus.Debug) - mainmenu.add_item_to_application_menu(list_action, debug_menu) + mainmenu.add_item_to_application_menu( + list_action, menu_id=ApplicationMenus.Debug) # --- Private API # ------------------------------------------------------------------------ diff --git a/spyder/plugins/shortcuts/plugin.py b/spyder/plugins/shortcuts/plugin.py index 5a37c825c11..72291f19873 100644 --- a/spyder/plugins/shortcuts/plugin.py +++ b/spyder/plugins/shortcuts/plugin.py @@ -91,10 +91,9 @@ def on_main_menu_available(self): ShortcutActions.ShortcutSummaryAction) # Add to Help menu. - help_menu = mainmenu.get_application_menu(ApplicationMenus.Help) mainmenu.add_item_to_application_menu( shortcuts_action, - help_menu, + menu_id=ApplicationMenus.Help, section=HelpMenuSections.Documentation, )