diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index 067c2b5e260..6a39bcd2ad8 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -16,10 +16,10 @@ on: - '**.sh' pull_request: - branches: - - master - - 5.* - - 4.x + # branches: + # - master + # - 5.* + # - 4.x paths: - '.github/scripts/*.sh' - '.github/workflows/*.yml' diff --git a/.github/workflows/test-mac.yml b/.github/workflows/test-mac.yml index 37c2dbcde4f..95a74ab2c35 100644 --- a/.github/workflows/test-mac.yml +++ b/.github/workflows/test-mac.yml @@ -16,10 +16,10 @@ on: - '**.sh' pull_request: - branches: - - master - - 5.* - - 4.x + # branches: + # - master + # - 5.* + # - 4.x paths: - '.github/scripts/*.sh' - '.github/workflows/*.yml' diff --git a/.github/workflows/test-win.yml b/.github/workflows/test-win.yml index c5f39301066..70c2f8bc776 100644 --- a/.github/workflows/test-win.yml +++ b/.github/workflows/test-win.yml @@ -16,10 +16,10 @@ on: - '**.sh' pull_request: - branches: - - master - - 5.* - - 4.x + # branches: + # - master + # - 5.* + # - 4.x paths: - '.github/scripts/*.sh' - '.github/workflows/*.yml' diff --git a/spyder/api/plugin_registration/_confpage.py b/spyder/api/plugin_registration/_confpage.py new file mode 100644 index 00000000000..9da92ecf3e5 --- /dev/null +++ b/spyder/api/plugin_registration/_confpage.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Plugin registry configuration page.""" + +# Third party imports +from qtpy.QtWidgets import (QGroupBox, QVBoxLayout, QCheckBox, + QGridLayout, QLabel) + +# Local imports +from spyder.api.plugins import SpyderPlugin +from spyder.api.preferences import PluginConfigPage +from spyder.config.base import _ +from spyder.config.manager import CONF + + +class PluginsConfigPage(PluginConfigPage): + def setup_page(self): + newcb = self.create_checkbox + self.plugins_checkboxes = {} + + header_label = QLabel( + _("Here you can turn on/off any internal or external Spyder plugin " + "to disable functionality that is not desired or to have a lighter " + "experience. Unchecked plugins in this page will be unloaded " + "immediately and will not be loaded the next time Spyder starts.")) + header_label.setWordWrap(True) + + # ------------------ Internal plugin status group --------------------- + internal_layout = QGridLayout() + self.internal_plugins_group = QGroupBox(_("Internal plugins")) + + i = 0 + for plugin_name in self.plugin.all_internal_plugins: + (conf_section_name, + PluginClass) = self.plugin.all_internal_plugins[plugin_name] + + if not getattr(PluginClass, 'CAN_BE_DISABLED', True): + # Do not list core plugins that can not be disabled + continue + + plugin_loc_name = None + if hasattr(PluginClass, 'get_name'): + plugin_loc_name = PluginClass.get_name() + elif hasattr(PluginClass, 'get_plugin_title'): + plugin_loc_name = PluginClass.get_plugin_title() + + plugin_state = CONF.get(conf_section_name, 'enable', True) + cb = newcb(plugin_loc_name, 'enable', default=True, + section=conf_section_name, restart=True) + internal_layout.addWidget(cb, i // 2, i % 2) + self.plugins_checkboxes[plugin_name] = (cb, plugin_state) + i += 1 + + self.internal_plugins_group.setLayout(internal_layout) + + # ------------------ External plugin status group --------------------- + external_layout = QGridLayout() + self.external_plugins_group = QGroupBox(_("External plugins")) + + i = 0 + for i, plugin_name in enumerate(self.plugin.all_external_plugins): + (conf_section_name, + PluginClass) = self.plugin.all_external_plugins[plugin_name] + + plugin_loc_name = None + if hasattr(PluginClass, 'get_name'): + plugin_loc_name = PluginClass.get_name() + elif hasattr(PluginClass, 'get_plugin_title'): + plugin_loc_name = PluginClass.get_plugin_title() + + cb = newcb(plugin_loc_name, 'enable', default=True, + section=conf_section_name, restart=True) + external_layout.addWidget(cb, i // 2, i % 2) + self.plugins_checkboxes[plugin_name] = cb + i += 1 + + self.external_plugins_group.setLayout(external_layout) + + layout = QVBoxLayout() + layout.addWidget(header_label) + layout.addWidget(self.internal_plugins_group) + if self.plugin.all_external_plugins: + layout.addWidget(self.external_plugins_group) + layout.addStretch(1) + self.setLayout(layout) + + def apply_settings(self): + for plugin_name in self.plugins_checkboxes: + cb, previous_state = self.plugins_checkboxes[plugin_name] + if cb.isChecked() and not previous_state: + self.plugin.set_plugin_enabled(plugin_name) + PluginClass = None + external = False + if plugin_name in self.plugin.all_internal_plugins: + (__, + PluginClass) = self.plugin.all_internal_plugins[plugin_name] + elif plugin_name in self.plugin.all_external_plugins: + (__, + PluginClass) = self.plugin.all_external_plugins[plugin_name] + external = True + + # TODO: Once we can test that all plugins can be restarted + # without problems during runtime, we can enable the + # autorestart feature provided by the plugin registry: + # self.plugin.register_plugin(self.main, PluginClass, + # external=external) + elif not cb.isChecked() and previous_state: + # TODO: Once we can test that all plugins can be restarted + # without problems during runtime, we can enable the + # autorestart feature provided by the plugin registry: + # self.plugin.delete_plugin(plugin_name) + pass + return set({}) diff --git a/spyder/api/plugin_registration/registry.py b/spyder/api/plugin_registration/registry.py index 0f7102c5426..b8a1437f17c 100644 --- a/spyder/api/plugin_registration/registry.py +++ b/spyder/api/plugin_registration/registry.py @@ -8,17 +8,23 @@ # Standard library imports import logging -from typing import Dict, List, Union, Type, Any, Set, Optional +from typing import Dict, List, Union, Type, Any, Set, Optional, Tuple # Third-party library imports from qtpy.QtCore import QObject, Signal # Local imports +from spyder import dependencies +from spyder.config.base import _, running_under_pytest from spyder.config.manager import CONF +from spyder.api.config.mixins import SpyderConfigurationAccessor +from spyder.api.plugin_registration._confpage import PluginsConfigPage +from spyder.api.plugins.enum import Plugins from spyder.api.exceptions import SpyderAPIError from spyder.api.plugins import ( Plugins, SpyderPluginV2, SpyderDockablePlugin, SpyderPluginWidget, SpyderPlugin) +from spyder.utils.icon_manager import ima # TODO: Remove SpyderPlugin and SpyderPluginWidget once the migration @@ -34,7 +40,23 @@ logger = logging.getLogger(__name__) -class SpyderPluginRegistry(QObject): +class PreferencesAdapter(SpyderConfigurationAccessor): + # Fake class constants used to register the configuration page + CONF_WIDGET_CLASS = PluginsConfigPage + NAME = 'plugin_registry' + CONF_VERSION = None + ADDITIONAL_CONF_OPTIONS = None + ADDITIONAL_CONF_TABS = None + CONF_SECTION = "" + + def apply_plugin_settings(self, _unused): + pass + + def apply_conf(self, _unused): + pass + + +class SpyderPluginRegistry(QObject, PreferencesAdapter): """ Global plugin registry. @@ -66,6 +88,11 @@ class SpyderPluginRegistry(QObject): def __init__(self): super().__init__() + PreferencesAdapter.__init__(self) + + # Reference to the main window + self.main = None + # Dictionary that maps a plugin name to a list of the plugin names # that depend on it. self.plugin_dependents = {} # type: Dict[str, Dict[str, List[str]]] @@ -92,6 +119,12 @@ def __init__(self): # Set that stores the names of the external plugins self.external_plugins = set({}) # type: set[str] + # Dictionary that contains all the internal plugins (enabled or not) + self.all_internal_plugins = {} # type: Dict[str, Tuple[str, Type[SpyderPluginClass]]] + + # Dictionary that contains all the external plugins (enabled or not) + self.all_external_plugins = {} # type: Dict[str, Tuple[str, Type[SpyderPluginClass]]] + # ------------------------- PRIVATE API ----------------------------------- def _update_dependents(self, plugin: str, dependent_plugin: str, key: str): """Add `dependent_plugin` to the list of dependents of `plugin`.""" @@ -170,6 +203,15 @@ def _instantiate_spyder5_plugin( else: self.internal_plugins |= {plugin_name} + if external: + # These attributes come from spyder.app.find_plugins + module = PluginClass._spyder_module_name + package_name = PluginClass._spyder_package_name + version = PluginClass._spyder_version + description = plugin_instance.get_description() + dependencies.add(module, package_name, description, + version, None, kind=dependencies.PLUGIN) + return plugin_instance def _instantiate_spyder4_plugin( @@ -344,6 +386,10 @@ def notify_plugin_availability(self, plugin_name: str, plugin_instance = self.plugin_registry[plugin] plugin_instance._on_plugin_available(plugin_name) + if plugin_name == Plugins.Preferences and not running_under_pytest(): + plugin_instance = self.plugin_registry[plugin_name] + plugin_instance.register_plugin_preferences(self) + def delete_plugin(self, plugin_name: str) -> bool: """ Remove and delete a plugin from the registry by its name. @@ -418,7 +464,8 @@ def delete_plugin(self, plugin_name: str) -> bool: self.plugin_registry.pop(plugin_name) return True - def delete_all_plugins(self, excluding: Optional[Set[str]] = None) -> bool: + def delete_all_plugins(self, excluding: Optional[Set[str]] = None, + close_immediately: bool = False) -> bool: """ Remove all plugins from the registry. @@ -430,6 +477,8 @@ def delete_all_plugins(self, excluding: Optional[Set[str]] = None) -> bool: ---------- excluding: Optional[Set[str]] A set that lists plugins (by name) that will not be deleted. + close_immediately: bool + If true, then the `can_close` status will be ignored. Returns ------- @@ -445,7 +494,7 @@ def delete_all_plugins(self, excluding: Optional[Set[str]] = None) -> bool: plugin_instance = self.plugin_registry[plugin_name] if isinstance(plugin_instance, SpyderPlugin): can_close &= self.delete_plugin(plugin_name) - if not can_close: + if not can_close and not close_immediately: break if not can_close: @@ -457,10 +506,10 @@ def delete_all_plugins(self, excluding: Optional[Set[str]] = None) -> bool: plugin_instance = self.plugin_registry[plugin_name] if isinstance(plugin_instance, SpyderPluginV2): can_close &= self.delete_plugin(plugin_name) - if not can_close: + if not can_close and not close_immediately: break - if not can_close: + if not can_close and not close_immediately: return False # Delete Spyder 4 internal plugins @@ -469,7 +518,7 @@ def delete_all_plugins(self, excluding: Optional[Set[str]] = None) -> bool: plugin_instance = self.plugin_registry[plugin_name] if isinstance(plugin_instance, SpyderPlugin): can_close &= self.delete_plugin(plugin_name) - if not can_close: + if not can_close and not close_immediately: break if not can_close: @@ -480,7 +529,7 @@ def delete_all_plugins(self, excluding: Optional[Set[str]] = None) -> bool: plugin_instance = self.plugin_registry[plugin_name] if isinstance(plugin_instance, SpyderPluginV2): can_close &= self.delete_plugin(plugin_name) - if not can_close: + if not can_close and not close_immediately: break return can_close @@ -589,6 +638,23 @@ def reset(self): # Omit failures if there are no slots connected pass + def set_all_internal_plugins( + self, all_plugins: Dict[str, Type[SpyderPluginClass]]): + self.all_internal_plugins = all_plugins + + def set_all_external_plugins( + self, all_plugins: Dict[str, Type[SpyderPluginClass]]): + self.all_external_plugins = all_plugins + + def set_main(self, main): + self.main = main + + def get_icon(self): + return ima.icon('plugins') + + def get_name(self): + return _('Plugins') + def __contains__(self, plugin_name: str) -> bool: """ Determine if a plugin name is contained in the registry. diff --git a/spyder/api/plugins/new_api.py b/spyder/api/plugins/new_api.py index 564e59f9274..8ae380d6382 100644 --- a/spyder/api/plugins/new_api.py +++ b/spyder/api/plugins/new_api.py @@ -603,7 +603,7 @@ def initialize(self): Notes ----- This method should be called to initialize the plugin, but it should - not be overriden, since it internally calls `on_initialize` and emits + not be overridden, since it internally calls `on_initialize` and emits the `sig_plugin_ready` signal. """ self.on_initialize() @@ -650,7 +650,8 @@ def get_font(cls, rich_text=False): # --- API: Mandatory methods to define ----------------------------------- # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): """ Return the plugin localized name. diff --git a/spyder/api/preferences.py b/spyder/api/preferences.py index 85a04361b04..70602e81d4b 100644 --- a/spyder/api/preferences.py +++ b/spyder/api/preferences.py @@ -82,9 +82,13 @@ class PluginConfigPage(SpyderConfigPage): def __init__(self, plugin, parent): self.plugin = plugin - self.CONF_SECTION = plugin.CONF_SECTION self.main = parent.main - self.get_font = plugin.get_font + + if hasattr(plugin, 'CONF_SECTION'): + self.CONF_SECTION = plugin.CONF_SECTION + + if hasattr(plugin, 'get_font'): + self.get_font = plugin.get_font if not self.APPLY_CONF_PAGE_SETTINGS: self._patch_apply_settings(plugin) @@ -124,6 +128,12 @@ def aggregate_sections_partials(self, opts): """Aggregate options by sections in order to notify observers.""" to_update = {} for opt in opts: + if isinstance(opt, tuple): + # This is necessary to filter tuple options that do not + # belong to a section. + if len(opt) == 2 and opt[0] is None: + opt = opt[1] + section = self.CONF_SECTION if opt in self.cross_section_options: section = self.cross_section_options[opt] diff --git a/spyder/api/widgets/menus.py b/spyder/api/widgets/menus.py index a83b736614b..11590318e0c 100644 --- a/spyder/api/widgets/menus.py +++ b/spyder/api/widgets/menus.py @@ -168,6 +168,20 @@ def add_action(self: T, self._dirty = True self._actions_map[item_id] = action + def remove_action(self, item_id: str): + if item_id in self._actions_map: + action = self._actions_map.pop(item_id) + position = None + + for i, (_, act) in enumerate(self._actions): + if act == action: + position = i + break + + if position is not None: + self._actions.pop(position) + self._dirty = True + def get_title(self): """ Return the title for menu. diff --git a/spyder/api/widgets/toolbars.py b/spyder/api/widgets/toolbars.py index 8a87cf15727..3c9912bb4c3 100644 --- a/spyder/api/widgets/toolbars.py +++ b/spyder/api/widgets/toolbars.py @@ -210,6 +210,18 @@ def add_item(self, action_or_widget: ToolbarItem, self.add_item(item, section=section, before=before, before_section=before_section) + def remove_item(self, item_id: str): + """Remove action or widget from toolbar by id.""" + item = self._item_map.pop(item_id) + for section in list(self._section_items.keys()): + section_items = self._section_items[section] + if item in section_items: + section_items.remove(item) + if len(section_items) == 0: + self._section_items.pop(section) + self.clear() + self._render() + def _render(self): """ Create the toolbar taking into account sections and locations. diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py index ca738e1cb82..ef1c26c3105 100644 --- a/spyder/app/mainwindow.py +++ b/spyder/app/mainwindow.py @@ -765,6 +765,8 @@ def setup(self): lambda plugin_name, omit_conf: self.register_plugin( plugin_name, omit_conf=omit_conf)) + PLUGIN_REGISTRY.set_main(self) + # TODO: Remove circular dependency between help and ipython console # and remove this import. Help plugin should take care of it from spyder.plugins.help.utils.sphinxify import CSS_PATH, DARK_CSS_PATH @@ -851,12 +853,20 @@ def setup(self): # Determine 'enable' config for the plugins that have it enabled_plugins = {} + registry_internal_plugins = {} + registry_external_plugins = {} for plugin in all_plugins.values(): plugin_name = plugin.NAME plugin_main_attribute_name = ( self._INTERNAL_PLUGINS_MAPPING[plugin_name] if plugin_name in self._INTERNAL_PLUGINS_MAPPING else plugin_name) + if plugin_name in internal_plugins: + registry_internal_plugins[plugin_name] = ( + plugin_main_attribute_name, plugin) + else: + registry_external_plugins[plugin_name] = ( + plugin_main_attribute_name, plugin) try: if CONF.get(plugin_main_attribute_name, "enable"): enabled_plugins[plugin_name] = plugin @@ -865,6 +875,9 @@ def setup(self): enabled_plugins[plugin_name] = plugin PLUGIN_REGISTRY.set_plugin_enabled(plugin_name) + PLUGIN_REGISTRY.set_all_internal_plugins(registry_internal_plugins) + PLUGIN_REGISTRY.set_all_external_plugins(registry_external_plugins) + # Instantiate internal Spyder 5 plugins for plugin_name in internal_plugins: if plugin_name in enabled_plugins: @@ -896,15 +909,6 @@ def setup(self): try: plugin_instance = PLUGIN_REGISTRY.register_plugin( self, PluginClass, external=True) - - # These attributes come from spyder.app.find_plugins to - # add plugins to the dependencies dialog - module = PluginClass._spyder_module_name - package_name = PluginClass._spyder_package_name - version = PluginClass._spyder_version - description = plugin_instance.get_description() - dependencies.add(module, package_name, description, - version, None, kind=dependencies.PLUGIN) except Exception as error: print("%s: %s" % (PluginClass, str(error)), file=STDERR) traceback.print_exc(file=STDERR) @@ -1197,7 +1201,7 @@ def post_visible_setup(self): # Show Help and Consoles by default plugins_to_show = [self.ipyconsole] - if self.help is not None: + if hasattr(self, 'help'): plugins_to_show.append(self.help) for plugin in plugins_to_show: if plugin.dockwidget.isVisible(): @@ -1461,10 +1465,9 @@ def moveEvent(self, event): if hasattr(self, 'layouts'): if not self.isMaximized() and not self.layouts.get_fullscreen_flag(): self.window_position = self.pos() - QMainWindow.moveEvent(self, event) - - # To be used by the tour to be able to move - self.sig_moved.emit(event) + QMainWindow.moveEvent(self, event) + # To be used by the tour to be able to move + self.sig_moved.emit(event) def hideEvent(self, event): """Reimplement Qt method""" @@ -1495,7 +1498,7 @@ def change_last_focused_widget(self, old, now): self.previous_focused_widget = old - def closing(self, cancelable=False): + def closing(self, cancelable=False, close_immediately=False): """Exit tasks""" if self.already_closed or self.is_starting_up: return True @@ -1511,9 +1514,9 @@ def closing(self, cancelable=False): self.open_files_server.close() can_close = PLUGIN_REGISTRY.delete_all_plugins( - excluding={Plugins.Layout}) + excluding={Plugins.Layout}, close_immediately=close_immediately) - if not can_close: + if not can_close and not close_immediately: return False # Save window settings *after* closing all plugin windows, in order @@ -1814,6 +1817,25 @@ def start_open_files_server(self): self.sig_open_external_file.emit(fname) req.sendall(b' ') + # ---- Quit and restart, and reset spyder defaults + @Slot() + def reset_spyder(self): + """ + Quit and reset Spyder and then Restart application. + """ + answer = QMessageBox.warning(self, _("Warning"), + _("Spyder will restart and reset to default settings:

" + "Do you want to continue?"), + QMessageBox.Yes | QMessageBox.No) + if answer == QMessageBox.Yes: + self.restart(reset=True) + + @Slot() + def restart(self, reset=False, close_immediately=False): + """Wrapper to handle plugins request to restart Spyder.""" + self.application.restart( + reset=reset, close_immediately=close_immediately) + # ---- Global Switcher def open_switcher(self, symbol=False): """Open switcher dialog box.""" diff --git a/spyder/config/manager.py b/spyder/config/manager.py index 9eedb99141b..124398994e5 100644 --- a/spyder/config/manager.py +++ b/spyder/config/manager.py @@ -614,6 +614,9 @@ def config_shortcut(self, action, context, name, parent): def iter_shortcuts(self): """Iterate over keyboard shortcuts.""" for context_name, keystr in self._user_config.items('shortcuts'): + if context_name == 'enable': + continue + if 'additional_configuration' not in context_name: context, name = context_name.split('/', 1) yield context, name, keystr diff --git a/spyder/plugins/appearance/plugin.py b/spyder/plugins/appearance/plugin.py index 6232032dbb1..3e09c5908f1 100644 --- a/spyder/plugins/appearance/plugin.py +++ b/spyder/plugins/appearance/plugin.py @@ -39,7 +39,8 @@ class Appearance(SpyderPluginV2): # --- SpyderPluginV2 API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _("Appearance") def get_description(self): diff --git a/spyder/plugins/application/plugin.py b/spyder/plugins/application/plugin.py index d27c09f5bb5..ac5ba0df33c 100644 --- a/spyder/plugins/application/plugin.py +++ b/spyder/plugins/application/plugin.py @@ -28,7 +28,9 @@ running_under_pytest) from spyder.plugins.application.confpage import ApplicationConfigPage from spyder.plugins.application.container import ( - ApplicationContainer, ApplicationPluginMenus, WinUserEnvDialog) + ApplicationActions, ApplicationContainer, ApplicationPluginMenus, + WinUserEnvDialog) +from spyder.plugins.console.api import ConsoleActions from spyder.plugins.mainmenu.api import ( ApplicationMenus, FileMenuSections, HelpMenuSections, ToolsMenuSections) from spyder.utils.qthelpers import add_actions @@ -48,7 +50,8 @@ class Application(SpyderPluginV2): CONF_WIDGET_CLASS = ApplicationConfigPage CAN_BE_DISABLED = False - def get_name(self): + @staticmethod + def get_name(): return _('Application') def get_icon(self): @@ -101,6 +104,28 @@ def on_editor_available(self): self.get_container().sig_load_log_file.connect(editor.load) # -------------------------- PLUGIN TEARDOWN ------------------------------ + @on_plugin_teardown(plugin=Plugins.Preferences) + def on_preferences_teardown(self): + preferences = self.get_plugin(Plugins.Preferences) + preferences.deregister_plugin_preferences(self) + + @on_plugin_teardown(plugin=Plugins.Editor) + def on_editor_teardown(self): + editor = self.get_plugin(Plugins.Editor) + self.get_container().sig_load_log_file.disconnect(editor.load) + + @on_plugin_teardown(plugin=Plugins.Console) + def on_console_teardown(self): + if self.is_plugin_available(Plugins.MainMenu): + self.report_action.setVisible(False) + + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_main_menu_teardown(self): + self._depopulate_file_menu() + self._depopulate_tools_menu() + self._depopulate_help_menu() + self.report_action.setVisible(False) + def on_close(self, _unused=True): self.get_container().on_close() @@ -206,6 +231,60 @@ def _populate_help_menu_about_section(self): def _window(self): return self.main.window() + def _depopulate_help_menu(self): + self._depopulate_help_menu_documentation_section() + self._depopulate_help_menu_support_section() + self._depopulate_help_menu_about_section() + + def _depopulate_help_menu_documentation_section(self): + mainmenu = self.get_plugin(Plugins.MainMenu) + for documentation_action in [ + ApplicationActions.SpyderDocumentationAction, + ApplicationActions.SpyderDocumentationVideoAction]: + mainmenu.remove_item_from_application_menu( + documentation_action, + menu_id=ApplicationMenus.Help) + + def _depopulate_help_menu_support_section(self): + """Remove Spyder base support actions from the Help main menu.""" + mainmenu = self.get_plugin(Plugins.MainMenu) + for support_action in [ + ApplicationActions.SpyderTroubleshootingAction, + ConsoleActions.SpyderReportAction, + ApplicationActions.SpyderDependenciesAction, + ApplicationActions.SpyderCheckUpdatesAction, + ApplicationActions.SpyderSupportAction]: + mainmenu.remove_item_from_application_menu( + support_action, + menu_id=ApplicationMenus.Help) + + def _depopulate_help_menu_about_section(self): + mainmenu = self.get_plugin(Plugins.MainMenu) + mainmenu.remove_item_from_application_menu( + ApplicationActions.SpyderAbout, + menu_id=ApplicationMenus.Help) + + def _depopulate_file_menu(self): + mainmenu = self.get_plugin(Plugins.MainMenu) + for action_id in [ApplicationActions.SpyderRestart, + ApplicationActions.SpyderRestartDebug]: + mainmenu.remove_item_from_application_menu( + action_id, + menu_id=ApplicationMenus.File) + + def _depopulate_tools_menu(self): + """Add base actions and menus to the Tools menu.""" + mainmenu = self.get_plugin(Plugins.MainMenu) + if WinUserEnvDialog is not None: + mainmenu.remove_item_from_application_menu( + ApplicationActions.SpyderWindowsEnvVariables, + menu_id=ApplicationMenus.Tools) + + if get_debug_level() >= 2: + mainmenu.remove_item_from_application_menu( + ApplicationPluginMenus.DebugLogsMenu, + menu_id=ApplicationMenus.Tools) + # ---- Public API # ------------------------------------------------------------------------ def get_application_context_menu(self, parent=None): @@ -248,7 +327,7 @@ def apply_settings(self): self._main.apply_settings() @Slot() - def restart(self): + def restart(self, reset=False, close_immediately=False): """ Quit and Restart Spyder application. @@ -304,7 +383,7 @@ def restart(self): command = command.format(python, restart_script) try: - if self.main.closing(True): + if self.main.closing(True, close_immediately=close_immediately): subprocess.Popen(command, shell=shell, env=env, startupinfo=startupinfo) console.quit() diff --git a/spyder/plugins/breakpoints/plugin.py b/spyder/plugins/breakpoints/plugin.py index 2c7a20a1146..66ec9b8d0b4 100644 --- a/spyder/plugins/breakpoints/plugin.py +++ b/spyder/plugins/breakpoints/plugin.py @@ -18,7 +18,8 @@ # Local imports from spyder.api.plugins import Plugins, SpyderDockablePlugin -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.plugins.breakpoints.widgets.main_widget import BreakpointWidget from spyder.plugins.mainmenu.api import ApplicationMenus @@ -88,7 +89,8 @@ class Breakpoints(SpyderDockablePlugin): # --- SpyderDockablePlugin API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _("Breakpoints") def get_description(self): @@ -140,6 +142,29 @@ def on_main_menu_available(self): mainmenu.add_item_to_application_menu( list_action, menu_id=ApplicationMenus.Debug) + @on_plugin_teardown(plugin=Plugins.Editor) + def on_editor_teardown(self): + widget = self.get_widget() + editor = self.get_plugin(Plugins.Editor) + list_action = self.get_action(BreakpointsActions.ListBreakpoints) + + editor.breakpoints_saved.disconnect(self.set_data) + widget.sig_clear_all_breakpoints_requested.disconnect( + editor.clear_all_breakpoints) + widget.sig_clear_breakpoint_requested.disconnect( + editor.clear_breakpoint) + widget.sig_edit_goto_requested.disconnect(editor.load) + widget.sig_conditional_breakpoint_requested.disconnect( + editor.set_or_edit_conditional_breakpoint) + + editor.pythonfile_dependent_actions.remove(list_action) + + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_main_menu_teardown(self): + mainmenu = self.get_plugin(Plugins.MainMenu) + mainmenu.remove_item_from_application_menu( + BreakpointsActions.ListBreakpoints, menu_id=ApplicationMenus.Debug) + # --- Private API # ------------------------------------------------------------------------ def _load_data(self): diff --git a/spyder/plugins/completion/api.py b/spyder/plugins/completion/api.py index 4e2b01b8bba..2d52fcda6cf 100644 --- a/spyder/plugins/completion/api.py +++ b/spyder/plugins/completion/api.py @@ -1275,6 +1275,35 @@ def create_action(self, name, text, icon=None, icon_text='', tip=None, shortcut_context=shortcut_context, context=context, initial=initial, register_shortcut=register_shortcut) + def get_action(self, name, context=None, plugin=None): + """ + Return an action by name, context and plugin. + + Parameters + ---------- + name: str + Name of the action to retrieve. + context: Optional[str] + Widget or context identifier under which the action was stored. + If None, then `CONTEXT_NAME` is used instead + plugin: Optional[str] + Name of the plugin where the action was defined. If None, then + `PLUGIN_NAME` is used. + + Returns + ------- + action: SpyderAction + The corresponding action stored under the given `name`, `context` + and `plugin`. + + Raises + ------ + KeyError + If either of `name`, `context` or `plugin` keys do not exist in the + toolbar registry. + """ + return self.main.get_action(name, context=context, plugin=plugin) + def create_application_menu(self, menu_id, title, dynamic=True): """ Create a Spyder application menu. @@ -1361,3 +1390,17 @@ def add_item_to_application_menu(self, item, menu_id=None, self.main.add_item_to_application_menu( item, menu_id=menu_id, section=section, before=before, before_section=before_section) + + def remove_item_from_application_menu(self, item_id: str, + menu_id: Optional[str] = None): + """ + Remove action or widget from given application menu by id. + + Parameters + ---------- + item_id: str + The item identifier to remove from the given menu. + menu_id: str or None + The application menu unique string identifier. + """ + self.main.remove_item_from_application_menu(item_id, menu_id=menu_id) diff --git a/spyder/plugins/completion/plugin.py b/spyder/plugins/completion/plugin.py index 0587dc43d74..8e7cb79834a 100644 --- a/spyder/plugins/completion/plugin.py +++ b/spyder/plugins/completion/plugin.py @@ -27,7 +27,8 @@ # Local imports from spyder.config.manager import CONF from spyder.api.plugins import SpyderPluginV2, Plugins -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.config.base import _, running_under_pytest from spyder.config.user import NoDefault from spyder.plugins.completion.api import (CompletionRequestTypes, @@ -253,7 +254,8 @@ def __init__(self, parent, configuration=None): self.ADDITIONAL_CONF_TABS = {'completions': conf_tabs} # ---------------- Public Spyder API required methods --------------------- - def get_name(self) -> str: + @staticmethod + def get_name() -> str: return _('Completion and linting') def get_description(self) -> str: @@ -310,7 +312,43 @@ def on_mainmenu_available(self): for args, kwargs in self.items_to_add_to_application_menus: main_menu.add_item_to_application_menu(*args, **kwargs) - def unregister(self): + @on_plugin_teardown(plugin=Plugins.Preferences) + def on_preferences_teardown(self): + preferences = self.get_plugin(Plugins.Preferences) + preferences.deregister_plugin_preferences(self) + + @on_plugin_teardown(plugin=Plugins.StatusBar) + def on_statusbar_teardown(self): + container = self.get_container() + self.statusbar = self.get_plugin(Plugins.StatusBar) + for sb in container.all_statusbar_widgets(): + self.statusbar.remove_status_widget(sb.ID) + + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_mainmenu_teardown(self): + main_menu = self.get_plugin(Plugins.MainMenu) + signature = inspect.signature(main_menu.add_item_to_application_menu) + + for args, kwargs in self.application_menus_to_create: + menu_id = args[0] + main_menu.remove_application_menu(menu_id) + + for args, kwargs in self.items_to_add_to_application_menus: + binding = signature.bind(*args, **kwargs) + binding.apply_defaults() + + item = binding.arguments['item'] + menu_id = binding.arguments['menu_id'] + item_id = None + if hasattr(item, 'action_id'): + item_id = item.action_id + elif hasattr(item, 'menu_id'): + item_id = item.menu_id + if item_id is not None: + main_menu.remove_item_from_application_menu( + item_id, menu_id=menu_id) + + def stop_all_providers(self): """Stop all running completion providers.""" for provider_name in self.providers: provider_info = self.providers[provider_name] @@ -318,6 +356,17 @@ def unregister(self): # TODO: Remove status bar widgets provider_info['instance'].shutdown() + def can_close(self) -> bool: + """Check if any provider has any pending task.""" + can_close = False + for provider_name in self.providers: + provider_info = self.providers[provider_name] + if provider_info['status'] == self.RUNNING: + provider = provider_info['instance'] + provider_can_close = provider.can_close() + can_close |= provider_can_close + return can_close + def on_close(self, cancelable=False) -> bool: """Check if any provider has any pending task before closing.""" can_close = False @@ -339,6 +388,8 @@ def after_configuration_update(self, options: List[Union[tuple, str]]): provider tabs. """ providers_to_update = set({}) + options = [x[1] if isinstance(x, tuple) and + len(x) == 2 and x[0] is None else x for x in options] for option in options: if option == 'completions_wait_for_ms': self.wait_for_ms = self.get_conf( @@ -886,6 +937,10 @@ def create_action(self, *args, **kwargs): kwargs['parent'] = container return container.create_action(*args, **kwargs) + def get_action(self, *args, **kwargs): + container = self.get_container() + return container.get_action(*args, **kwargs) + def get_application_menu(self, *args, **kwargs): # TODO: Check if this method makes sense with the new plugin # registration mechanism. @@ -907,6 +962,11 @@ def create_menu(self, *args, **kwargs): def add_item_to_application_menu(self, *args, **kwargs): self.items_to_add_to_application_menus.append((args, kwargs)) + def remove_item_from_application_menu(self, *args, **kwargs): + main_menu = self.get_plugin(Plugins.MainMenu) + if main_menu: + main_menu.remove_item_from_application_menu(*args, **kwargs) + def add_item_to_menu(self, *args, **kwargs): container = self.get_container() container.add_item_to_menu(*args, **kwargs) diff --git a/spyder/plugins/completion/providers/kite/provider.py b/spyder/plugins/completion/providers/kite/provider.py index 3c2f96bfa26..2118f57952e 100644 --- a/spyder/plugins/completion/providers/kite/provider.py +++ b/spyder/plugins/completion/providers/kite/provider.py @@ -127,6 +127,14 @@ def start(self): self.client.start() def shutdown(self): + try: + self.remove_item_from_application_menu( + KiteProviderActions.Installation, + menu_id=ApplicationMenus.Tools) + except KeyError: + # Action does not exist + pass + self.client.stop() if self.kite_process is not None: self.kite_process.kill() diff --git a/spyder/plugins/completion/tests/conftest.py b/spyder/plugins/completion/tests/conftest.py index 2e22ee99725..354c0bdb076 100644 --- a/spyder/plugins/completion/tests/conftest.py +++ b/spyder/plugins/completion/tests/conftest.py @@ -188,7 +188,7 @@ def wait_until_all_started(): def teardown(): os.environ['SPY_TEST_USE_INTROSPECTION'] = 'False' - completion_plugin.unregister() + completion_plugin.stop_all_providers() request.addfinalizer(teardown) return completion_plugin, capabilities diff --git a/spyder/plugins/console/plugin.py b/spyder/plugins/console/plugin.py index f1f105f94a1..32c9d2a6443 100644 --- a/spyder/plugins/console/plugin.py +++ b/spyder/plugins/console/plugin.py @@ -17,9 +17,11 @@ # Local imports from spyder.api.plugins import Plugins, SpyderDockablePlugin -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation -from spyder.plugins.console.widgets.main_widget import ConsoleWidget +from spyder.plugins.console.widgets.main_widget import ( + ConsoleWidget, ConsoleWidgetActions) from spyder.plugins.mainmenu.api import ApplicationMenus, FileMenuSections # Localization @@ -74,7 +76,8 @@ class Console(SpyderDockablePlugin): # --- SpyderDockablePlugin API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _('Internal console') def get_icon(self): @@ -122,6 +125,14 @@ def on_main_menu_available(self): menu_id=ApplicationMenus.File, section=FileMenuSections.Restart) + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_main_menu_teardown(self): + widget = self.get_widget() + mainmenu = self.get_plugin(Plugins.MainMenu) + mainmenu.remove_item_from_application_menu( + ConsoleWidgetActions.Quit, + menu_id=ApplicationMenus.File) + def update_font(self): font = self.get_font() self.get_widget().set_font(font) diff --git a/spyder/plugins/editor/plugin.py b/spyder/plugins/editor/plugin.py index 24d31f52258..b594cba43a9 100644 --- a/spyder/plugins/editor/plugin.py +++ b/spyder/plugins/editor/plugin.py @@ -416,8 +416,11 @@ def _rpc_call(self, method, args, kwargs): meth(*args, **kwargs) #------ SpyderPluginWidget API --------------------------------------------- - def get_plugin_title(self): + @staticmethod + def get_plugin_title(): """Return widget title""" + # TODO: This is a temporary measure to get the title of this plugin + # without creating an instance title = _('Editor') return title @@ -1069,6 +1072,7 @@ def get_plugin_actions(self): find_previous_action, replace_action, gotoline_action] + self.main.search_toolbar_actions = [find_action, find_next_action, replace_action] @@ -1081,6 +1085,10 @@ def get_plugin_actions(self): self.text_lowercase_action] # ---- Search menu/toolbar construction ---- + if not hasattr(self.main, 'search_menu_actions'): + # This list will not exist in the fast tests. + self.main.search_menu_actions = [] + self.main.search_menu_actions = ( search_menu_actions + self.main.search_menu_actions) diff --git a/spyder/plugins/explorer/plugin.py b/spyder/plugins/explorer/plugin.py index 3ec04a6fe36..c643e360bc3 100644 --- a/spyder/plugins/explorer/plugin.py +++ b/spyder/plugins/explorer/plugin.py @@ -20,7 +20,8 @@ # Local imports from spyder.api.translations import get_translation from spyder.api.plugins import SpyderDockablePlugin, Plugins -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.plugins.explorer.widgets.main_widget import ExplorerWidget from spyder.plugins.explorer.confpage import ExplorerConfigPage @@ -152,7 +153,8 @@ class Explorer(SpyderDockablePlugin): # ---- SpyderDockablePlugin API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): """Return widget title""" return _("Files") @@ -210,6 +212,30 @@ def on_ipython_console_available(self): ipyconsole.run_script(fname, osp.dirname(fname), '', False, False, False, True, False)) + @on_plugin_teardown(plugin=Plugins.Editor) + def on_editor_teardown(self): + editor = self.get_plugin(Plugins.Editor) + + editor.sig_dir_opened.disconnect(self.chdir) + self.sig_file_created.disconnect() + self.sig_file_removed.disconnect(editor.removed) + self.sig_file_renamed.disconnect(editor.renamed) + self.sig_folder_removed.disconnect(editor.removed_tree) + self.sig_folder_renamed.disconnect(editor.renamed_tree) + self.sig_module_created.disconnect(editor.new) + self.sig_open_file_requested.disconnect(editor.load) + + @on_plugin_teardown(plugin=Plugins.Preferences) + def on_preferences_teardown(self): + preferences = self.get_plugin(Plugins.Preferences) + preferences.deregister_plugin_preferences(self) + + @on_plugin_teardown(plugin=Plugins.IPythonConsole) + def on_ipython_console_teardown(self): + ipyconsole = self.get_plugin(Plugins.IPythonConsole) + self.sig_interpreter_opened.disconnect( + ipyconsole.create_client_from_path) + self.sig_run_requested.disconnect() # ---- Public API # ------------------------------------------------------------------------ diff --git a/spyder/plugins/findinfiles/plugin.py b/spyder/plugins/findinfiles/plugin.py index 2b798a8b2b2..2615203c486 100644 --- a/spyder/plugins/findinfiles/plugin.py +++ b/spyder/plugins/findinfiles/plugin.py @@ -13,7 +13,8 @@ # Local imports from spyder.api.plugins import Plugins, SpyderDockablePlugin -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.plugins.findinfiles.widgets import FindInFilesWidget from spyder.plugins.mainmenu.api import ApplicationMenus @@ -45,7 +46,8 @@ class FindInFiles(SpyderDockablePlugin): # --- SpyderDocakblePlugin API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _("Find") def get_description(self): @@ -91,6 +93,29 @@ def on_main_menu_available(self): menu_id=ApplicationMenus.Search, ) + @on_plugin_teardown(plugin=Plugins.Editor) + def on_editor_teardown(self): + widget = self.get_widget() + editor = self.get_plugin(Plugins.Editor) + widget.sig_edit_goto_requested.disconnect() + editor.sig_file_opened_closed_or_updated.disconnect( + self.set_current_opened_file) + + @on_plugin_teardown(plugin=Plugins.Projects) + def on_projects_teardon_plugin_teardown(self): + projects = self.get_plugin(Plugins.Projects) + projects.sig_project_loaded.disconnect(self.set_project_path) + projects.sig_project_closed.disconnect(self.unset_project_path) + + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_main_menu_teardown(self): + mainmenu = self.get_plugin(Plugins.MainMenu) + + mainmenu.remove_item_from_application_menu( + FindInFilesActions.FindInFiles, + menu_id=ApplicationMenus.Search, + ) + def on_close(self, cancelable=False): self.get_widget()._update_options() self.get_widget()._stop_and_reset_thread(ignore_results=True) diff --git a/spyder/plugins/help/plugin.py b/spyder/plugins/help/plugin.py index c2411f0e0bc..62b64527acd 100644 --- a/spyder/plugins/help/plugin.py +++ b/spyder/plugins/help/plugin.py @@ -17,7 +17,8 @@ # Local imports from spyder import __docs_url__, __forum_url__, __trouble_url__ from spyder.api.plugins import Plugins, SpyderDockablePlugin -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.config.base import get_conf_path from spyder.config.fonts import DEFAULT_SMALL_DELTA @@ -62,7 +63,8 @@ class Help(SpyderDockablePlugin): # --- SpyderDocakblePlugin API # ----------------------------------------------------------------------- - def get_name(self): + @staticmethod + def get_name(): return _('Help') def get_description(self): @@ -126,8 +128,8 @@ def on_shortcuts_available(self): shortcuts = self.get_plugin(Plugins.Shortcuts) # See: spyder-ide/spyder#6992 - shortcuts.sig_shortcuts_updated.connect( - lambda: self.show_intro_message()) + self._show_intro_message = lambda: self.show_intro_message() + shortcuts.sig_shortcuts_updated.connect(self._show_intro_message) if self.is_plugin_available(Plugins.MainMenu): self._setup_menus() @@ -140,6 +142,46 @@ def on_main_menu_available(self): else: self._setup_menus() + @on_plugin_teardown(plugin=Plugins.Console) + def on_console_teardown(self): + widget = self.get_widget() + internal_console = self.get_plugin(Plugins.Console) + internal_console.sig_help_requested.disconnect(self.set_object_text) + widget.set_internal_console(None) + + @on_plugin_teardown(plugin=Plugins.Editor) + def on_editor_teardown(self): + editor = self.get_plugin(Plugins.Editor) + editor.sig_help_requested.disconnect(self.set_editor_doc) + + @on_plugin_teardown(plugin=Plugins.IPythonConsole) + def on_ipython_console_teardown(self): + ipyconsole = self.get_plugin(Plugins.IPythonConsole) + + ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget) + ipyconsole.sig_shellwidget_created.disconnect( + self.set_shellwidget) + ipyconsole.sig_render_plain_text_requested.disconnect( + self.show_plain_text) + ipyconsole.sig_render_rich_text_requested.disconnect( + self.show_rich_text) + + ipyconsole.sig_help_requested.disconnect(self.set_object_text) + + @on_plugin_teardown(plugin=Plugins.Preferences) + def on_preferences_teardown(self): + preferences = self.get_plugin(Plugins.Preferences) + preferences.deregister_plugin_preferences(self) + + @on_plugin_teardown(plugin=Plugins.Shortcuts) + def on_shortcuts_teardown(self): + shortcuts = self.get_plugin(Plugins.Shortcuts) + shortcuts.sig_shortcuts_updated.disconnect(self._show_intro_message) + + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_main_menu_teardown(self): + self._remove_menus() + def update_font(self): color_scheme = self.get_color_scheme() font = self.get_font() @@ -189,6 +231,13 @@ def _setup_menus(self): before=shortcuts_summary_action, before_section=HelpMenuSections.Support) + def _remove_menus(self): + from spyder.plugins.mainmenu.api import ApplicationMenus + mainmenu = self.get_plugin(Plugins.MainMenu) + mainmenu.remove_item_from_application_menu( + HelpActions.ShowSpyderTutorialAction, + menu_id=ApplicationMenus.Help) + # --- Public API # ------------------------------------------------------------------------ def set_shellwidget(self, shellwidget): diff --git a/spyder/plugins/help/widgets.py b/spyder/plugins/help/widgets.py index 12bc411b5d8..c3fa1cef088 100644 --- a/spyder/plugins/help/widgets.py +++ b/spyder/plugins/help/widgets.py @@ -1143,4 +1143,5 @@ def set_internal_console(self, console): Console plugin. """ self.internal_console = console - self.internal_shell = console.get_widget().shell + if self.internal_console is not None: + self.internal_shell = console.get_widget().shell diff --git a/spyder/plugins/history/plugin.py b/spyder/plugins/history/plugin.py index 40173f18661..b8d50b78981 100644 --- a/spyder/plugins/history/plugin.py +++ b/spyder/plugins/history/plugin.py @@ -13,7 +13,8 @@ # Local imports from spyder.api.plugins import Plugins, SpyderDockablePlugin -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.plugins.history.confpage import HistoryConfigPage from spyder.plugins.history.widgets import HistoryWidget @@ -45,7 +46,8 @@ class HistoryLog(SpyderDockablePlugin): # --- SpyderDockablePlugin API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _('History') def get_description(self): @@ -68,6 +70,16 @@ def on_console_available(self): console = self.get_plugin(Plugins.Console) console.sig_refreshed.connect(self.refresh) + @on_plugin_teardown(plugin=Plugins.Preferences) + def on_preferences_teardown(self): + preferences = self.get_plugin(Plugins.Preferences) + preferences.deregister_plugin_preferences(self) + + @on_plugin_teardown(plugin=Plugins.Console) + def on_console_teardown(self): + console = self.get_plugin(Plugins.Console) + console.sig_refreshed.disconnect(self.refresh) + def update_font(self): color_scheme = self.get_color_scheme() font = self.get_font() diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py index 6a7e3503c7b..5eaa3cd5bb6 100644 --- a/spyder/plugins/ipythonconsole/plugin.py +++ b/spyder/plugins/ipythonconsole/plugin.py @@ -520,8 +520,11 @@ def toggle_view(self, checked): self.dockwidget.hide() #------ SpyderPluginWidget API -------------------------------------------- - def get_plugin_title(self): + @staticmethod + def get_plugin_title(): """Return widget title""" + # TODO: This is a temporary measure to get the title of this plugin + # without creating an instance return _('IPython console') def get_plugin_icon(self): @@ -760,7 +763,7 @@ def register_plugin(self): self.main.sig_pythonpath_changed.connect(self.update_path) # Show history file if no console is visible - if not self._isvisible and self.main.historylog: + if not self._isvisible and hasattr(self.main, 'historylog'): self.main.historylog.add_history(get_conf_path('history.py')) #------ Public API (for clients) ------------------------------------------ diff --git a/spyder/plugins/layout/container.py b/spyder/plugins/layout/container.py index e25f055788e..18b2c6f47b4 100644 --- a/spyder/plugins/layout/container.py +++ b/spyder/plugins/layout/container.py @@ -58,6 +58,11 @@ class LayoutContainerActions: LockDockwidgetsAndToolbars = 'Lock unlock panes' +class LayoutPluginMenus: + PluginsMenu = "plugins_menu" + LayoutsMenu = 'layouts_menu' + + class LayoutContainer(PluginMainContainer): """ Plugin container class that handles the Spyder quick layouts functionality. @@ -155,10 +160,10 @@ def setup(self): # Layouts menu self._layouts_menu = self.create_menu( - "layouts_menu", _("Window layouts")) + LayoutPluginMenus.LayoutsMenu, _("Window layouts")) self._plugins_menu = self.create_menu( - "plugins_menu", _("Panes")) + LayoutPluginMenus.PluginsMenu, _("Panes")) self._plugins_menu.setObjectName('checkbox-padding') def update_actions(self): diff --git a/spyder/plugins/layout/plugin.py b/spyder/plugins/layout/plugin.py index d48b4d5b1e0..c2abf8e7066 100644 --- a/spyder/plugins/layout/plugin.py +++ b/spyder/plugins/layout/plugin.py @@ -18,11 +18,13 @@ # Local imports from spyder.api.exceptions import SpyderAPIError from spyder.api.plugins import Plugins, SpyderPluginV2 -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.api.utils import get_class_values from spyder.plugins.mainmenu.api import ApplicationMenus, ViewMenuSections -from spyder.plugins.layout.container import LayoutContainer +from spyder.plugins.layout.container import ( + LayoutContainer, LayoutContainerActions, LayoutPluginMenus) from spyder.plugins.layout.layouts import (DefaultLayouts, HorizontalSplitLayout, MatlabLayout, RLayout, @@ -70,7 +72,8 @@ class Layout(SpyderPluginV2): # --- SpyderDockablePlugin API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _("Layout") def get_description(self): @@ -146,6 +149,43 @@ def on_toolbar_available(self): before=PreferencesActions.Show ) + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_main_menu_teardown(self): + mainmenu = self.get_plugin(Plugins.MainMenu) + # Remove Panes related actions from the View application menu + panes_items = [ + LayoutPluginMenus.PluginsMenu, + LayoutContainerActions.LockDockwidgetsAndToolbars, + LayoutContainerActions.CloseCurrentDockwidget, + LayoutContainerActions.MaximizeCurrentDockwidget] + for panes_item in panes_items: + mainmenu.remove_item_from_application_menu( + panes_item, + menu_id=ApplicationMenus.View) + # Remove layouts menu from the View application menu + layout_items = [ + LayoutPluginMenus.LayoutsMenu, + LayoutContainerActions.NextLayout, + LayoutContainerActions.PreviousLayout] + for layout_item in layout_items: + mainmenu.remove_item_from_application_menu( + layout_item, + menu_id=ApplicationMenus.View) + # Remove fullscreen action from the View application menu + mainmenu.remove_item_from_application_menu( + LayoutContainerActions.Fullscreen, + menu_id=ApplicationMenus.View) + + @on_plugin_teardown(plugin=Plugins.Toolbar) + def on_toolbar_teardown(self): + toolbars = self.get_plugin(Plugins.Toolbar) + + # Remove actions from the Main application toolbar + toolbars.remove_item_from_application_toolbar( + LayoutContainerActions.MaximizeCurrentDockwidget, + toolbar_id=ApplicationToolbars.Main + ) + def before_mainwindow_visible(self): # Update layout menu self.update_layout_menu_actions() diff --git a/spyder/plugins/maininterpreter/plugin.py b/spyder/plugins/maininterpreter/plugin.py index 8fb85779c3b..91ad3be6949 100644 --- a/spyder/plugins/maininterpreter/plugin.py +++ b/spyder/plugins/maininterpreter/plugin.py @@ -17,7 +17,8 @@ # Local imports from spyder.api.plugins import Plugins, SpyderPluginV2 -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.plugins.maininterpreter.confpage import MainInterpreterConfigPage from spyder.plugins.maininterpreter.container import MainInterpreterContainer @@ -41,7 +42,8 @@ class MainInterpreter(SpyderPluginV2): CONF_FILE = False # ---- SpyderPluginV2 API - def get_name(self): + @staticmethod + def get_name(): return _("Python interpreter") def get_description(self): @@ -87,8 +89,19 @@ def on_preferences_available(self): def on_statusbar_available(self): # Add status widget statusbar = self.get_plugin(Plugins.StatusBar) - if statusbar: - statusbar.add_status_widget(self.interpreter_status) + statusbar.add_status_widget(self.interpreter_status) + + @on_plugin_teardown(plugin=Plugins.Preferences) + def on_preferences_teardown(self): + # Deregister conf page + preferences = self.get_plugin(Plugins.Preferences) + preferences.deregister_plugin_preferences(self) + + @on_plugin_teardown(plugin=Plugins.StatusBar) + def on_statusbar_teardown(self): + # Add status widget + statusbar = self.get_plugin(Plugins.StatusBar) + statusbar.remove_status_widget(self.interpreter_status.ID) # ---- Public API def get_interpreter(self): diff --git a/spyder/plugins/mainmenu/plugin.py b/spyder/plugins/mainmenu/plugin.py index 672fff513b1..9b97421645a 100644 --- a/spyder/plugins/mainmenu/plugin.py +++ b/spyder/plugins/mainmenu/plugin.py @@ -40,7 +40,8 @@ class MainMenu(SpyderPluginV2): CONF_SECTION = NAME CONF_FILE = False - def get_name(self): + @staticmethod + def get_name(): return _('Main menus') def get_icon(self): @@ -264,6 +265,80 @@ def add_item_to_application_menu(self, item: ItemType, menu.add_action(item, section=section, before=before, before_section=before_section, omit_id=omit_id) + def remove_application_menu(self, menu_id: str): + """ + Remove a Spyder application menu. + + Parameters + ---------- + menu_id: str + The menu unique identifier string. + """ + if menu_id in self._APPLICATION_MENUS: + menu = self._APPLICATION_MENUS.pop(menu_id) + self.main.menuBar().removeAction(menu.menuAction()) + + def remove_item_from_application_menu(self, item_id: str, + menu_id: Optional[str] = None): + """ + Remove action or widget from given application menu by id. + + Parameters + ---------- + item_id: str + The item identifier to remove from the given menu. + menu_id: str or None + The application menu unique string identifier. + """ + if 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 = { + ApplicationMenus.Edit: ( + self._main.edit_menu_actions, self._main.edit_menu), + ApplicationMenus.Search: ( + self._main.search_menu_actions, self._main.search_menu), + ApplicationMenus.Source: ( + self._main.source_menu_actions, self._main.source_menu), + ApplicationMenus.Run: ( + self._main.run_menu_actions, self._main.run_menu), + ApplicationMenus.Debug: ( + self._main.debug_menu_actions, self._main.debug_menu), + } + + app_menus = { + ApplicationMenus.Edit: self._main.edit_menu, + ApplicationMenus.Search: self._main.search_menu, + ApplicationMenus.Source: self._main.source_menu, + ApplicationMenus.Run: self._main.run_menu, + ApplicationMenus.Debug: self._main.debug_menu + } + + menu = self.get_application_menu(menu_id) + + if menu_id in app_menu_actions: + actions = app_menu_actions[menu_id] # type: list + menu = app_menus[menu_id] + position = None + for i, action in enumerate(actions): + this_item_id = None + if (isinstance(action, SpyderAction) or + hasattr(action, 'action_id')): + this_item_id = action.action_id + elif (isinstance(action, SpyderMenu) or + hasattr(action, 'menu_id')): + this_item_id = action.menu_id + if this_item_id is not None and this_item_id == item_id: + position = i + break + if position is not None: + actions.pop(position) + menu.remove_action(item_id) + else: + menu.remove_action(item_id) + def get_application_menu(self, menu_id: str) -> SpyderMenu: """ Return an application menu by menu unique id. diff --git a/spyder/plugins/onlinehelp/plugin.py b/spyder/plugins/onlinehelp/plugin.py index ba9dd218574..664c7fab4fd 100644 --- a/spyder/plugins/onlinehelp/plugin.py +++ b/spyder/plugins/onlinehelp/plugin.py @@ -45,7 +45,8 @@ class OnlineHelp(SpyderDockablePlugin): # --- SpyderDockablePlugin API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _('Online help') def get_description(self): diff --git a/spyder/plugins/outlineexplorer/plugin.py b/spyder/plugins/outlineexplorer/plugin.py index a4fb71b3f2b..3839f01e0a5 100644 --- a/spyder/plugins/outlineexplorer/plugin.py +++ b/spyder/plugins/outlineexplorer/plugin.py @@ -10,7 +10,8 @@ from qtpy.QtCore import Slot # Local imports -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.api.plugins import SpyderDockablePlugin, Plugins from spyder.plugins.outlineexplorer.main_widget import OutlineExplorerWidget @@ -30,7 +31,8 @@ class OutlineExplorer(SpyderDockablePlugin): # ---- SpyderDockablePlugin API # ------------------------------------------------------------------------ - def get_name(self) -> str: + @staticmethod + def get_name() -> str: """Return widget title.""" return _('Outline Explorer') @@ -63,6 +65,22 @@ def on_editor_available(self): editor.sig_open_files_finished.connect( self.update_all_editors) + @on_plugin_teardown(plugin=Plugins.Completions) + def on_completions_teardown(self): + completions = self.get_plugin(Plugins.Completions) + + completions.sig_language_completions_available.disconnect( + self.start_symbol_services) + completions.sig_stop_completions.disconnect( + self.stop_symbol_services) + + @on_plugin_teardown(plugin=Plugins.Editor) + def on_editor_teardown(self): + editor = self.get_plugin(Plugins.Editor) + + editor.sig_open_files_finished.disconnect( + self.update_all_editors) + #------ Public API --------------------------------------------------------- def restore_scrollbar_position(self): """Restoring scrollbar position after main window is visible""" diff --git a/spyder/plugins/plots/plugin.py b/spyder/plugins/plots/plugin.py index 959f9724664..629de029fc0 100644 --- a/spyder/plugins/plots/plugin.py +++ b/spyder/plugins/plots/plugin.py @@ -13,7 +13,8 @@ # Local imports from spyder.api.plugins import Plugins, SpyderDockablePlugin -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.plugins.plots.widgets.main_widget import PlotsWidget @@ -36,7 +37,8 @@ class Plots(SpyderDockablePlugin): # ---- SpyderDockablePlugin API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _('Plots') def get_description(self): @@ -62,14 +64,16 @@ def on_ipython_console_available(self): ipyconsole.sig_shellwidget_deleted.connect( self.remove_shellwidget) - def unregister(self): + @on_plugin_teardown(plugin=Plugins.IPythonConsole) + def on_ipython_console_teardown(self): # Plugins ipyconsole = self.get_plugin(Plugins.IPythonConsole) # Signals + ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget) ipyconsole.sig_shellwidget_created.disconnect( self.add_shellwidget) - ipyconsole.sig_shellwidget_deleted.connect( + ipyconsole.sig_shellwidget_deleted.disconnect( self.remove_shellwidget) # ---- Public API diff --git a/spyder/plugins/preferences/api.py b/spyder/plugins/preferences/api.py index 23bdaa32a3b..cd266acaa6c 100644 --- a/spyder/plugins/preferences/api.py +++ b/spyder/plugins/preferences/api.py @@ -10,6 +10,7 @@ # Standard library imports import ast +import functools import os.path as osp # Third party imports @@ -211,25 +212,27 @@ def load_from_conf(self): """Load settings from configuration file.""" for checkbox, (sec, option, default) in list(self.checkboxes.items()): checkbox.setChecked(self.get_option(option, default, section=sec)) - checkbox.clicked.connect(lambda _, opt=option: - self.has_been_modified(opt)) + checkbox.clicked.connect(lambda _, opt=option, sect=sec: + self.has_been_modified(sect, opt)) + if checkbox.restart_required: + self.restart_options[(sec, option)] = checkbox.text() for radiobutton, (sec, option, default) in list( self.radiobuttons.items()): radiobutton.setChecked(self.get_option(option, default, section=sec)) - radiobutton.toggled.connect(lambda _foo, opt=option: - self.has_been_modified(opt)) + radiobutton.toggled.connect(lambda _foo, opt=option, sect=sec: + self.has_been_modified(sect, opt)) if radiobutton.restart_required: - self.restart_options[option] = radiobutton.label_text + self.restart_options[(sec, option)] = radiobutton.label_text for lineedit, (sec, option, default) in list(self.lineedits.items()): data = self.get_option(option, default, section=sec) if getattr(lineedit, 'content_type', None) == list: data = ', '.join(data) lineedit.setText(data) - lineedit.textChanged.connect(lambda _, opt=option: - self.has_been_modified(opt)) + lineedit.textChanged.connect(lambda _, opt=option, sect=sec: + self.has_been_modified(sect, opt)) if lineedit.restart_required: - self.restart_options[option] = lineedit.label_text + self.restart_options[(sec, option)] = lineedit.label_text for textedit, (sec, option, default) in list(self.textedits.items()): data = self.get_option(option, default, section=sec) if getattr(textedit, 'content_type', None) == list: @@ -237,14 +240,14 @@ def load_from_conf(self): elif getattr(textedit, 'content_type', None) == dict: data = to_text_string(data) textedit.setPlainText(data) - textedit.textChanged.connect(lambda opt=option: - self.has_been_modified(opt)) + textedit.textChanged.connect(lambda opt=option, sect=sec: + self.has_been_modified(sect, opt)) if textedit.restart_required: - self.restart_options[option] = textedit.label_text + self.restart_options[(sec, option)] = textedit.label_text for spinbox, (sec, option, default) in list(self.spinboxes.items()): spinbox.setValue(self.get_option(option, default, section=sec)) - spinbox.valueChanged.connect(lambda _foo, opt=option: - self.has_been_modified(opt)) + spinbox.valueChanged.connect(lambda _foo, opt=option, sect=sec: + self.has_been_modified(sect, opt)) for combobox, (sec, option, default) in list(self.comboboxes.items()): value = self.get_option(option, default, section=sec) for index in range(combobox.count()): @@ -259,10 +262,11 @@ def load_from_conf(self): index = None if index: combobox.setCurrentIndex(index) - combobox.currentIndexChanged.connect(lambda _foo, opt=option: - self.has_been_modified(opt)) + combobox.currentIndexChanged.connect(lambda _foo, opt=option, sect=sec: + self.has_been_modified( + sect, opt)) if combobox.restart_required: - self.restart_options[option] = combobox.label_text + self.restart_options[(sec, option)] = combobox.label_text for (fontbox, sizebox), option in list(self.fontboxes.items()): rich_font = True if "rich" in option.lower() else False @@ -274,9 +278,11 @@ def load_from_conf(self): else: property = option fontbox.currentIndexChanged.connect(lambda _foo, opt=property: - self.has_been_modified(opt)) + self.has_been_modified( + self.CONF_SECTION, opt)) sizebox.valueChanged.connect(lambda _foo, opt=property: - self.has_been_modified(opt)) + self.has_been_modified( + self.CONF_SECTION, opt)) for clayout, (sec, option, default) in list(self.coloredits.items()): property = to_qvariant(option) edit = clayout.lineedit @@ -284,13 +290,13 @@ def load_from_conf(self): edit.setText(self.get_option(option, default, section=sec)) # QAbstractButton works differently for PySide and PyQt if not API == 'pyside': - btn.clicked.connect(lambda _foo, opt=option: - self.has_been_modified(opt)) + btn.clicked.connect(lambda _foo, opt=option, sect=sec: + self.has_been_modified(sect, opt)) else: - btn.clicked.connect(lambda opt=option: - self.has_been_modified(opt)) - edit.textChanged.connect(lambda _foo, opt=option: - self.has_been_modified(opt)) + btn.clicked.connect(lambda opt=option, sect=sec: + self.has_been_modified(sect, opt)) + edit.textChanged.connect(lambda _foo, opt=option, sect=sec: + self.has_been_modified(sect, opt)) for (clayout, cb_bold, cb_italic ), (sec, option, default) in list(self.scedits.items()): edit = clayout.lineedit @@ -302,39 +308,39 @@ def load_from_conf(self): cb_bold.setChecked(bold) cb_italic.setChecked(italic) - edit.textChanged.connect(lambda _foo, opt=option: - self.has_been_modified(opt)) + edit.textChanged.connect(lambda _foo, opt=option, sect=sec: + self.has_been_modified(sect, opt)) # QAbstractButton works differently for PySide and PyQt if not API == 'pyside': - btn.clicked.connect(lambda _foo, opt=option: - self.has_been_modified(opt)) - cb_bold.clicked.connect(lambda _foo, opt=option: - self.has_been_modified(opt)) - cb_italic.clicked.connect(lambda _foo, opt=option: - self.has_been_modified(opt)) + btn.clicked.connect(lambda _foo, opt=option, sect=sec: + self.has_been_modified(sect, opt)) + cb_bold.clicked.connect(lambda _foo, opt=option, sect=sec: + self.has_been_modified(sect, opt)) + cb_italic.clicked.connect(lambda _foo, opt=option, sect=sec: + self.has_been_modified(sect, opt)) else: - btn.clicked.connect(lambda opt=option: - self.has_been_modified(opt)) - cb_bold.clicked.connect(lambda opt=option: - self.has_been_modified(opt)) - cb_italic.clicked.connect(lambda opt=option: - self.has_been_modified(opt)) + btn.clicked.connect(lambda opt=option, sect=sec: + self.has_been_modified(sect, opt)) + cb_bold.clicked.connect(lambda opt=option, sect=sec: + self.has_been_modified(sect, opt)) + cb_italic.clicked.connect(lambda opt=option, sect=sec: + self.has_been_modified(sect, opt)) def save_to_conf(self): """Save settings to configuration file""" for checkbox, (sec, option, _default) in list( self.checkboxes.items()): - if option in self.changed_options: + if (sec, option) in self.changed_options: value = checkbox.isChecked() self.set_option(option, value, section=sec, recursive_notification=False) for radiobutton, (sec, option, _default) in list( self.radiobuttons.items()): - if option in self.changed_options: + if (sec, option) in self.changed_options: self.set_option(option, radiobutton.isChecked(), section=sec, recursive_notification=False) for lineedit, (sec, option, _default) in list(self.lineedits.items()): - if option in self.changed_options: + if (sec, option) in self.changed_options: data = lineedit.text() content_type = getattr(lineedit, 'content_type', None) if content_type == list: @@ -344,7 +350,7 @@ def save_to_conf(self): self.set_option(option, data, section=sec, recursive_notification=False) for textedit, (sec, option, _default) in list(self.textedits.items()): - if option in self.changed_options: + if (sec, option) in self.changed_options: data = textedit.toPlainText() content_type = getattr(textedit, 'content_type', None) if content_type == dict: @@ -359,27 +365,27 @@ def save_to_conf(self): self.set_option(option, data, section=sec, recursive_notification=False) for spinbox, (sec, option, _default) in list(self.spinboxes.items()): - if option in self.changed_options: + if (sec, option) in self.changed_options: self.set_option(option, spinbox.value(), section=sec, recursive_notification=False) for combobox, (sec, option, _default) in list(self.comboboxes.items()): - if option in self.changed_options: + if (sec, option) in self.changed_options: data = combobox.itemData(combobox.currentIndex()) self.set_option(option, from_qvariant(data, to_text_string), section=sec, recursive_notification=False) for (fontbox, sizebox), option in list(self.fontboxes.items()): - if option in self.changed_options: + if (self.CONF_SECTION, option) in self.changed_options: font = fontbox.currentFont() font.setPointSize(sizebox.value()) self.set_font(font, option) for clayout, (sec, option, _default) in list(self.coloredits.items()): - if option in self.changed_options: + if (sec, option) in self.changed_options: self.set_option(option, to_text_string(clayout.lineedit.text()), section=sec, recursive_notification=False) for (clayout, cb_bold, cb_italic), (sec, option, _default) in list( self.scedits.items()): - if option in self.changed_options: + if (sec, option) in self.changed_options: color = to_text_string(clayout.lineedit.text()) bold = cb_bold.isChecked() italic = cb_italic.isChecked() @@ -387,13 +393,13 @@ def save_to_conf(self): recursive_notification=False) @Slot(str) - def has_been_modified(self, option): + def has_been_modified(self, section, option): self.set_modified(True) - self.changed_options.add(option) + self.changed_options.add((section, option)) def create_checkbox(self, text, option, default=NoDefault, tip=None, msg_warning=None, msg_info=None, - msg_if_enabled=False, section=None): + msg_if_enabled=False, section=None, restart=False): checkbox = QCheckBox(text) self.checkboxes[checkbox] = (section, option, default) if section is not None and section != self.CONF_SECTION: @@ -410,6 +416,7 @@ def show_message(is_checked=False): QMessageBox.information(self, self.get_name(), msg_info, QMessageBox.Ok) checkbox.clicked.connect(show_message) + checkbox.restart_required = restart return checkbox def create_radiobutton(self, text, option, default=NoDefault, @@ -773,7 +780,8 @@ def create_button(self, text, callback): btn = QPushButton(text) btn.clicked.connect(callback) btn.clicked.connect( - lambda checked=False, opt='': self.has_been_modified(opt)) + lambda checked=False, opt='': self.has_been_modified( + self.CONF_SECTION, opt)) return btn def create_tab(self, *widgets): @@ -809,7 +817,11 @@ def prompt_restart_required(self): answer = QMessageBox.information(self, msg_title, msg, QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: - self.main.application.sig_restart_requested.emit() + self.restart() + + def restart(self): + """Restart Spyder.""" + self.main.restart(close_immediately=True) def add_tab(self, Widget): widget = Widget(self) diff --git a/spyder/plugins/preferences/plugin.py b/spyder/plugins/preferences/plugin.py index b755a4f0e94..44370d58215 100644 --- a/spyder/plugins/preferences/plugin.py +++ b/spyder/plugins/preferences/plugin.py @@ -25,12 +25,14 @@ # Local imports from spyder.api.plugins import Plugins, SpyderPluginV2, SpyderPlugin -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.config.base import _ from spyder.config.main import CONF_VERSION from spyder.config.user import NoDefault from spyder.plugins.mainmenu.api import ApplicationMenus, ToolsMenuSections -from spyder.plugins.preferences.widgets.container import PreferencesContainer +from spyder.plugins.preferences.widgets.container import ( + PreferencesActions, PreferencesContainer) from spyder.plugins.toolbar.api import ApplicationToolbars, MainToolbarSections logger = logging.getLogger(__name__) @@ -255,7 +257,8 @@ def open_dialog(self, prefs_dialog_size): self.get_main()) # ---------------- Public Spyder API required methods --------------------- - def get_name(self) -> str: + @staticmethod + def get_name() -> str: return _('Preferences') def get_description(self) -> str: @@ -303,6 +306,36 @@ def on_application_available(self): container = self.get_container() container.sig_reset_preferences_requested.connect(self.reset) + + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_main_menu_teardown(self): + container = self.get_container() + main_menu = self.get_plugin(Plugins.MainMenu) + + main_menu.remove_item_from_application_menu( + PreferencesActions.Show, + menu_id=ApplicationMenus.Tools, + ) + + main_menu.remove_item_from_application_menu( + PreferencesActions.Reset, + menu_id=ApplicationMenus.Tools, + ) + + @on_plugin_teardown(plugin=Plugins.Toolbar) + def on_toolbar_teardown(self): + container = self.get_container() + toolbar = self.get_plugin(Plugins.Toolbar) + toolbar.remove_item_from_application_toolbar( + PreferencesActions.Show, + toolbar_id=ApplicationToolbars.Main + ) + + @on_plugin_teardown(plugin=Plugins.Application) + def on_application_teardown(self): + container = self.get_container() + container.sig_reset_preferences_requested.disconnect(self.reset) + @Slot() def reset(self): answer = QMessageBox.warning(self.main, _("Warning"), @@ -314,6 +347,6 @@ def reset(self): application = self.get_plugin(Plugins.Application) application.sig_restart_requested.emit() - def on_close(self, cancelable=False) -> bool: + def can_close(self) -> bool: container = self.get_container() return not container.is_dialog_open() diff --git a/spyder/plugins/profiler/plugin.py b/spyder/plugins/profiler/plugin.py index 884328032be..e19bc89209d 100644 --- a/spyder/plugins/profiler/plugin.py +++ b/spyder/plugins/profiler/plugin.py @@ -16,7 +16,8 @@ # Local imports from spyder.api.plugins import Plugins, SpyderDockablePlugin -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.plugins.mainmenu.api import ApplicationMenus from spyder.plugins.profiler.confpage import ProfilerConfigPage @@ -61,7 +62,8 @@ class Profiler(SpyderDockablePlugin): # --- SpyderDockablePlugin API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _("Profiler") def get_description(self): @@ -105,6 +107,26 @@ def on_main_menu_available(self): mainmenu.add_item_to_application_menu( run_action, menu_id=ApplicationMenus.Run) + @on_plugin_teardown(plugin=Plugins.Editor) + def on_editor_teardown(self): + widget = self.get_widget() + editor = self.get_plugin(Plugins.Editor) + widget.sig_edit_goto_requested.disconnect(editor.load) + + @on_plugin_teardown(plugin=Plugins.Preferences) + def on_preferences_teardown(self): + preferences = self.get_plugin(Plugins.Preferences) + preferences.deregister_plugin_preferences(self) + + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_main_menu_teardown(self): + mainmenu = self.get_plugin(Plugins.MainMenu) + + mainmenu.remove_item_from_application_menu( + ProfilerActions.ProfileCurrentFile, + menu_id=ApplicationMenus.Run + ) + # --- Public API # ------------------------------------------------------------------------ def run_profiler(self): diff --git a/spyder/plugins/projects/plugin.py b/spyder/plugins/projects/plugin.py index 8287ddd70e4..3188ace10f6 100644 --- a/spyder/plugins/projects/plugin.py +++ b/spyder/plugins/projects/plugin.py @@ -26,7 +26,8 @@ # Local imports from spyder.api.exceptions import SpyderAPIError -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.api.plugins import Plugins, SpyderDockablePlugin from spyder.config.base import (get_home_dir, get_project_config_folder, @@ -137,7 +138,8 @@ def __init__(self, parent=None, configuration=None): # ---- SpyderDockablePlugin API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _("Project") def get_description(self): @@ -188,18 +190,14 @@ def on_editor_available(self): treewidget.sig_renamed.connect(self.editor.renamed) treewidget.sig_tree_renamed.connect(self.editor.renamed_tree) treewidget.sig_module_created.connect(self.editor.new) - treewidget.sig_file_created.connect( - lambda t: self.editor.new(text=t)) + treewidget.sig_file_created.connect(self._new_editor) + + self.sig_project_loaded.connect(self._setup_editor_files) + self.sig_project_closed[bool].connect(self._setup_editor_files) - self.sig_project_loaded.connect( - lambda v: self.editor.setup_open_files()) - self.sig_project_closed[bool].connect( - lambda v: self.editor.setup_open_files()) self.editor.set_projects(self) - self.sig_project_loaded.connect( - lambda v: self.editor.set_current_project_path(v)) - self.sig_project_closed.connect( - lambda v: self.editor.set_current_project_path()) + self.sig_project_loaded.connect(self._set_path_in_editor) + self.sig_project_closed.connect(self._unset_path_in_editor) @on_plugin_available(plugin=Plugins.Completions) def on_completions_available(self): @@ -212,29 +210,17 @@ def on_completions_available(self): # self.start_workspace_services()) self.completions.sig_stop_completions.connect( self.stop_workspace_services) - self.sig_project_loaded.connect( - functools.partial(self.completions.project_path_update, - update_kind=WorkspaceUpdateKind.ADDITION, - instance=self)) - self.sig_project_closed.connect( - functools.partial(self.completions.project_path_update, - update_kind=WorkspaceUpdateKind.DELETION, - instance=self)) + self.sig_project_loaded.connect(self._add_path_to_completions) + self.sig_project_closed.connect(self._remove_path_from_completions) @on_plugin_available(plugin=Plugins.IPythonConsole) def on_ipython_console_available(self): self.ipyconsole = self.get_plugin(Plugins.IPythonConsole) widget = self.get_widget() treewidget = widget.treewidget - treewidget.sig_open_interpreter_requested.connect( self.ipyconsole.create_client_from_path) - treewidget.sig_run_requested.connect( - lambda fname: - self.ipyconsole.run_script( - fname, osp.dirname(fname), '', False, False, False, True, - False) - ) + treewidget.sig_run_requested.connect(self._run_file_in_ipyconsole) @on_plugin_available(plugin=Plugins.MainMenu) def on_main_menu_available(self): @@ -263,6 +249,58 @@ def on_main_menu_available(self): menu_id=ApplicationMenus.Projects, section=ProjectsMenuSections.Extras) + @on_plugin_teardown(plugin=Plugins.Editor) + def on_editor_teardown(self): + self.editor = self.get_plugin(Plugins.Editor) + widget = self.get_widget() + treewidget = widget.treewidget + + treewidget.sig_open_file_requested.disconnect(self.editor.load) + treewidget.sig_removed.disconnect(self.editor.removed) + treewidget.sig_tree_removed.disconnect(self.editor.removed_tree) + treewidget.sig_renamed.disconnect(self.editor.renamed) + treewidget.sig_tree_renamed.disconnect(self.editor.renamed_tree) + treewidget.sig_module_created.disconnect(self.editor.new) + treewidget.sig_file_created.disconnect(self._new_editor) + + self.sig_project_loaded.disconnect(self._setup_editor_files) + self.sig_project_closed[bool].disconnect(self._setup_editor_files) + self.editor.set_projects(None) + self.sig_project_loaded.disconnect(self._set_path_in_editor) + self.sig_project_closed.disconnect(self._unset_path_in_editor) + + self.editor = None + + @on_plugin_teardown(plugin=Plugins.Completions) + def on_completions_teardown(self): + self.completions = self.get_plugin(Plugins.Completions) + + self.completions.sig_stop_completions.disconnect( + self.stop_workspace_services) + + self.sig_project_loaded.disconnect(self._add_path_to_completions) + self.sig_project_closed.disconnect(self._remove_path_from_completions) + + self.completions = None + + @on_plugin_teardown(plugin=Plugins.IPythonConsole) + def on_ipython_console_teardown(self): + self.ipyconsole = self.get_plugin(Plugins.IPythonConsole) + widget = self.get_widget() + treewidget = widget.treewidget + + treewidget.sig_open_interpreter_requested.disconnect( + self.ipyconsole.create_client_from_path) + treewidget.sig_run_requested.disconnect(self._run_file_in_ipyconsole) + + self._ipython_run_script = None + self.ipyconsole = None + + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_main_menu_teardown(self): + main_menu = self.get_plugin(Plugins.MainMenu) + main_menu.remove_application_menu(ApplicationMenus.Projects) + def setup(self): """Setup the plugin actions.""" self.create_action( @@ -904,3 +942,37 @@ def get_project_types(self): are project type classes. """ return self._project_types + + # --- Private API + # ------------------------------------------------------------------------- + def _new_editor(self, text): + self.editor.new(text=text) + + def _setup_editor_files(self, __unused): + self.editor.setup_open_files() + + def _set_path_in_editor(self, path): + self.editor.set_current_project_path(path) + + def _unset_path_in_editor(self, __unused): + self.editor.set_current_project_path() + + def _add_path_to_completions(self, path): + self.completions.project_path_update( + path, + update_kind=WorkspaceUpdateKind.ADDITION, + instance=self + ) + + def _remove_path_from_completions(self, path): + self.completions.project_path_update( + path, + update_kind=WorkspaceUpdateKind.DELETION, + instance=self + ) + + def _run_file_in_ipyconsole(self, fname): + self.ipyconsole.run_script( + fname, osp.dirname(fname), '', False, False, False, True, + False + ) diff --git a/spyder/plugins/pylint/plugin.py b/spyder/plugins/pylint/plugin.py index 752f623d285..1adf79faeee 100644 --- a/spyder/plugins/pylint/plugin.py +++ b/spyder/plugins/pylint/plugin.py @@ -15,8 +15,10 @@ from qtpy.QtCore import Qt, Signal, Slot # Local imports +from spyder.api.exceptions import SpyderAPIError from spyder.api.plugins import Plugins, SpyderDockablePlugin -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.utils.programs import is_module_installed from spyder.plugins.mainmenu.api import ApplicationMenus @@ -60,7 +62,8 @@ class Pylint(SpyderDockablePlugin): Word to select on given row. """ - def get_name(self): + @staticmethod + def get_name(): return _("Code Analysis") def get_description(self): @@ -116,10 +119,9 @@ def on_projects_available(self): # Connect to projects projects = self.get_plugin(Plugins.Projects) - projects.sig_project_loaded.connect( - lambda value: widget.set_conf("project_dir", value)) - projects.sig_project_closed.connect( - lambda value: widget.set_conf("project_dir", None)) + + projects.sig_project_loaded.connect(self._set_project_dir) + projects.sig_project_closed.connect(self._unset_project_dir) @on_plugin_available(plugin=Plugins.MainMenu) def on_main_menu_available(self): @@ -129,6 +131,41 @@ def on_main_menu_available(self): mainmenu.add_item_to_application_menu( pylint_act, menu_id=ApplicationMenus.Source) + @on_plugin_teardown(plugin=Plugins.Editor) + def on_editor_teardown(self): + widget = self.get_widget() + editor = self.get_plugin(Plugins.Editor) + + # Connect to Editor + widget.sig_edit_goto_requested.disconnect(editor.load) + editor.sig_editor_focus_changed.disconnect(self._set_filename) + + pylint_act = self.get_action(PylintActions.AnalyzeCurrentFile) + + # TODO: use new API when editor has migrated + pylint_act.setVisible(False) + editor.pythonfile_dependent_actions.remove(pylint_act) + + @on_plugin_teardown(plugin=Plugins.Preferences) + def on_preferences_teardown(self): + preferences = self.get_plugin(Plugins.Preferences) + preferences.deregister_plugin_preferences(self) + + @on_plugin_teardown(plugin=Plugins.Projects) + def on_projects_teardown(self): + # Disconnect from projects + projects = self.get_plugin(Plugins.Projects) + projects.sig_project_loaded.disconnect(self._set_project_dir) + projects.sig_project_closed.disconnect(self._unset_project_dir) + + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_main_menu_teardown(self): + mainmenu = self.get_plugin(Plugins.MainMenu) + mainmenu.remove_item_from_application_menu( + PylintActions.AnalyzeCurrentFile, + menu_id=ApplicationMenus.Source + ) + # --- Private API # ------------------------------------------------------------------------ @Slot() @@ -136,9 +173,21 @@ def _set_filename(self): """ Set filename without code analysis. """ - editor = self.get_plugin(Plugins.Editor) - if editor: - self.get_widget().set_filename(editor.get_current_filename()) + try: + editor = self.get_plugin(Plugins.Editor) + if editor: + self.get_widget().set_filename(editor.get_current_filename()) + except SpyderAPIError: + # Editor was deleted + pass + + def _set_project_dir(self, value): + widget = self.get_widget() + widget.set_conf("project_dir", value) + + def _unset_project_dir(self, _unused): + widget = self.get_widget() + widget.set_conf("project_dir", None) # --- Public API # ------------------------------------------------------------------------ diff --git a/spyder/plugins/run/plugin.py b/spyder/plugins/run/plugin.py index 34b1a6c4a61..c8f196991ab 100644 --- a/spyder/plugins/run/plugin.py +++ b/spyder/plugins/run/plugin.py @@ -12,7 +12,8 @@ # Local imports from spyder.api.plugins import Plugins, SpyderPluginV2 -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.plugins.run.confpage import RunConfigPage @@ -37,7 +38,8 @@ class Run(SpyderPluginV2): # --- SpyderPluginV2 API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _("Run") def get_description(self): @@ -54,5 +56,10 @@ def on_preferences_available(self): preferences = self.get_plugin(Plugins.Preferences) preferences.register_plugin_preferences(self) + @on_plugin_teardown(plugin=Plugins.Preferences) + def on_preferences_teardown(self): + preferences = self.get_plugin(Plugins.Preferences) + preferences.deregister_plugin_preferences(self) + # --- Public API # ------------------------------------------------------------------------ diff --git a/spyder/plugins/shortcuts/plugin.py b/spyder/plugins/shortcuts/plugin.py index 72291f19873..360ddd25380 100644 --- a/spyder/plugins/shortcuts/plugin.py +++ b/spyder/plugins/shortcuts/plugin.py @@ -21,7 +21,8 @@ # Local imports from spyder.api.plugins import Plugins, SpyderPluginV2 -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.plugins.mainmenu.api import ApplicationMenus, HelpMenuSections from spyder.plugins.shortcuts.confpage import ShortcutsConfigPage @@ -60,7 +61,8 @@ class Shortcuts(SpyderPluginV2): # --- SpyderPluginV2 API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _("Keyboard shortcuts") def get_description(self): @@ -97,6 +99,19 @@ def on_main_menu_available(self): section=HelpMenuSections.Documentation, ) + @on_plugin_teardown(plugin=Plugins.Preferences) + def on_preferences_teardown(self): + preferences = self.get_plugin(Plugins.Preferences) + preferences.deregister_plugin_preferences(self) + + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_main_menu_teardown(self): + mainmenu = self.get_plugin(Plugins.MainMenu) + mainmenu.remove_item_from_application_menu( + ShortcutActions.ShortcutSummaryAction, + menu_id=ApplicationMenus.Help + ) + def on_mainwindow_visible(self): self.apply_shortcuts() diff --git a/spyder/plugins/statusbar/plugin.py b/spyder/plugins/statusbar/plugin.py index 58a5bc0cea7..1f38affb8fc 100644 --- a/spyder/plugins/statusbar/plugin.py +++ b/spyder/plugins/statusbar/plugin.py @@ -14,7 +14,8 @@ # Local imports from spyder.api.exceptions import SpyderAPIError from spyder.api.plugins import Plugins, SpyderPluginV2 -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.api.widgets.status import StatusBarWidget from spyder.config.base import running_under_pytest @@ -51,7 +52,8 @@ class StatusBar(SpyderPluginV2): 'vcs_status', 'interpreter_status', 'lsp_status', 'kite_status'} # ---- SpyderPluginV2 API - def get_name(self): + @staticmethod + def get_name(): return _('Status bar') def get_icon(self): @@ -67,11 +69,19 @@ def on_initialize(self): self.add_status_widget( self.clock_status, StatusBarWidgetPosition.Right) + def on_close(self, _unused): + self._statusbar.setVisible(False) + @on_plugin_available(plugin=Plugins.Preferences) def on_preferences_available(self): preferences = self.get_plugin(Plugins.Preferences) preferences.register_plugin_preferences(self) + @on_plugin_teardown(plugin=Plugins.Preferences) + def on_preferences_teardown(self): + preferences = self.get_plugin(Plugins.Preferences) + preferences.deregister_plugin_preferences(self) + def after_container_creation(self): container = self.get_container() container.sig_show_status_bar_requested.connect( diff --git a/spyder/plugins/toolbar/container.py b/spyder/plugins/toolbar/container.py index 338300947a5..d5b735ab4dd 100644 --- a/spyder/plugins/toolbar/container.py +++ b/spyder/plugins/toolbar/container.py @@ -212,6 +212,28 @@ def add_application_toolbar(self, toolbar, mainwindow=None): self._add_missing_toolbar_elements(toolbar, toolbar_id) + def remove_application_toolbar(self, toolbar_id: str, mainwindow=None): + """ + Remove toolbar from application toolbars. + + Parameters + ---------- + toolbar: str + The application toolbar to remove from the `mainwindow`. + mainwindow: QMainWindow + The main application window. + """ + + if toolbar_id not in self._ADDED_TOOLBARS: + raise SpyderAPIError( + 'Toolbar with ID "{}" is not in the main window'.format( + toolbar_id)) + + toolbar = self._ADDED_TOOLBARS.pop(toolbar_id) + self._toolbarslist.remove(toolbar) + + if mainwindow: + mainwindow.removeToolBar(toolbar) def add_item_to_application_toolbar(self, item: ToolbarItem, @@ -250,6 +272,25 @@ def add_item_to_application_toolbar(self, toolbar.add_item(item, section=section, before=before, before_section=before_section, omit_id=omit_id) + def remove_item_from_application_toolbar(self, item_id: str, + toolbar_id: Optional[str] = None): + """ + Remove action or widget from given application toolbar by id. + + Parameters + ---------- + item: str + The item to remove from the `toolbar`. + toolbar_id: str or None + The application toolbar unique string identifier. + """ + if toolbar_id not in self._APPLICATION_TOOLBARS: + raise SpyderAPIError( + '{} is not a valid toolbar_id'.format(toolbar_id)) + + toolbar = self.get_application_toolbar(toolbar_id) + toolbar.remove_item(item_id) + def get_application_toolbar(self, toolbar_id: str) -> ApplicationToolbar: """ Return an application toolbar by toolbar_id. @@ -304,6 +345,9 @@ def load_last_visible_toolbars(self): else: self._get_visible_toolbars() + for toolbar in self._visible_toolbars: + toolbar.setVisible(True) + self.update_actions() def create_toolbars_menu(self): diff --git a/spyder/plugins/toolbar/plugin.py b/spyder/plugins/toolbar/plugin.py index 90d0e72e44e..b7e796ed0e5 100644 --- a/spyder/plugins/toolbar/plugin.py +++ b/spyder/plugins/toolbar/plugin.py @@ -15,11 +15,13 @@ # Local imports from spyder.api.exceptions import SpyderAPIError from spyder.api.plugins import SpyderPluginV2, Plugins -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.plugins.mainmenu.api import ApplicationMenus, ViewMenuSections from spyder.plugins.toolbar.api import ApplicationToolbars -from spyder.plugins.toolbar.container import ToolbarContainer +from spyder.plugins.toolbar.container import ( + ToolbarContainer, ToolbarMenus, ToolbarActions) # Third-party imports from qtpy.QtWidgets import QWidget @@ -40,7 +42,8 @@ class Toolbar(SpyderPluginV2): # --- SpyderDocakblePlugin API # ----------------------------------------------------------------------- - def get_name(self): + @staticmethod + def get_name(): return _('Toolbar') def get_description(self): @@ -71,6 +74,17 @@ def on_main_menu_available(self): section=ViewMenuSections.Toolbar, before_section=ViewMenuSections.Layout) + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_main_menu_teardown(self): + mainmenu = self.get_plugin(Plugins.MainMenu) + # View menu Toolbar section + mainmenu.remove_item_from_application_menu( + ToolbarMenus.ToolbarsMenu, + menu_id=ApplicationMenus.View) + mainmenu.remove_item_from_application_menu( + ToolbarActions.ShowToolbars, + menu_id=ApplicationMenus.View) + def on_mainwindow_visible(self): container = self.get_container() @@ -103,8 +117,9 @@ def on_mainwindow_visible(self): def on_close(self, _unused): container = self.get_container() - if container._visible_toolbars: - container._save_visible_toolbars() + container._save_visible_toolbars() + for toolbar in container._visible_toolbars: + toolbar.setVisible(False) # --- Public API # ------------------------------------------------------------------------ @@ -143,6 +158,20 @@ def add_application_toolbar(self, toolbar): """ self.get_container().add_application_toolbar(toolbar, self._main) + def remove_application_toolbar(self, toolbar_id: str): + """ + Remove toolbar from the application toolbars. + + This can be used to remove a custom toolbar. The `WorkingDirectory` + plugin is an example of this. + + Parameters + ---------- + toolbar: str + The application toolbar to remove from the main window. + """ + self.get_container().remove_application_toolbar(toolbar_id, self._main) + def add_item_to_application_toolbar(self, item: Union[SpyderAction, QWidget], toolbar_id: Optional[str] = None, @@ -184,6 +213,23 @@ def add_item_to_application_toolbar(self, omit_id=omit_id ) + def remove_item_from_application_toolbar(self, item_id: str, + toolbar_id: Optional[str] = None): + """ + Remove action or widget `item` from given application menu by id. + + Parameters + ---------- + item_id: str + The item to remove from the toolbar. + toolbar_id: str or None + The application toolbar unique string identifier. + """ + self.get_container().remove_item_from_application_toolbar( + item_id, + toolbar_id=toolbar_id + ) + def get_application_toolbar(self, toolbar_id): """ Return an application toolbar by toolbar_id. diff --git a/spyder/plugins/tours/plugin.py b/spyder/plugins/tours/plugin.py index 8b041e1ce5c..59fbfe68927 100644 --- a/spyder/plugins/tours/plugin.py +++ b/spyder/plugins/tours/plugin.py @@ -12,11 +12,12 @@ # Local imports from spyder.api.plugins import Plugins, SpyderPluginV2 -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.config.base import get_safe_mode, running_under_pytest from spyder.plugins.application.api import ApplicationActions -from spyder.plugins.tours.container import ToursContainer +from spyder.plugins.tours.container import TourActions, ToursContainer from spyder.plugins.tours.tours import INTRO_TOUR, TourIdentifiers from spyder.plugins.mainmenu.api import ApplicationMenus, HelpMenuSections @@ -38,7 +39,8 @@ class Tours(SpyderPluginV2): # --- SpyderPluginV2 API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _("Interactive tours") def get_description(self): @@ -64,6 +66,13 @@ def on_main_menu_available(self): section=HelpMenuSections.Documentation, before=ApplicationActions.SpyderDocumentationAction) + @on_plugin_teardown(plugin=Plugins.MainMenu) + def on_main_menu_teardown(self): + mainmenu = self.get_plugin(Plugins.MainMenu) + mainmenu.remove_item_from_application_menu( + TourActions.ShowTour, + menu_id=ApplicationMenus.Help) + def on_mainwindow_visible(self): self.show_tour_message() diff --git a/spyder/plugins/variableexplorer/plugin.py b/spyder/plugins/variableexplorer/plugin.py index 279cc6c88e5..ddc79c882eb 100644 --- a/spyder/plugins/variableexplorer/plugin.py +++ b/spyder/plugins/variableexplorer/plugin.py @@ -10,7 +10,8 @@ # Local imports from spyder.api.plugins import Plugins, SpyderDockablePlugin -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.plugins.variableexplorer.confpage import ( VariableExplorerConfigPage) @@ -37,7 +38,8 @@ class VariableExplorer(SpyderDockablePlugin): # ---- SpyderDockablePlugin API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _('Variable explorer') def get_description(self): @@ -67,8 +69,13 @@ def on_ipyconsole_available(self): ipyconsole.sig_shellwidget_deleted.connect( self.remove_shellwidget) - def unregister(self): - # Plugins + @on_plugin_teardown(plugin=Plugins.Preferences) + def on_preferences_teardown(self): + preferences = self.get_plugin(Plugins.Preferences) + preferences.deregister_plugin_preferences(self) + + @on_plugin_teardown(plugin=Plugins.IPythonConsole) + def on_ipyconsole_teardown(self): ipyconsole = self.get_plugin(Plugins.IPythonConsole) # Signals diff --git a/spyder/plugins/workingdirectory/plugin.py b/spyder/plugins/workingdirectory/plugin.py index b46e7d36e82..0bd75855856 100644 --- a/spyder/plugins/workingdirectory/plugin.py +++ b/spyder/plugins/workingdirectory/plugin.py @@ -16,12 +16,14 @@ # Local imports from spyder.api.plugins import SpyderPluginV2, Plugins -from spyder.api.plugin_registration.decorators import on_plugin_available +from spyder.api.plugin_registration.decorators import ( + on_plugin_available, on_plugin_teardown) from spyder.api.translations import get_translation from spyder.config.base import get_conf_path from spyder.plugins.workingdirectory.confpage import WorkingDirectoryConfigPage from spyder.plugins.workingdirectory.container import ( WorkingDirectoryContainer) +from spyder.plugins.toolbar.api import ApplicationToolbars from spyder.utils import encoding # Localization @@ -57,7 +59,8 @@ class WorkingDirectory(SpyderPluginV2): # --- SpyderPluginV2 API # ------------------------------------------------------------------------ - def get_name(self): + @staticmethod + def get_name(): return _('Current working directory') def get_description(self): @@ -90,17 +93,13 @@ def on_preferences_available(self): @on_plugin_available(plugin=Plugins.Editor) def on_editor_available(self): editor = self.get_plugin(Plugins.Editor) - editor.sig_dir_opened.connect( - lambda path, plugin=editor: self.chdir(path, editor)) + editor.sig_dir_opened.connect(self._editor_change_dir) @on_plugin_available(plugin=Plugins.Explorer) def on_explorer_available(self): explorer = self.get_plugin(Plugins.Explorer) - - self.sig_current_directory_changed.connect( - lambda path: explorer.chdir(path, emit=False)) - explorer.sig_dir_opened.connect( - lambda path, plugin=explorer: self.chdir(path, plugin)) + self.sig_current_directory_changed.connect(self._explorer_change_dir) + explorer.sig_dir_opened.connect(self._explorer_dir_opened) @on_plugin_available(plugin=Plugins.IPythonConsole) def on_ipyconsole_available(self): @@ -108,28 +107,51 @@ def on_ipyconsole_available(self): self.sig_current_directory_changed.connect( ipyconsole.set_current_client_working_directory) - # TODO: chdir_current_client might follow a better naming - # convention ipyconsole.sig_current_directory_changed.connect( - lambda path, plugin=ipyconsole: self.chdir(path, plugin)) + self._ipyconsole_change_dir) @on_plugin_available(plugin=Plugins.Projects) def on_projects_available(self): projects = self.get_plugin(Plugins.Projects) - projects.sig_project_loaded.connect( - lambda path: - self.chdir( - directory=path, - sender_plugin=projects - ) - ) + projects.sig_project_loaded.connect(self._project_loaded) + projects.sig_project_closed[object].connect(self._project_closed) - projects.sig_project_closed[object].connect( - lambda path: self.chdir( - directory=projects.get_last_working_dir(), - sender_plugin=projects - ) - ) + @on_plugin_teardown(plugin=Plugins.Toolbar) + def on_toolbar_teardown(self): + toolbar = self.get_plugin(Plugins.Toolbar) + toolbar.remove_application_toolbar( + ApplicationToolbars.WorkingDirectory) + + @on_plugin_teardown(plugin=Plugins.Preferences) + def on_preferences_teardown(self): + preferences = self.get_plugin(Plugins.Preferences) + preferences.deregister_plugin_preferences(self) + + @on_plugin_teardown(plugin=Plugins.Editor) + def on_editor_teardown(self): + editor = self.get_plugin(Plugins.Editor) + editor.sig_dir_opened.disconnect(self._editor_change_dir) + + @on_plugin_teardown(plugin=Plugins.Explorer) + def on_explorer_teardown(self): + explorer = self.get_plugin(Plugins.Explorer) + self.sig_current_directory_changed.disconnect(self._explorer_change_dir) + explorer.sig_dir_opened.disconnect(self._explorer_dir_opened) + + @on_plugin_teardown(plugin=Plugins.IPythonConsole) + def on_ipyconsole_teardown(self): + ipyconsole = self.get_plugin(Plugins.IPythonConsole) + + self.sig_current_directory_changed.disconnect( + ipyconsole.set_current_client_working_directory) + ipyconsole.sig_current_directory_changed.disconnect( + self._ipyconsole_change_dir) + + @on_plugin_teardown(plugin=Plugins.Projects) + def on_projects_teardown(self): + projects = self.get_plugin(Plugins.Projects) + projects.sig_project_loaded.disconnect(self._project_loaded) + projects.sig_project_closed[object].disconnect(self._project_closed) # --- Public API # ------------------------------------------------------------------------ @@ -205,3 +227,31 @@ def get_workdir(self): Current working directory. """ return self.get_container().get_workdir() + + # -------------------------- Private API ---------------------------------- + def _editor_change_dir(self, path): + editor = self.get_plugin(Plugins.Editor) + self.chdir(path, editor) + + def _explorer_change_dir(self, path): + explorer = self.get_plugin(Plugins.Explorer) + explorer.chdir(path, emit=False) + + def _explorer_dir_opened(self, path): + explorer = self.get_plugin(Plugins.Explorer) + self.chdir(path, explorer) + + def _ipyconsole_change_dir(self, path): + ipyconsole = self.get_plugin(Plugins.IPythonConsole) + self.chdir(path, ipyconsole) + + def _project_loaded(self, path): + projects = self.get_plugin(Plugins.Projects) + self.chdir(directory=path, sender_plugin=projects) + + def _project_closed(self, path): + projects = self.get_plugin(Plugins.Projects) + self.chdir( + directory=projects.get_last_working_dir(), + sender_plugin=projects + ) diff --git a/spyder/utils/icon_manager.py b/spyder/utils/icon_manager.py index 11c404c7014..8bc7a153594 100644 --- a/spyder/utils/icon_manager.py +++ b/spyder/utils/icon_manager.py @@ -334,6 +334,8 @@ def __init__(self): # --- Status bar -------------------------------------------------------- 'code_fork': [('mdi.source-fork',), {'color': self.MAIN_FG_COLOR}], 'statusbar': [('mdi.dock-bottom',), {'color': self.MAIN_FG_COLOR}], + # --- Plugin registry --------------------------------------------------- + 'plugins': [('mdi.puzzle',), {'color': self.MAIN_FG_COLOR}], } def get_std_icon(self, name, size=None):