diff --git a/spyder/api/decorators.py b/spyder/api/decorators.py index 08f798e9452..945a8894ee6 100644 --- a/spyder/api/decorators.py +++ b/spyder/api/decorators.py @@ -8,39 +8,61 @@ Spyder API helper decorators. """ +# Standard library imports +import functools +from typing import Callable, Type, Any, Optional, Union, List +import inspect -def configuration_observer(cls): - """ - Enable a class to recieve configuration update notifications. +# Local imports +from spyder.config.types import ConfigurationKey - This class decorator maps completion listener configuration sections and - options to their corresponding class methods. - """ - cls._configuration_listeners = {} - for method_name in dir(cls): - method = getattr(cls, method_name) - if hasattr(method, '_conf_listen'): - section, option = method._conf_listen - sect_ion_listeners = cls.configuration_listeners.get(section, {}) - option_listeners = section_listeners.get(option, []) - option_listeners.append(method_name) - section_listeners[option] = option_listeners - cls._configuration_listeners[section] = section_listeners - return cls - - -def on_conf_change(section=None, option=None): + +ConfigurationKeyList = List[ConfigurationKey] +ConfigurationKeyOrList = Union[ConfigurationKeyList, ConfigurationKey] + + +def on_conf_change(func: Callable = None, + section: Optional[str] = None, + option: Optional[ConfigurationKeyOrList] = None) -> Callable: """ Method decorator used to handle changes on the configuration option `option` of the section `section`. The methods that use this decorator must have the following signature - `def method(value): ...` + `def method(self, value)` when observing a single value or the whole + section and `def method(self, option, value): ...` when observing + multiple values. + + Parameters + ---------- + func: Callable + Method to decorate. Given by default when applying the decorator. + section: Optional[str] + Name of the configuration whose option is going to be observed for + changes. If None, then the `CONF_SECTION` attribute of the class + where the method is defined is used. + option: Optional[ConfigurationKeyOrList] + Name/tuple of the option to observe or a list of name/tuples if the + method expects updates from multiple keys. If None, then all changes + on the specified section are observed. + + Returns + ------- + func: Callable + The same method that was given as input. """ + if func is None: + return functools.partial( + on_conf_change, section=section, option=option) + if option is None: option = '__section' - def wrapper(func): - func._conf_listen = (section, option) - return func - return wrapper + info = [] + if isinstance(option, list): + info = [(section, opt) for opt in option] + else: + info = [(section, option)] + + func._conf_listen = info + return func diff --git a/spyder/api/mixins.py b/spyder/api/mixins.py index dc019a711be..5afd740fec0 100644 --- a/spyder/api/mixins.py +++ b/spyder/api/mixins.py @@ -9,23 +9,125 @@ """ # Standard library imports -from typing import Any +import logging +from typing import Any, Union, Optional # Local imports -from spyder.api.decorators import configuration_observer from spyder.config.manager import CONF from spyder.config.types import ConfigurationKey +from spyder.config.user import NoDefault -@configuration_observer -class SpyderConfigurationObserver: +logger = logging.getLogger(__name__) + +BasicTypes = Union[bool, int, str, tuple, list, dict] + + +class SpyderConfigurationAccessor: + """ + Mixin used to access options stored in the Spyder configuration. + """ + + # Name of the configuration section that's going to be + # used to record the object's permanent data in Spyder + # config system (i.e. in spyder.ini) + CONF_SECTION = None + + def get_conf(self, + option: ConfigurationKey, + default: Union[NoDefault, BasicTypes] = NoDefault, + section: Optional[str] = None): + """ + Get an option from Spyder configuration system. + + Parameters + ---------- + option: ConfigurationKey + Name/Tuple path of the option to get its value from. + default: Union[NoDefault, BasicTypes] + Fallback value to return if the option is not found on the + configuration system. + section: str + Section in the configuration system, e.g. `shortcuts`. If None, + then the value of `CONF_SECTION` is used. + + Returns + ------- + value: BasicTypes + Value of the option in the configuration section. + + Raises + ------ + spyder.py3compat.configparser.NoOptionError + If the option does not exist in the configuration under the given + section and the default value is NoDefault. + """ + section = self.CONF_SECTION if section is None else section + if section is None: + raise AttributeError( + 'A SpyderConfigurationAccessor must define a `CONF_SECTION` ' + 'class attribute!' + ) + + return CONF.get(section, option, default) + + def set_conf(self, + option: ConfigurationKey, + value: BasicTypes, + section: Optional[str] = None): + """ + Set an option in the Spyder configuration system. + + Parameters + ---------- + option: ConfigurationKey + Name/Tuple path of the option to set its value. + value: BasicTypes + Value to set on the configuration system. + section: Optional[str] + Section in the configuration system, e.g. `shortcuts`. If None, + then the value of `CONF_SECTION` is used. + """ + section = self.CONF_SECTION if section is None else section + if section is None: + raise AttributeError( + 'A SpyderConfigurationAccessor must define a `CONF_SECTION` ' + 'class attribute!' + ) + CONF.set(section, option, value) + + + def remove_conf(self, + option: ConfigurationKey, + section: Optional[str] = None): + """ + Remove an option in the Spyder configuration system. + + Parameters + ---------- + option: ConfigurationKey + Name/Tuple path of the option to set its value. + section: Optional[str] + Section in the configuration system, e.g. `shortcuts`. If None, + then the value of `CONF_SECTION` is used. + """ + section = self.CONF_SECTION if section is None else section + if section is None: + raise AttributeError( + 'A SpyderConfigurationAccessor must define a `CONF_SECTION` ' + 'class attribute!' + ) + CONF.remove_option(section, option) + + +class SpyderConfigurationObserver(SpyderConfigurationAccessor): """ Concrete implementation of the protocol :class:`spyder.config.types.ConfigurationObserver`. - This mixin enables a class to recieve configuration updates seamlessly, + This mixin enables a class to receive configuration updates seamlessly, by registering methods using the - :function:`spyder.api.decorators.on_conf_change` decorator, which recieves + :function:`spyder.api.decorators.on_conf_change` decorator, which receives a configuration section and option to observe. When a change occurs on any of the registered configuration options, @@ -33,16 +135,63 @@ class SpyderConfigurationObserver: """ def __init__(self): + if self.CONF_SECTION is None: + raise AttributeError( + 'A SpyderConfigurationObserver must define a `CONF_SECTION` ' + 'class attribute!' + ) + + self._configuration_listeners = {} + self._multi_option_listeners = set({}) + self._gather_observers() + self._merge_none_observers() + # Register class to listen for changes in all the registered options for section in self._configuration_listeners: + section = self.CONF_SECTION if section is None else section observed_options = self._configuration_listeners[section] for option in observed_options: + logger.debug(f'{self} is observing {option} ' + f'in section {section}') CONF.observe_configuration(self, section, option) def __del__(self): # Remove object from the configuration observer CONF.unobserve_configuration(self) + def _gather_observers(self): + """Gather all the methods decorated with `on_conf_change`.""" + for method_name in dir(self): + method = getattr(self, method_name, None) + if hasattr(method, '_conf_listen'): + info = method._conf_listen + if len(info) > 1: + self._multi_option_listeners |= {method_name} + + for section, option in info: + section_listeners = self._configuration_listeners.get( + section, {}) + option_listeners = section_listeners.get(option, []) + option_listeners.append(method_name) + section_listeners[option] = option_listeners + self._configuration_listeners[section] = section_listeners + + def _merge_none_observers(self): + """Replace observers that declared section as None by CONF_SECTION.""" + default_selectors = self._configuration_listeners.get(None, {}) + section_selectors = self._configuration_listeners.get( + self.CONF_SECTION, {}) + + for option in default_selectors: + default_option_receivers = default_selectors.get(option, []) + section_option_receivers = section_selectors.get(option, []) + merged_receivers = ( + default_option_receivers + section_option_receivers) + section_selectors[option] = merged_receivers + + self._configuration_listeners[self.CONF_SECTION] = section_selectors + self._configuration_listeners.pop(None, None) + def on_configuration_change(self, option: ConfigurationKey, section: str, value: Any): """ @@ -58,8 +207,11 @@ def on_configuration_change(self, option: ConfigurationKey, section: str, value: Any New value of the configuration option that produced the event. """ - section_recievers = self._configuration_listeners.get(section, {}) - option_recievers = section_recievers.get(option, []) - for receiver in option_recievers: + section_receivers = self._configuration_listeners.get(section, {}) + option_receivers = section_receivers.get(option, []) + for receiver in option_receivers: method = getattr(self, receiver) - method(value) + if receiver in self._multi_option_listeners: + method(option, value) + else: + method(value) diff --git a/spyder/api/plugins.py b/spyder/api/plugins.py index e903f899a15..4118f97c7d1 100644 --- a/spyder/api/plugins.py +++ b/spyder/api/plugins.py @@ -37,8 +37,9 @@ from spyder.api.exceptions import SpyderAPIError from spyder.api.translations import get_translation from spyder.api.widgets import PluginMainContainer, PluginMainWidget -from spyder.api.widgets.mixins import SpyderActionMixin, SpyderOptionMixin +from spyder.api.widgets.mixins import SpyderActionMixin from spyder.api.widgets.mixins import SpyderWidgetMixin +from spyder.api.mixins import SpyderConfigurationObserver from spyder.config.gui import get_color_scheme, get_font from spyder.config.manager import CONF # TODO: Remove after migration from spyder.config.user import NoDefault @@ -616,7 +617,7 @@ class Plugins: # --- Base API plugins # ---------------------------------------------------------------------------- -class SpyderPluginV2(QObject, SpyderActionMixin, SpyderOptionMixin): +class SpyderPluginV2(QObject, SpyderActionMixin, SpyderConfigurationObserver): """ A Spyder plugin to extend functionality without a dockable widget. @@ -691,14 +692,6 @@ class SpyderPluginV2(QObject, SpyderActionMixin, SpyderOptionMixin): # Widget to be used as entry in Spyder Preferences dialog. CONF_WIDGET_CLASS = None - # Some widgets may use configuration options from other plugins. - # This variable helps translate CONF to options when the option comes - # from another plugin. - # Example: - # CONF_FROM_OPTIONS = {'widget_option': ('section', 'option'), ...} - # See: spyder/plugins/console/plugin.py - CONF_FROM_OPTIONS = None - # Some plugins may add configuration options for other plugins. # Example: # ADDITIONAL_CONF_OPTIONS = {'section': } @@ -796,19 +789,6 @@ class SpyderPluginV2(QObject, SpyderActionMixin, SpyderOptionMixin): This signal is automatically connected to the main container/widget. """ - sig_option_changed = Signal(str, object) - """ - This signal is emitted when an option has been set on the main container - or the main widget. - - Parameters - ---------- - option: str - Option name. - value: object - New value of the changed option. - """ - # --- Private attributes ------------------------------------------------- # ------------------------------------------------------------------------ # Define configuration name map for plugin to split configuration @@ -830,23 +810,16 @@ def __init__(self, parent, configuration=None): self.main = parent if self.CONTAINER_CLASS is not None: - options = self.options_from_conf( - self.CONTAINER_CLASS.DEFAULT_OPTIONS) self._container = container = self.CONTAINER_CLASS( name=self.NAME, plugin=self, - parent=parent, - options=options, + parent=parent ) if isinstance(container, SpyderWidgetMixin): - container.setup(options=options) + container.setup() container.update_actions() - # Set options without emitting a signal - container.change_options(options=options) - container.sig_option_changed.connect(self.sig_option_changed) - if isinstance(container, PluginMainContainer): # Default signals to connect in main container or main widget. container.sig_exception_occurred.connect( @@ -862,8 +835,8 @@ def __init__(self, parent, configuration=None): self.after_container_creation() - # ---- Widget setup - container._setup(options=options) + if hasattr(container, '_setup'): + container._setup() # --- Private methods ---------------------------------------------------- # ------------------------------------------------------------------------ @@ -889,7 +862,6 @@ def _register(self): # Signals # -------------------------------------------------------------------- - self.sig_option_changed.connect(self.set_conf_option) self.is_registered = True self.update_font() @@ -899,10 +871,6 @@ def _unregister(self): Disconnect signals and clean up the plugin to be able to stop it while Spyder is running. """ - try: - self.sig_option_changed.disconnect() - except TypeError: - pass if self._conf is not None: self._conf.unregister_plugin() @@ -967,52 +935,6 @@ def get_plugin(self, plugin_name): 'OPTIONAL requirements!'.format(plugin_name) ) - def options_from_conf(self, options): - """ - Get `options` values from the configuration system. - - Returns - ------- - Dictionary of {str: object} - """ - conf_from_options = self.CONF_FROM_OPTIONS or {} - config_options = {} - if self._conf is not None: - # options could be a list, or a dictionary - for option in options: - if option in conf_from_options: - section, new_option = conf_from_options[option] - else: - section, new_option = (self.CONF_SECTION, option) - - try: - config_options[option] = self.get_conf_option( - new_option, - section=section, - ) - except (cp.NoSectionError, cp.NoOptionError): - # TODO: Remove when migration is done, move to logger. - # Needed to check how the options API needs to cover - # options from all plugins - print('\nspyder.api.plugins.options_from_conf\n' - 'Warning: option "{}" not found in section "{}" ' - 'of configuration!'.format(option, self.NAME)) - - # Currently when the preferences dialog is used, a set of - # changed options is passed. - - # This method can get the values from the DEFAULT_OPTIONS - # of the PluginMainWidget or the PluginMainContainer - # subclass if `options`is a dictionary instead of a set - # of options. - if isinstance(options, (dict, OrderedDict)): - try: - config_options[option] = options[option] - except Exception: - pass - - return config_options - def get_conf_option(self, option, default=NoDefault, section=None): """ Get an option from Spyder configuration system. @@ -1099,19 +1021,6 @@ def apply_conf(self, options_set, notify=True): container = self.get_container() if notify: self.after_configuration_update(list(options_set)) - # The container might not implement the SpyderWidgetMixin API - # for example a completion client that only implements the - # completion client interface without any options. - if isinstance(container, SpyderWidgetMixin): - options = self.options_from_conf(options_set) - new_options = self.options_from_keys( - options, - container.DEFAULT_OPTIONS, - ) - # By using change_options we will not emit sig_option_changed - # when setting the options - # This will also cascade on all children - container.change_options(new_options) @Slot(str) @Slot(str, int) diff --git a/spyder/api/widgets/__init__.py b/spyder/api/widgets/__init__.py index b3b302cdb2e..bab2646ee48 100644 --- a/spyder/api/widgets/__init__.py +++ b/spyder/api/widgets/__init__.py @@ -78,7 +78,6 @@ class PluginMainContainer(QWidget, SpyderWidgetMixin, SpyderToolbarMixin): All Spyder non dockable plugins can define a plugin container that must subclass this. """ - DEFAULT_OPTIONS = {} # --- Signals # ------------------------------------------------------------------------ @@ -150,9 +149,8 @@ class PluginMainContainer(QWidget, SpyderWidgetMixin, SpyderToolbarMixin): error dialog. """ - def __init__(self, name, plugin, parent=None, options=DEFAULT_OPTIONS): - super().__init__(parent=parent) - self._options = options + def __init__(self, name, plugin, parent=None): + super().__init__(parent=parent, class_parent=plugin) # Attributes # -------------------------------------------------------------------- @@ -172,7 +170,7 @@ def __init__(self, name, plugin, parent=None, options=DEFAULT_OPTIONS): # --- API: methods to define or override # ------------------------------------------------------------------------ - def setup(self, options=DEFAULT_OPTIONS): + def setup(self): """ Create actions, widgets, add to menu and other setup requirements. """ @@ -189,22 +187,6 @@ def update_actions(self): 'A PluginMainContainer subclass must define a `update_actions` ' 'method!') - def on_option_update(self, option, value): - """ - This method is called when an option is set with the `set_option` - or `set_options` method from the OptionMixin. - """ - raise NotImplementedError( - 'A PluginMainContainer subclass must define a `on_option_update` ' - 'method!') - - # ---- Private methods - # ------------------------------------------------------------------------ - def _setup(self, options=DEFAULT_OPTIONS): - """Apply options when instantiated by the plugin.""" - for option, value in options.items(): - self.on_option_update(option, value) - class PluginMainWidget(QWidget, SpyderWidgetMixin, SpyderToolbarMixin): """ @@ -225,7 +207,6 @@ class PluginMainWidget(QWidget, SpyderWidgetMixin, SpyderToolbarMixin): horizontal space available for the plugin. This mean that toolbars must be stacked vertically and cannot be placed horizontally next to each other. """ - DEFAULT_OPTIONS = {} # --- Attributes # ------------------------------------------------------------------------ @@ -338,12 +319,11 @@ class PluginMainWidget(QWidget, SpyderWidgetMixin, SpyderToolbarMixin): needs its ancestor to be updated. """ - def __init__(self, name, plugin, parent=None, options=DEFAULT_OPTIONS): - super().__init__(parent=parent) + def __init__(self, name, plugin, parent=None): + super().__init__(parent=parent, class_parent=plugin) # Attributes # -------------------------------------------------------------------- - self._options = options self._is_tab = False self._name = name self._plugin = plugin @@ -434,7 +414,7 @@ def _find_children(obj, all_children): return all_children - def _setup(self, options=DEFAULT_OPTIONS): + def _setup(self): """ Setup default actions, create options menu, and connect signals. """ @@ -511,6 +491,7 @@ def _setup(self, options=DEFAULT_OPTIONS): context=Qt.WidgetWithChildrenShortcut, shortcut_context='_', ) + bottom_section = OptionsMenuSections.Bottom for item in [self.undock_action, self.close_action, self.dock_action]: self.add_item_to_menu( @@ -624,7 +605,7 @@ def get_actions(self, filter_actions=True): self.toggle_view_action, ] - for child in all_children: + for child in set(all_children): get_actions_method = getattr(child, 'get_actions', None) _actions = getattr(child, '_actions', None) @@ -1079,7 +1060,7 @@ def set_ancestor(self, ancestor): """ pass - def setup(self, options): + def setup(self): """ Create widget actions, add to menu and other setup requirements. """ @@ -1097,15 +1078,6 @@ def update_actions(self): 'A PluginMainWidget subclass must define an `update_actions` ' 'method!') - def on_option_update(self, option, value): - """ - This method is called when the an option is set with the `set_option` - or `set_options` method from the OptionMixin. - """ - raise NotImplementedError( - 'A PluginMainWidget subclass must define an `on_option_update` ' - 'method!') - def run_test(): # Third party imports @@ -1117,9 +1089,8 @@ def run_test(): app = qapplication() main = QMainWindow() widget = PluginMainWidget('test', main) - options = PluginMainWidget.DEFAULT_OPTIONS widget.get_title = lambda x=None: 'Test title' - widget._setup(options) + widget._setup() layout = QHBoxLayout() layout.addWidget(QTableWidget()) widget.setLayout(layout) diff --git a/spyder/api/widgets/mixins.py b/spyder/api/widgets/mixins.py index 310738267e1..77a9f2d97d4 100644 --- a/spyder/api/widgets/mixins.py +++ b/spyder/api/widgets/mixins.py @@ -23,7 +23,8 @@ # Local imports from spyder.api.exceptions import SpyderAPIError -from spyder.api.mixins import SpyderConfigurationObserver +from spyder.api.mixins import ( + SpyderConfigurationObserver, SpyderConfigurationAccessor) from spyder.api.widgets.menus import SpyderMenu from spyder.config.types import ConfigurationKey from spyder.config.manager import CONF @@ -31,189 +32,6 @@ from spyder.utils.qthelpers import create_action, create_toolbutton -class SpyderOptionMixin(SpyderConfigurationObserver): - """ - This mixin provides option handling tools for any widget that needs to - track options, their state and methods to call on option change. - - These options will usually be tied to configuration options in the Spyder - configuration files (e.g. spyder.ini). Some options might not be connected - to configuration options, and in this case using DEFAULT_OPTIONS is - preferred instead of using class attributes. - """ - # EXAMPLE: {'option_1': value_1, 'option_2': value_2, ...} - DEFAULT_OPTIONS = {} - - @staticmethod - def _find_option_mixin_children(obj, all_children): - """ - Find all children of `obj` that use SpyderOptionMixin recursively. - - `all_children` is a list on which to append the results. - """ - children = obj.findChildren(SpyderOptionMixin) - all_children.extend(children) - - if obj not in all_children: - all_children.append(obj) - - for child in children: - children = child.findChildren(SpyderOptionMixin) - all_children.extend(children) - - return all_children - - def _check_options_dictionary_exist(self): - """ - Helper method to check the options dictionary has been initialized. - """ - options = getattr(self, '_options', None) - if not options: - self._options = self.DEFAULT_OPTIONS - - def _check_options_exist(self, options): - """ - Helper method to check that an option was defined in the - DEFAULT_OPTIONS dictionary. - """ - for option in options: - if option not in self.DEFAULT_OPTIONS: - raise SpyderAPIError( - 'Option "{0}" has not been defined in the the widget ' - 'DEFAULT_OPTIONS attribute! Options are: ' - '{1}'.format(option, self.DEFAULT_OPTIONS) - ) - - def _set_option(self, option, value, emit): - """ - Helper method to set/change options with option to emit signal. - """ - # Check if a togglable action exists with this name and update state - try: - action_name = 'toggle_{}_action'.format(option) - self._update_action_state(action_name, value) - except SpyderAPIError: - pass - - self._check_options_dictionary_exist() - if option in self.DEFAULT_OPTIONS: - self._options[option] = value - - if emit: - # This eventually calls on_option_update too. - self.sig_option_changed.emit(option, value) - else: - self.on_option_update(option, value) - else: - raise SpyderAPIError( - 'Option "{}" has not been defined in the widget ' - 'DEFAULT_OPTIONS attribute!' - ''.format(option) - ) - - def get_option(self, option): - """ - Return option for this widget. - - `option` must be defined in the DEFAULT_OPTIONS class attribute. - """ - self._check_options_dictionary_exist() - if option in self.DEFAULT_OPTIONS: - return self._options[option] - - raise SpyderAPIError( - 'Option "{0}" has not been defined in the widget DEFAULT_OPTIONS ' - 'attribute {1}'.format(option, self.DEFAULT_OPTIONS) - ) - - def get_options(self): - """ - Return the current options dictionary. - """ - self._check_options_dictionary_exist() - return self._options - - def set_option(self, option, value): - """ - Set option for this widget. - - `option` must be defined in the DEFAULT_OPTIONS class attribute. - Setting an option will emit the sig_option_changed signal and will - call the `on_option_update` method. - """ - signal = getattr(self, 'sig_option_changed', None) - if signal is None: - raise SpyderAPIError( - 'A Spyder widget must define a ' - '"sig_option_changed = Signal(str, object)" signal!' - ) - self._set_option(option, value, emit=True) - - def set_options(self, options): - """ - Set options for this widget. - - `options` must be defined in the DEFAULT_OPTIONS class attribute. - Setting each option will emit the sig_option_changed signal and will - call the `on_option_update` method. - - This method will propagate the options on all children. - """ - parent_and_children = self._find_option_mixin_children(self, [self]) - for child in parent_and_children: - child_options = self.options_from_keys(options, - child.DEFAULT_OPTIONS) - child._check_options_exist(child_options) - - for option, value in child_options.items(): - child.set_option(option, value) - - def change_option(self, option, value): - """ - Change option for this widget. - - `option` must be defined in the DEFAULT_OPTIONS class attribute. - Changing an option will call the `on_option_update` method. - """ - self._set_option(option, value, emit=False) - - def change_options(self, options): - """ - Change options for this widget. - - `options` must be defined in the DEFAULT_OPTIONS class attribute. - Changing each option will call the `on_option_update` method. - - This method will propagate the options on all children. - """ - parent_and_children = self._find_option_mixin_children(self, [self]) - for child in parent_and_children: - child_options = self.options_from_keys(options, - child.DEFAULT_OPTIONS) - child._check_options_exist(child_options) - - for option, value in child_options.items(): - child.change_option(option, value) - - def options_from_keys(self, options, keys): - """ - Create an options dictionary that only contains given `keys`. - """ - new_options = {} - for option in keys: - if option in options: - new_options[option] = options[option] - - return new_options - - def on_option_update(self, option, value): - """ - When an option is set or changed, this method is called. - """ - raise NotImplementedError( - 'Widget must define a `on_option_update` method!') - - class SpyderToolButtonMixin: """ Provide methods to create, add and get toolbuttons. @@ -461,8 +279,11 @@ def create_action(self, name, text, icon=None, icon_text='', tip=None, the tooltip on this toolbutton if part of a toolbar. tip: str Tooltip to define for action on menu or toolbar. - toggled: callable - The callable to use when toggling this action + toggled: Optional[Union[Callable, bool]] + If True, then the action modifies the configuration option on the + section specified. Otherwise, it should be a callable to use + when toggling this action. If None, then the action does not + behave like a checkbox. triggered: callable The callable to use when triggering this action. shortcut_context: str @@ -472,9 +293,10 @@ def create_action(self, name, text, icon=None, icon_text='', tip=None, initial: object Sets the initial state of a togglable action. This does not emit the toggled signal. - section: str + section: Optional[str] Name of the configuration section whose option is going to be - modified. + modified. If None, and option is not None, then it defaults to the + class `CONF_SECTION` attribute. option: ConfigurationKey Name of the configuration option whose value is reflected and affected by the action @@ -514,9 +336,12 @@ def create_action(self, name, text, icon=None, icon_text='', tip=None, if parent is None: parent = self + if toggled and not callable(toggled): + toggled = lambda value: None + if toggled is not None: - if section is not None and option is not None: - toggled = self.wrap_toggled(toggled, section, option) + if section is None and option is not None: + section = self.CONF_SECTION action = create_action( parent, @@ -526,6 +351,8 @@ def create_action(self, name, text, icon=None, icon_text='', tip=None, toggled=toggled, triggered=triggered, context=context, + section=section, + option=option ) action.name = name if icon_text: @@ -536,16 +363,9 @@ def create_action(self, name, text, icon=None, icon_text='', tip=None, action.register_shortcut = register_shortcut action.tip = tip - if toggled: - if section is not None and option is not None: - CONF.observe_configuration(action, section, option) - self.add_configuration_update(action) - if initial is not None: if toggled: - self.blockSignals(True) action.setChecked(initial) - self.blockSignals(False) elif triggered: raise SpyderAPIError( 'Initial values can only apply to togglable actions!') @@ -553,29 +373,11 @@ def create_action(self, name, text, icon=None, icon_text='', tip=None, if toggled: if section is not None and option is not None: value = CONF.get(section, option) - self.blockSignals(True) action.setChecked(value) - self.blockSignals(False) self._actions[name] = action return action - def wrap_toggled(self, toggled, section, option): - @functools.wraps(toggled) - def wrapper(*args, **kwargs): - value = self.isChecked() - CONF.set(section, option, value, recursive_notification=True) - toggled(*args, **kwargs) - return wrapper - - def add_configuration_update(self, action): - def on_configuration_change(self, _option, _section, value): - self.blockSignals(True) - self.setChecked(value) - self.blockSignals(False) - method = types.MethodType(on_configuration_change, action) - setattr(action, 'on_configuration_change', method) - def get_action(self, name): """ Return an action by name. @@ -628,7 +430,7 @@ def update_actions(self, options): class SpyderWidgetMixin(SpyderActionMixin, SpyderMenuMixin, - SpyderOptionMixin, SpyderToolButtonMixin): + SpyderConfigurationObserver, SpyderToolButtonMixin): """ Basic functionality for all Spyder widgets and Qt items. @@ -639,6 +441,12 @@ class SpyderWidgetMixin(SpyderActionMixin, SpyderMenuMixin, This provides a simple management of widget options, as well as Qt helpers for defining the actions a widget provides. """ + def __init__(self, class_parent=None): + if getattr(self, 'CONF_SECTION', None) is None: + if hasattr(class_parent, 'CONF_SECTION'): + # Inherit class_parent CONF_SECTION value + self.CONF_SECTION = class_parent.CONF_SECTION + super().__init__() @staticmethod def create_icon(name, image_file=False): diff --git a/spyder/api/widgets/status.py b/spyder/api/widgets/status.py index 6352ce63732..6edbdb939ee 100644 --- a/spyder/api/widgets/status.py +++ b/spyder/api/widgets/status.py @@ -39,7 +39,7 @@ class StatusBarWidget(QWidget, SpyderWidgetMixin): def __init__(self, parent=None, spinner=False): """Status bar widget base.""" - super().__init__(parent) + super().__init__(parent, class_parent=parent) # Variables self.value = None diff --git a/spyder/config/main.py b/spyder/config/main.py index 52e0bf54c05..b32eb57e139 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -261,11 +261,16 @@ 'connect/ipython_console': False, 'math': True, 'automatic_import': True, + 'plain_mode': False, + 'rich_mode': True, + 'show_source': False, + 'locked': False, }), ('onlinehelp', { 'enable': True, 'zoom_factor': .8, + 'handle_links': False, 'max_history_entries': 20, }), ('outline_explorer', @@ -285,11 +290,21 @@ 'show_all': True, 'show_hscrollbar': True, 'max_recent_projects': 10, - 'visible_if_project_open': True + 'visible_if_project_open': True, + 'single_click_to_open': False, + 'date_column': False, + 'kind_column': True, + 'size_column': False, + 'show_hidden': False, + 'type_column': False, + 'file_associations': {}, }), ('explorer', { 'enable': True, + 'date_column': True, + 'type_column': False, + 'size_column': False, 'name_filters': NAME_FILTERS, 'show_hidden': False, 'single_click_to_open': False, @@ -306,6 +321,7 @@ 'search_text_samples': [TASKS_PATTERN], 'more_options': False, 'case_sensitive': False, + 'exclude_case_sensitive': False, 'max_results': 1000, }), ('breakpoints', @@ -335,6 +351,8 @@ }), ('workingdir', { + 'history': [], + 'workdir': None, 'working_dir_adjusttocontents': False, 'working_dir_history': 20, 'console/use_project_or_home_directory': False, @@ -560,6 +578,8 @@ ('find_in_files', [ 'path_history' 'search_text', + 'exclude_index', + 'search_in_index', ] ), ('main_interpreter', [ diff --git a/spyder/plugins/application/container.py b/spyder/plugins/application/container.py index 916e2c589e2..619e0d74a36 100644 --- a/spyder/plugins/application/container.py +++ b/spyder/plugins/application/container.py @@ -85,7 +85,7 @@ class ApplicationContainer(PluginMainContainer): 'show_internal_errors': True, } - def setup(self, options=DEFAULT_OPTIONS): + def setup(self): # Attributes self.dialog_manager = DialogManager() self.give_updates_feedback = False @@ -202,7 +202,7 @@ def _check_updates_ready(self): # Adjust the checkbox depending on the stored configuration option = 'check_updates_on_startup' - check_updates = self.get_option(option) + check_updates = self.get_conf(option) box.set_checked(check_updates) if error_msg is not None: @@ -248,7 +248,7 @@ def _check_updates_ready(self): check_updates = box.is_checked() # Update checkbox based on user interaction - self.set_option(option, check_updates) + self.set_conf(option, check_updates) # Enable check_updates_action after the thread has finished self.check_updates_action.setDisabled(False) diff --git a/spyder/plugins/breakpoints/widgets/main_widget.py b/spyder/plugins/breakpoints/widgets/main_widget.py index 936c0fbdf5b..27dabc370f8 100644 --- a/spyder/plugins/breakpoints/widgets/main_widget.py +++ b/spyder/plugins/breakpoints/widgets/main_widget.py @@ -184,7 +184,7 @@ class BreakpointTableView(QTableView, SpyderWidgetMixin): sig_conditional_breakpoint_requested = Signal() def __init__(self, parent, data): - super().__init__(parent) + super().__init__(parent, class_parent=parent) # Widgets self.model = BreakpointTableModel(self, data) @@ -202,7 +202,7 @@ def __init__(self, parent, data): # --- SpyderWidgetMixin API # ------------------------------------------------------------------------ - def setup(self, options={}): + def setup(self): clear_all_action = self.create_action( BreakpointTableViewActions.ClearAllBreakpoints, _("Clear breakpoints in all files"), @@ -351,9 +351,8 @@ class BreakpointWidget(PluginMainWidget): Send a request to set/edit a condition on a single selected breakpoint. """ - def __init__(self, name=None, plugin=None, parent=None, - options=DEFAULT_OPTIONS): - super().__init__(name, plugin, parent=parent, options=options) + def __init__(self, name=None, plugin=None, parent=None): + super().__init__(name, plugin, parent=parent) # Widgets self.breakpoints_table = BreakpointTableView(self, {}) @@ -381,12 +380,9 @@ def get_title(self): def get_focus_widget(self): return self.breakpoints_table - def setup(self, options): + def setup(self): self.breakpoints_table.setup() - def on_option_update(self, option, value): - pass - def update_actions(self): rows = self.breakpoints_table.selectionModel().selectedRows() c_row = rows[0] if rows else None diff --git a/spyder/plugins/console/plugin.py b/spyder/plugins/console/plugin.py index 7e6b02c313c..7ab702764d5 100644 --- a/spyder/plugins/console/plugin.py +++ b/spyder/plugins/console/plugin.py @@ -242,7 +242,7 @@ def start_interpreter(self, namespace): Stdin and stdout are now redirected through the internal console. """ widget = self.get_widget() - widget.change_option('namespace', namespace) + widget.set_conf('namespace', namespace) widget.start_interpreter(namespace) def set_namespace_item(self, name, value): diff --git a/spyder/plugins/console/widgets/main_widget.py b/spyder/plugins/console/widgets/main_widget.py index f605dada77e..75f8562240c 100644 --- a/spyder/plugins/console/widgets/main_widget.py +++ b/spyder/plugins/console/widgets/main_widget.py @@ -27,6 +27,7 @@ # Local imports from spyder.api.translations import get_translation from spyder.api.widgets import PluginMainWidget +from spyder.api.decorators import on_conf_change from spyder.app.solver import find_internal_plugins from spyder.config.base import DEV, get_debug_level from spyder.plugins.console.widgets.internalshell import InternalShell @@ -121,8 +122,8 @@ class ConsoleWidget(PluginMainWidget): Example `{'name': str, 'ignore_unknown': bool}`. """ - def __init__(self, name, plugin, parent=None, options=DEFAULT_OPTIONS): - super().__init__(name, plugin, parent, options) + def __init__(self, name, plugin, parent=None): + super().__init__(name, plugin, parent) logger.info("Initializing...") @@ -135,12 +136,12 @@ def __init__(self, name, plugin, parent=None, options=DEFAULT_OPTIONS): self.error_dlg = None self.shell = InternalShell( # TODO: Move to use SpyderWidgetMixin? parent=parent, - namespace=self.get_option('namespace'), - commands=self.get_option('commands'), - message=self.get_option('message'), - max_line_count=self.get_option('max_line_count'), - profile=self.get_option('profile'), - multithreaded=self.get_option('multithreaded'), + namespace=self.get_conf('namespace'), + commands=self.get_conf('commands'), + message=self.get_conf('message'), + max_line_count=self.get_conf('max_line_count'), + profile=self.get_conf('profile'), + multithreaded=self.get_conf('multithreaded'), ) self.find_widget = FindReplace(self) @@ -148,7 +149,7 @@ def __init__(self, name, plugin, parent=None, options=DEFAULT_OPTIONS): self.setAcceptDrops(True) self.find_widget.set_editor(self.shell) self.find_widget.hide() - self.shell.toggle_wrap_mode(self.get_option('wrap')) + self.shell.toggle_wrap_mode(self.get_conf('wrap')) # Layout layout = QVBoxLayout() @@ -172,7 +173,7 @@ def __init__(self, name, plugin, parent=None, options=DEFAULT_OPTIONS): def get_title(self): return _('Internal console') - def setup(self, options): + def setup(self): # TODO: Move this to the shell self.quit_action = self.create_action( ConsoleWidgetActions.Quit, @@ -221,14 +222,14 @@ def setup(self, options): wrap_action = self.create_action( ConsoleWidgetActions.ToggleWrap, text=_("Wrap lines"), - toggled=lambda val: self.set_option('wrap', val), - initial=self.get_option('wrap'), + toggled=lambda val: self.set_conf('wrap', val), + initial=self.get_conf('wrap'), ) codecompletion_action = self.create_action( ConsoleWidgetActions.ToggleCodeCompletion, text=_("Automatic code completion"), - toggled=lambda val: self.set_option('codecompletion/auto', val), - initial=self.get_option('codecompletion/auto'), + toggled=lambda val: self.set_conf('codecompletion/auto', val), + initial=self.get_conf('codecompletion/auto'), ) # Submenu @@ -262,15 +263,19 @@ def setup(self, options): ) self.shell.set_external_editor( - self.get_option('external_editor/path'), '') + self.get_conf('external_editor/path'), '') - def on_option_update(self, option, value): - if option == 'max_line_count': - self.shell.setMaximumBlockCount(value) - elif option == 'wrap': - self.shell.toggle_wrap_mode(value) - elif option == 'external_editor/path': - self.shell.set_external_editor(value, '') + @on_conf_change(option='max_line_count') + def max_line_count_update(self, value): + self.shell.setMaximumBlockCount(value) + + @on_conf_change(option='wrap') + def wrap_mode_update(self, value): + self.shell.toggle_wrap_mode(value) + + @on_conf_change(option='external_editor/path') + def external_editor_update(self, value): + self.shell.set_external_editor(value, '') def update_actions(self): pass @@ -338,7 +343,7 @@ def set_help(self, help_plugin): def report_issue(self): """Report an issue with the SpyderErrorDialog.""" self._report_dlg = SpyderErrorDialog(self, is_report=True) - self._report_dlg.set_color_scheme(self.get_option('color_theme')) + self._report_dlg.set_color_scheme(self.get_conf('color_theme')) self._report_dlg.show() @Slot(dict) @@ -415,10 +420,10 @@ def handle_exception(self, error_data, sender=None, internal_plugins=None): 'the "error_data" dictionary!'.format(plugin_name) ) - if self.get_option('show_internal_errors'): + if self.get_conf('show_internal_errors'): if self.error_dlg is None: self.error_dlg = SpyderErrorDialog(self) - self.error_dlg.set_color_scheme(self.get_option('color_theme')) + self.error_dlg.set_color_scheme(self.get_conf('color_theme')) self.error_dlg.close_btn.clicked.connect(self.close_error_dlg) self.error_dlg.rejected.connect(self.remove_error_dlg) self.error_dlg.details.go_to_error.connect(self.go_to_error) @@ -556,13 +561,13 @@ def change_max_line_count(self, value=None): self, _('Buffer'), _('Maximum line count'), - self.get_option('max_line_count'), + self.get_conf('max_line_count'), 0, 1000000, ) if valid: - self.set_option('max_line_count', value) + self.set_conf('max_line_count', value) @Slot() def change_exteditor(self, path=None): @@ -576,11 +581,11 @@ def change_exteditor(self, path=None): _('External editor'), _('External editor executable path:'), QLineEdit.Normal, - self.get_option('external_editor/path'), + self.get_conf('external_editor/path'), ) if valid: - self.set_option('external_editor/path', to_text_string(path)) + self.set_conf('external_editor/path', to_text_string(path)) def set_exit_function(self, func): """ diff --git a/spyder/plugins/explorer/widgets/explorer.py b/spyder/plugins/explorer/widgets/explorer.py index 17972edfdde..5709a69c308 100644 --- a/spyder/plugins/explorer/widgets/explorer.py +++ b/spyder/plugins/explorer/widgets/explorer.py @@ -30,6 +30,7 @@ QMessageBox, QTextEdit, QTreeView, QVBoxLayout) # Local imports +from spyder.api.decorators import on_conf_change from spyder.api.translations import get_translation from spyder.api.widgets import SpyderWidgetMixin from spyder.config.base import get_home_dir, running_under_pytest @@ -263,7 +264,7 @@ class DirView(QTreeView, SpyderWidgetMixin): File path to run. """ - def __init__(self, parent=None, options=DEFAULT_OPTIONS): + def __init__(self, parent=None): """Initialize the DirView. Parameters @@ -273,7 +274,7 @@ def __init__(self, parent=None, options=DEFAULT_OPTIONS): options: dict Dictionary with all the options of the widget. """ - super().__init__(parent=parent) + super().__init__(parent=parent, class_parent=parent) # Attributes self._parent = parent @@ -298,15 +299,14 @@ def __init__(self, parent=None, options=DEFAULT_OPTIONS): self.setup_fs_model() self.setSelectionMode(self.ExtendedSelection) header.setContextMenuPolicy(Qt.CustomContextMenu) - self.change_options(options) # ---- SpyderWidgetMixin API # ------------------------------------------------------------------------ - def setup(self, options=DEFAULT_OPTIONS): + def setup(self): self.setup_view() - self.set_name_filters(self.get_option('name_filters')) - self.set_name_filters(self.get_option('file_associations')) + self.set_name_filters(self.get_conf('name_filters')) + self.set_name_filters(self.get_conf('file_associations')) # New actions new_file_action = self.create_action( @@ -436,8 +436,9 @@ def setup(self, options=DEFAULT_OPTIONS): self.hidden_action = self.create_action( DirViewActions.ToggleHiddenFiles, text=_("Show hidden files"), - toggled=lambda val: self.set_option('show_hidden', val), - initial=self.get_option('show_hidden') + toggled=True, + initial=self.get_conf('show_hidden'), + option='show_hidden' ) self.filters_action = self.create_action( @@ -450,8 +451,9 @@ def setup(self, options=DEFAULT_OPTIONS): self.create_action( DirViewActions.ToggleSingleClick, text=_("Single click to open"), - toggled=lambda val: self.set_option('single_click_to_open', val), - initial=self.get_option('single_click_to_open') + toggled=True, + initial=self.get_conf('single_click_to_open'), + option='single_click_to_open' ) # IPython console actions @@ -482,23 +484,26 @@ def setup(self, options=DEFAULT_OPTIONS): size_column_action = self.create_action( DirViewActions.ToggleSizeColumn, text=_('Size'), - toggled=lambda val: self.set_option('size_column', val), - initial=self.get_option('size_column'), + toggled=True, + initial=self.get_conf('size_column'), register_shortcut=False, + option='size_column' ) type_column_action = self.create_action( DirViewActions.ToggleTypeColumn, text=_('Type') if sys.platform == 'darwin' else _('Type'), - toggled=lambda val: self.set_option('type_column', val), - initial=self.get_option('type_column'), + toggled=True, + initial=self.get_conf('type_column'), register_shortcut=False, + option='type_column' ) date_column_action = self.create_action( DirViewActions.ToggleDateColumn, text=_("Date modified"), - toggled=lambda val: self.set_option('date_column', val), - initial=self.get_option('date_column'), + toggled=True, + initial=self.get_conf('date_column'), register_shortcut=False, + option='date_column' ) # Header Context Menu @@ -582,7 +587,9 @@ def setup(self, options=DEFAULT_OPTIONS): # Signals self.context_menu.aboutToShow.connect(self.update_actions) - def on_option_update(self, option, value): + @on_conf_change(option=['size_column', 'type_column', 'date_column', + 'name_filters', 'show_hidden']) + def on_conf_update(self, option, value): if option == 'size_column': self.setColumnHidden(DirViewColumns.Size, not value) elif option == 'type_column': @@ -593,7 +600,7 @@ def on_option_update(self, option, value): if self.filter_on: self.filter_files(value) elif option == 'show_hidden': - self.set_show_hidden(self.get_option('show_hidden')) + self.set_show_hidden(self.get_conf('show_hidden')) def update_actions(self): fnames = self.get_selected_filenames() @@ -774,7 +781,7 @@ def mouseDoubleClickEvent(self, event): def mouseReleaseEvent(self, event): """Reimplement Qt method.""" super().mouseReleaseEvent(event) - if self.get_option('single_click_to_open'): + if self.get_conf('single_click_to_open'): self.clicked() def dragEnterEvent(self, event): @@ -899,7 +906,7 @@ def edit_filter(self): 'want to show, separated by commas.')) description_label.setOpenExternalLinks(True) description_label.setWordWrap(True) - filters = QTextEdit(", ".join(self.get_option('name_filters'))) + filters = QTextEdit(", ".join(self.get_conf('name_filters'))) layout = QVBoxLayout() layout.addWidget(description_label) layout.addWidget(filters) @@ -913,7 +920,7 @@ def handle_ok(): def handle_reset(): self.set_name_filters(NAME_FILTERS) - filters.setPlainText(", ".join(self.get_option('name_filters'))) + filters.setPlainText(", ".join(self.get_conf('name_filters'))) # Dialog buttons button_box = QDialogButtonBox(QDialogButtonBox.Reset | @@ -1382,7 +1389,7 @@ def open_interpreter(self, fnames=None): def filter_files(self, name_filters=None): """Filter files given the defined list of filters.""" if name_filters is None: - name_filters = self.get_option('name_filters') + name_filters = self.get_conf('name_filters') if self.filter_on: self.fsmodel.setNameFilters(name_filters) @@ -1407,7 +1414,7 @@ def get_common_file_associations(self, fnames): def get_file_associations(self, fname): """Return the list of matching file associations for `fname`.""" - for exts, values in self.get_option('file_associations').items(): + for exts, values in self.get_conf('file_associations').items(): clean_exts = [ext.strip() for ext in exts.split(',')] for ext in clean_exts: if fname.endswith((ext, ext[1:])): @@ -1549,22 +1556,21 @@ def restore_expanded_state(self): # ------------------------------------------------------------------------ def set_single_click_to_open(self, value): """Set single click to open items.""" - self.set_option('single_click_to_open', value) + self.set_conf('single_click_to_open', value) def set_file_associations(self, value): """Set file associations open items.""" - self.set_option('file_associations', value) + self.set_conf('file_associations', value) def set_name_filters(self, name_filters): """Set name filters""" - if self.get_option('name_filters') == ['']: - self.set_option('name_filters', []) + if self.get_conf('name_filters') == ['']: + self.set_conf('name_filters', []) else: if running_under_pytest(): - self.change_option('name_filters', name_filters) - self.set_option('name_filters', name_filters) + self.set_conf('name_filters', name_filters) else: - self.set_option('name_filters', name_filters) + self.set_conf('name_filters', name_filters) def set_show_hidden(self, state): """Toggle 'show hidden files' state""" @@ -1808,7 +1814,7 @@ class ExplorerTreeWidget(DirView): a folder, turning this folder in the new root parent of the tree. """ - def __init__(self, parent=None, options=DEFAULT_OPTIONS): + def __init__(self, parent=None): """Initialize the widget. Parameters @@ -1818,7 +1824,7 @@ def __init__(self, parent=None, options=DEFAULT_OPTIONS): options: dict, optional Dictionary with all the options used by the widget. """ - super().__init__(parent=parent, options=options) + super().__init__(parent=parent) # Attributes self._parent = parent @@ -1832,7 +1838,7 @@ def __init__(self, parent=None, options=DEFAULT_OPTIONS): # ---- SpyderWidgetMixin API # ------------------------------------------------------------------------ - def setup(self, options=DEFAULT_OPTIONS): + def setup(self): """ Perform the setup of the widget. @@ -1841,7 +1847,7 @@ def setup(self, options=DEFAULT_OPTIONS): options: dict, optional Dictionary with all the options used by the widget. """ - super().setup(options=options) + super().setup() # Actions self.previous_action = self.create_action( @@ -1876,19 +1882,6 @@ def update_actions(self): """Update the widget actions.""" super().update_actions() - def on_option_update(self, option, value): - """ - Handles the update or change of an option. - - Parameters - ---------- - option: str - String that define the option. - value: Any - The new value for the given option. - """ - super().on_option_update(option, value) - # ---- API # ------------------------------------------------------------------------ def change_filter_state(self): diff --git a/spyder/plugins/explorer/widgets/main_widget.py b/spyder/plugins/explorer/widgets/main_widget.py index a33391415c0..a324d835c4d 100644 --- a/spyder/plugins/explorer/widgets/main_widget.py +++ b/spyder/plugins/explorer/widgets/main_widget.py @@ -16,6 +16,7 @@ from qtpy.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget # Local imports +from spyder.api.decorators import on_conf_change from spyder.api.translations import get_translation from spyder.api.widgets import PluginMainWidget from spyder.plugins.explorer.widgets.explorer import ( @@ -157,7 +158,7 @@ class ExplorerWidget(PluginMainWidget): Path to use as working directory of interpreter. """ - def __init__(self, name, plugin, parent=None, options=DEFAULT_OPTIONS): + def __init__(self, name, plugin, parent=None): """ Initialize the widget. @@ -169,18 +170,14 @@ def __init__(self, name, plugin, parent=None, options=DEFAULT_OPTIONS): Plugin of the container parent: QWidget Parent of this widget - options: dict - Options of the container. """ - super().__init__(name, plugin=plugin, parent=parent, options=options) + super().__init__(name, plugin=plugin, parent=parent) # Widgets - tree_options = self.options_from_keys( - options, ExplorerTreeWidget.DEFAULT_OPTIONS) - self.treewidget = ExplorerTreeWidget(parent=self, options=tree_options) + self.treewidget = ExplorerTreeWidget(parent=self) # Setup widgets - self.treewidget.setup(tree_options) + self.treewidget.setup() self.chdir(getcwd_or_home()) # Layouts @@ -215,15 +212,8 @@ def get_title(self): """Return the title of the plugin tab.""" return _("Files") - def setup(self, options): - """ - Performs the setup of plugin's menu and actions. - - Parameters - ---------- - options: dict - Widget options. - """ + def setup(self): + """Performs the setup of plugin's menu and actions.""" # Menu menu = self.get_options_menu() @@ -259,19 +249,6 @@ def update_actions(self): """Handle the update of actions of the plugin.""" pass - def on_option_update(self, option, value): - """ - Handles the update or change of an option. - - Parameters - ---------- - option: str - String that define the option. - value: Any - The new value for the given option. - """ - self.treewidget.on_option_update(option, value) - # ---- Public API # ------------------------------------------------------------------------ def chdir(self, directory, emit=True): diff --git a/spyder/plugins/findinfiles/widgets.py b/spyder/plugins/findinfiles/widgets.py index 3e763e94b66..f04ba542546 100644 --- a/spyder/plugins/findinfiles/widgets.py +++ b/spyder/plugins/findinfiles/widgets.py @@ -27,6 +27,7 @@ QTreeWidgetItem) # Local imports +from spyder.api.decorators import on_conf_change from spyder.api.translations import get_translation from spyder.api.widgets import PluginMainWidget from spyder.config.gui import get_font, is_dark_interface @@ -867,21 +868,22 @@ class FindInFilesWidget(PluginMainWidget): to reaching the maximum number of results. """ - def __init__(self, name=None, plugin=None, parent=None, - options=DEFAULT_OPTIONS): - super().__init__(name, plugin, parent=parent, options=options) + def __init__(self, name=None, plugin=None, parent=None): + super().__init__(name, plugin, parent=parent) + self.set_conf('text_color', MAIN_TEXT_COLOR) + self.set_conf('hist_limit', MAX_PATH_HISTORY) # Attributes - self.text_color = self.get_option('text_color') - self.supported_encodings = self.get_option('supported_encodings') + self.text_color = self.get_conf('text_color') + self.supported_encodings = self.get_conf('supported_encodings') self.search_thread = None self.running = False self.more_options_action = None self.extras_toolbar = None - search_text = self.get_option('search_text') - path_history = self.get_option('path_history') - exclude = self.get_option('exclude') + search_text = self.get_conf('search_text', '') + path_history = self.get_conf('path_history', []) + exclude = self.get_conf('exclude') if not isinstance(search_text, (list, tuple)): search_text = [search_text] @@ -909,17 +911,17 @@ def __init__(self, name=None, plugin=None, parent=None, self.result_browser = ResultsBrowser( self, text_color=self.text_color, - max_results=self.get_option('max_results'), + max_results=self.get_conf('max_results'), ) # Setup self.exclude_label.setBuddy(self.exclude_pattern_edit) - exclude_idx = self.get_option('exclude_index') + exclude_idx = self.get_conf('exclude_index', None) if (exclude_idx is not None and exclude_idx >= 0 and exclude_idx < self.exclude_pattern_edit.count()): self.exclude_pattern_edit.setCurrentIndex(exclude_idx) - search_in_index = self.get_option('search_in_index') + search_in_index = self.get_conf('search_in_index', None) self.path_selection_combo.set_current_searchpath_index( search_in_index) @@ -949,22 +951,24 @@ def get_title(self): def get_focus_widget(self): return self.search_text_edit - def setup(self, options=DEFAULT_OPTIONS): + def setup(self): self.search_regexp_action = self.create_action( FindInFilesWidgetActions.ToggleSearchRegex, text=_('Regular expression'), tip=_('Regular expression'), icon=self.create_icon('regex'), - toggled=lambda val: self.set_option('search_text_regexp', val), - initial=self.get_option('search_text_regexp'), + toggled=True, + initial=self.get_conf('search_text_regexp'), + option='search_text_regexp' ) self.case_action = self.create_action( FindInFilesWidgetActions.ToggleExcludeCase, text=_("Case sensitive"), tip=_("Case sensitive"), icon=self.create_icon("format_letter_case"), - toggled=lambda val: self.set_option('case_sensitive', val), - initial=self.get_option('case_sensitive'), + toggled=True, + initial=self.get_conf('case_sensitive'), + option='case_sensitive' ) self.find_action = self.create_action( FindInFilesWidgetActions.Find, @@ -980,24 +984,27 @@ def setup(self, options=DEFAULT_OPTIONS): text=_('Regular expression'), tip=_('Regular expression'), icon=self.create_icon('regex'), - toggled=lambda val: self.set_option('exclude_regexp', val), - initial=self.get_option('exclude_regexp'), + toggled=True, + initial=self.get_conf('exclude_regexp'), + option='exclude_regexp' ) self.exclude_case_action = self.create_action( FindInFilesWidgetActions.ToggleCase, text=_("Exclude case sensitive"), tip=_("Exclude case sensitive"), icon=self.create_icon("format_letter_case"), - toggled=lambda val: self.set_option('exclude_case_sensitive', val), - initial=self.get_option('exclude_case_sensitive'), + toggled=True, + initial=self.get_conf('exclude_case_sensitive'), + option='exclude_case_sensitive' ) self.more_options_action = self.create_action( FindInFilesWidgetActions.ToggleMoreOptions, text=_('Show advanced options'), tip=_('Show advanced options'), icon=self.create_icon("options_more"), - toggled=lambda val: self.set_option('more_options', val), - initial=self.get_option('more_options'), + toggled=True, + initial=self.get_conf('more_options'), + option='more_options' ) self.set_max_results_action = self.create_action( FindInFilesWidgetActions.MaxResults, @@ -1093,6 +1100,29 @@ def on_option_update(self, option, value): elif option == 'max_results': self.result_browser.set_max_results(value) + @on_conf_change(option='more_options') + def on_more_options_update(self, value): + self.exclude_pattern_edit.setMinimumWidth( + self.search_text_edit.width()) + + if value: + icon = self.create_icon('options_less') + tip = _('Hide advanced options') + else: + icon = self.create_icon('options_more') + tip = _('Show advanced options') + + if self.extras_toolbar: + self.extras_toolbar.setVisible(value) + + if self.more_options_action: + self.more_options_action.setIcon(icon) + self.more_options_action.setToolTip(tip) + + @on_conf_change(option='max_results') + def on_max_results_update(self, value): + self.result_browser.set_max_results(value) + # --- Private API # ------------------------------------------------------------------------ def _update_size(self, size, old_size): @@ -1170,20 +1200,20 @@ def _update_options(self): """ Extract search options from widgets and set the corresponding option. """ - hist_limit = self.get_option('hist_limit') + hist_limit = self.get_conf('hist_limit') search_texts = [str(self.search_text_edit.itemText(index)) for index in range(self.search_text_edit.count())] excludes = [str(self.search_text_edit.itemText(index)) for index in range(self.exclude_pattern_edit.count())] path_history = self.path_selection_combo.get_external_paths() - self.set_option('path_history', path_history) - self.set_option('search_text', search_texts[:hist_limit]) - self.set_option('exclude', excludes[:hist_limit]) - self.set_option('path_history', path_history[-hist_limit:]) - self.set_option( + self.set_conf('path_history', path_history) + self.set_conf('search_text', search_texts[:hist_limit]) + self.set_conf('exclude', excludes[:hist_limit]) + self.set_conf('path_history', path_history[-hist_limit:]) + self.set_conf( 'exclude_index', self.exclude_pattern_edit.currentIndex()) - self.set_option( + self.set_conf( 'search_in_index', self.path_selection_combo.currentIndex()) def _handle_search_complete(self, completed): @@ -1375,11 +1405,11 @@ def set_max_results(self, value=None): # Connect slot dialog.intValueSelected.connect( - lambda value: self.set_option('max_results', value)) + lambda value: self.set_conf('max_results', value)) dialog.show() else: - self.set_option('max_results', value) + self.set_conf('max_results', value) def test(): diff --git a/spyder/plugins/help/widgets.py b/spyder/plugins/help/widgets.py index 7afe3592a8d..1f9717579e5 100644 --- a/spyder/plugins/help/widgets.py +++ b/spyder/plugins/help/widgets.py @@ -22,8 +22,10 @@ QVBoxLayout, QWidget) # Local imports +from spyder.api.decorators import on_conf_change from spyder.api.translations import get_translation from spyder.api.widgets import PluginMainWidget +from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.config.base import get_image_path, get_module_source_path from spyder.config.gui import is_dark_interface from spyder.plugins.help.utils.sphinxify import (CSS_PATH, DARK_CSS_PATH, @@ -98,7 +100,7 @@ def is_valid(self, qstr=None): return False objtxt = to_text_string(qstr) shell_is_defined = False - if self.help.get_option('automatic_import'): + if self.help.get_conf('automatic_import'): shell = self.help.internal_shell if shell is not None: shell_is_defined = shell.is_defined(objtxt, force_import=True) @@ -137,14 +139,14 @@ def validate(self, qstr, editing=True): self.valid.emit(False, False) -class RichText(QWidget): +class RichText(QWidget, SpyderWidgetMixin): """ WebView widget with find dialog """ sig_link_clicked = Signal(QUrl) def __init__(self, parent): - QWidget.__init__(self, parent) + super().__init__(parent, class_parent=parent) self.webview = FrameWebView(self) self.webview.setup() @@ -263,23 +265,6 @@ def select_all(self): class HelpWidget(PluginMainWidget): - DEFAULT_OPTIONS = { - 'automatic_import': True, - 'connect/editor': False, - 'connect/ipython_console': False, - 'css_path': DARK_CSS_PATH, - 'locked': False, - 'math': True, - 'max_history_entries': 20, - 'plain_mode': False, - 'rich_mode': True, - 'show_source': False, - 'wrap': True, - # Shortcut CONF - 'editor_shortcut': 'Ctrl+I', - # Shortcut CONF - 'console_shortcut': 'Ctrl+I', - } ENABLE_SPINNER = True # Signals @@ -292,9 +277,8 @@ class HelpWidget(PluginMainWidget): sig_render_finished = Signal() """This signal is emitted to inform a help text rendering has finished.""" - def __init__(self, name=None, plugin=None, parent=None, - options=DEFAULT_OPTIONS): - super().__init__(name, plugin, parent, options) + def __init__(self, name=None, plugin=None, parent=None): + super().__init__(name, plugin, parent) # Attributes self._starting_up = True @@ -303,7 +287,7 @@ def __init__(self, name=None, plugin=None, parent=None, self._last_editor_doc = None self._last_console_cb = None self._last_editor_cb = None - self.css_path = self.get_option('css_path') + self.css_path = self.get_conf('css_path') self.no_docs = _("No documentation available") self.docstring = True # TODO: What is this used for? @@ -325,9 +309,9 @@ def __init__(self, name=None, plugin=None, parent=None, # Setup self.object_edit.setReadOnly(True) - self.object_combo.setMaxCount(self.get_option('max_history_entries')) + self.object_combo.setMaxCount(self.get_conf('max_history_entries')) self.object_combo.setItemText(0, '') - self.plain_text.set_wrap_mode(self.get_option('wrap')) + self.plain_text.set_wrap_mode(self.get_conf('wrap')) self.source_combo.addItems([_("Console"), _("Editor")]) if (not programs.is_module_installed('rope') and not programs.is_module_installed('jedi', '>=0.11.0')): @@ -357,12 +341,13 @@ def __init__(self, name=None, plugin=None, parent=None, def get_title(self): return _('Help') - def setup(self, options): + def setup(self): self.wrap_action = self.create_action( name=HelpWidgetActions.ToggleWrap, text=_("Wrap lines"), - toggled=lambda value: self.set_option('wrap', value), - initial=self.get_option('wrap'), + toggled=True, + initial=self.get_conf('wrap'), + option='wrap' ) self.copy_action = self.create_action( name=HelpWidgetActions.CopyAction, @@ -379,32 +364,39 @@ def setup(self, options): self.auto_import_action = self.create_action( name=HelpWidgetActions.ToggleAutomaticImport, text=_("Automatic import"), - toggled=lambda value: self.set_option('automatic_import', value), - initial=self.get_option('automatic_import'), + toggled=True, + initial=self.get_conf('automatic_import'), + option='automatic_import' ) self.show_source_action = self.create_action( name=HelpWidgetActions.ToggleShowSource, text=_("Show Source"), - toggled=lambda value: self.set_option('show_source', value), + toggled=True, + option='show_source' ) + self.get_action(HelpWidgetActions.ToggleShowSource) + self.rich_text_action = self.create_action( name=HelpWidgetActions.ToggleRichMode, text=_("Rich Text"), - toggled=lambda value: self.set_option('rich_mode', value), - initial=self.get_option('rich_mode'), + toggled=True, + initial=self.get_conf('rich_mode'), + option='rich_mode' ) self.plain_text_action = self.create_action( name=HelpWidgetActions.TogglePlainMode, text=_("Plain Text"), - toggled=lambda value: self.set_option('plain_mode', value), - initial=self.get_option('plain_mode'), + toggled=True, + initial=self.get_conf('plain_mode'), + option='plain_mode' ) self.locked_action = self.create_action( name=HelpWidgetActions.ToggleLocked, text=_("Lock/Unlock"), - toggled=lambda value: self.set_option('locked', value), + toggled=True, icon=self.create_icon('lock_open'), - initial=self.get_option('locked'), + initial=self.get_conf('locked'), + option='locked' ) self.home_action = self.create_action( name=HelpWidgetActions.Home, @@ -473,47 +465,52 @@ def setup(self, options): self.plain_text.sig_custom_context_menu_requested.connect( self._show_plain_text_context_menu) - def on_option_update(self, option, value): - if option == 'wrap': - self.plain_text.set_wrap_mode(value) - elif option == 'locked': - if value: - icon = self.create_icon('lock') - tip = _("Unlock") - else: - icon = self.create_icon('lock_open') - tip = _("Lock") - - action = self.get_action(HelpWidgetActions.ToggleLocked) - action.setIcon(icon) - action.setToolTip(tip) - elif option == 'automatic_import': - self.object_combo.validate_current_text() - self.force_refresh() - elif option == 'rich_mode': - if value: - # Plain Text OFF / Rich text ON - self.docstring = not value - self.stack_layout.setCurrentWidget(self.rich_text) - self.get_action(HelpWidgetActions.ToggleShowSource).setChecked( - False) - else: - # Plain Text ON / Rich text OFF - self.docstring = value - self.stack_layout.setCurrentWidget(self.plain_text) - - self.force_refresh() - elif option == 'plain_mode': - # Handled on rich mode option - pass - elif option == 'show_source': - if value: - self.switch_to_plain_text() - self.get_action(HelpWidgetActions.ToggleRichMode).setChecked( - False) + @on_conf_change(option='wrap') + def on_wrap_option_update(self, value): + self.plain_text.set_wrap_mode(value) + @on_conf_change(option='locked') + def on_lock_update(self, value): + if value: + icon = self.create_icon('lock') + tip = _("Unlock") + else: + icon = self.create_icon('lock_open') + tip = _("Lock") + + action = self.get_action(HelpWidgetActions.ToggleLocked) + action.setIcon(icon) + action.setToolTip(tip) + + @on_conf_change(option='automatic_import') + def on_automatic_import_update(self, value): + self.object_combo.validate_current_text() + self.force_refresh() + + @on_conf_change(option='rich_mode') + def on_rich_mode_update(self, value): + if value: + # Plain Text OFF / Rich text ON self.docstring = not value - self.force_refresh() + self.stack_layout.setCurrentWidget(self.rich_text) + self.get_action(HelpWidgetActions.ToggleShowSource).setChecked( + False) + else: + # Plain Text ON / Rich text OFF + self.docstring = value + self.stack_layout.setCurrentWidget(self.plain_text) + + self.force_refresh() + + @on_conf_change(option='show_source') + def on_show_source_update(self, value): + if value: + self.switch_to_plain_text() + self.get_action(HelpWidgetActions.ToggleRichMode).setChecked( + False) + + self.docstring = not value + self.force_refresh() def update_actions(self): for __, action in self.get_actions().items(): @@ -630,7 +627,7 @@ def restore_text(self): cb = self._last_editor_cb if cb is None: - if self.get_option('plain_mode'): + if self.get_conf('plain_mode'): self.switch_to_plain_text() else: self.switch_to_rich_text() @@ -646,7 +643,7 @@ def restore_text(self): @property def find_widget(self): """Show find widget.""" - if self.get_option('plain_mode'): + if self.get_conf('plain_mode'): return self.plain_text.find_widget else: return self.rich_text.find_widget @@ -747,14 +744,16 @@ def show_intro_message(self): "activate this behavior in %s.") prefs = _("Preferences > Help") - shortcut_editor = self.get_option('editor_shortcut') - shortcut_console = self.get_option('console_shortcut') + shortcut_editor = self.get_conf('editor/inspect current object', + section='shortcuts') + shortcut_console = self.get_conf('console/inspect current object', + section='shortcuts') if sys.platform == 'darwin': shortcut_editor = shortcut_editor.replace('Ctrl', 'Cmd') shortcut_console = shortcut_console.replace('Ctrl', 'Cmd') - if self.get_option('rich_mode'): + if self.get_conf('rich_mode'): title = _("Usage") tutorial_message = _("New to Spyder? Read our") tutorial = _("tutorial") @@ -882,7 +881,7 @@ def set_object_text(self, text, force_refresh=False, ignore_unknown=False): -------- :py:meth:spyder.widgets.mixins.GetHelpMixin.show_object_info """ - if self.get_option('locked') and not force_refresh: + if self.get_conf('locked') and not force_refresh: return self.switch_to_console_source() @@ -926,14 +925,14 @@ def set_editor_doc(self, help_data, force_refresh=False): 'path': str, } """ - if self.get_option('locked') and not force_refresh: + if self.get_conf('locked') and not force_refresh: return self.switch_to_editor_source() self._last_editor_doc = help_data self.object_edit.setText(help_data['obj_text']) - if self.get_option('rich_mode'): + if self.get_conf('rich_mode'): self.render_sphinx_doc(help_data) else: self.set_plain_text(help_data, is_code=False) @@ -981,7 +980,7 @@ def render_sphinx_doc(self, help_data, context=None, css_path=CSS_PATH): dname = '' # Math rendering option could have changed - self._sphinx_thread.render(help_data, context, self.get_option('math'), + self._sphinx_thread.render(help_data, context, self.get_conf('math'), dname, css_path=self.css_path) self.show_loading_message() @@ -1005,7 +1004,7 @@ def show_help(self, obj_text, ignore_unknown=False): obj_text = to_text_string(obj_text) if not shell.is_defined(obj_text): - if (self.get_option('automatic_import') + if (self.get_conf('automatic_import') and self.internal_shell.is_defined(obj_text, force_import=True)): shell = self.internal_shell @@ -1020,7 +1019,7 @@ def show_help(self, obj_text, ignore_unknown=False): is_code = False - if self.get_option('rich_mode'): + if self.get_conf('rich_mode'): self.render_sphinx_doc(doc, css_path=self.css_path) return doc is not None elif self.docstring: diff --git a/spyder/plugins/history/widgets.py b/spyder/plugins/history/widgets.py index baa684fe7b5..c394256d4ae 100644 --- a/spyder/plugins/history/widgets.py +++ b/spyder/plugins/history/widgets.py @@ -17,6 +17,7 @@ from qtpy.QtWidgets import QInputDialog, QVBoxLayout, QWidget # Local imports +from spyder.api.decorators import on_conf_change from spyder.api.translations import get_translation from spyder.api.widgets import PluginMainWidget from spyder.py3compat import is_text_string, to_text_string @@ -69,8 +70,8 @@ class HistoryWidget(PluginMainWidget): changes. """ - def __init__(self, name, plugin, parent, options=DEFAULT_OPTIONS): - super().__init__(name, plugin, parent, options) + def __init__(self, name, plugin, parent): + super().__init__(name, plugin, parent) # Attributes self.editors = [] @@ -119,19 +120,21 @@ def get_title(self): def get_focus_widget(self): return self.tabwidget.currentWidget() - def setup(self, options): + def setup(self): # Actions self.wrap_action = self.create_action( HistoryWidgetActions.ToggleWrap, text=_("Wrap lines"), - toggled=lambda value: self.set_option('wrap', value), - initial=self.get_option('wrap'), + toggled=True, + initial=self.get_conf('wrap'), + option='wrap' ) self.linenumbers_action = self.create_action( HistoryWidgetActions.ToggleLineNumbers, text=_("Show line numbers"), - toggled=lambda value: self.set_option('line_numbers', value), - initial=self.get_option('line_numbers'), + toggled=True, + initial=self.get_conf('line_numbers'), + option='line_numbers' ) # Menu @@ -146,17 +149,20 @@ def setup(self, options): def update_actions(self): pass - def on_option_update(self, option, value): - if self.tabwidget is not None: - if option == 'wrap': - for editor in self.editors: - editor.toggle_wrap_mode(value) - elif option == 'line_numbers': - for editor in self.editors: - editor.toggle_line_numbers(value) - elif option == 'color_scheme_name': - for editor in self.editors: - editor.set_font(self.font) + @on_conf_change(option='wrap') + def on_wrap_update(self, value): + for editor in self.editors: + editor.toggle_wrap_mode(value) + + @on_conf_change(option='line_numbers') + def on_line_numbers_update(self, value): + for editor in self.editors: + editor.toggle_line_numbers(value) + + @on_conf_change(option='selected', section='appearance') + def on_color_scheme_change(self, value): + for editor in self.editors: + editor.set_font(self.font) # --- Public API # ------------------------------------------------------------------------ @@ -253,11 +259,11 @@ def add_history(self, filename): # Setup language = 'py' if osp.splitext(filename)[1] == '.py' else 'bat' editor.setup_editor( - linenumbers=self.get_option('line_numbers'), + linenumbers=self.get_conf('line_numbers'), language=language, - color_scheme=self.get_option('color_scheme_name'), + color_scheme=self.get_conf('selected', section='appearance'), font=self.font, - wrap=self.get_option('wrap'), + wrap=self.get_conf('wrap'), ) editor.setReadOnly(True) editor.set_text(self.get_filename_text(filename)) @@ -292,7 +298,7 @@ def append_to_history(self, filename, command): command = to_text_string(command) self.editors[index].append(command) - if self.get_option('go_to_eof'): + if self.get_conf('go_to_eof'): self.editors[index].set_cursor_position('eof') self.tabwidget.setCurrentIndex(index) diff --git a/spyder/plugins/maininterpreter/container.py b/spyder/plugins/maininterpreter/container.py index 50ee6ca2dde..397ba7610cc 100644 --- a/spyder/plugins/maininterpreter/container.py +++ b/spyder/plugins/maininterpreter/container.py @@ -14,6 +14,7 @@ from qtpy.QtCore import Signal # Local imports +from spyder.api.decorators import on_conf_change from spyder.api.widgets import PluginMainContainer from spyder.plugins.maininterpreter.widgets.status import InterpreterStatus from spyder.utils.misc import get_python_executable @@ -42,7 +43,7 @@ class MainInterpreterContainer(PluginMainContainer): """ # ---- PluginMainContainer API - def setup(self, options): + def setup(self): self.interpreter_status = InterpreterStatus( parent=self, @@ -61,25 +62,26 @@ def setup(self, options): def update_actions(self): pass - def on_option_update(self, option, value): + @on_conf_change(option=['default', 'custom_interpreter', 'custom']) + def section_conf_update(self, option, value): if option in ['default', 'custom_interpreter', 'custom'] and value: self._update_status() self.sig_interpreter_changed.emit() # Set new interpreter - executable = osp.normpath(self.get_option('custom_interpreter')) + executable = osp.normpath(self.get_conf('custom_interpreter')) if (option in ['custom', 'custom_interpreter'] and osp.isfile(executable)): self.sig_add_to_custom_interpreters_requested.emit(executable) # ---- Public API def get_main_interpreter(self): - if self.get_option('default'): + if self.get_conf('default'): return get_python_executable() else: - executable = osp.normpath(self.get_option('custom_interpreter')) + executable = osp.normpath(self.get_conf('custom_interpreter')) - # Check if custom interpreter is stil present + # Check if custom interpreter is still present if osp.isfile(executable): return executable else: diff --git a/spyder/plugins/onlinehelp/widgets.py b/spyder/plugins/onlinehelp/widgets.py index cd8247b705d..fb18e679160 100644 --- a/spyder/plugins/onlinehelp/widgets.py +++ b/spyder/plugins/onlinehelp/widgets.py @@ -140,9 +140,8 @@ class PydocBrowser(PluginMainWidget): This signal is emitted to indicate the help page has finished loading. """ - def __init__(self, name=None, plugin=None, parent=None, - options=DEFAULT_OPTIONS): - super().__init__(name, plugin, parent=parent, options=options) + def __init__(self, name=None, plugin=None, parent=None): + super().__init__(name, plugin, parent=parent) self._is_running = False self.home_url = None @@ -152,18 +151,18 @@ def __init__(self, name=None, plugin=None, parent=None, self.label = QLabel(_("Package:")) self.url_combo = UrlComboBox(self) self.webview = WebView(self, - handle_links=self.get_option('handle_links')) + handle_links=self.get_conf('handle_links')) self.find_widget = FindReplace(self) # Setup self.find_widget.set_editor(self.webview) self.find_widget.hide() - self.url_combo.setMaxCount(self.get_option('max_history_entries')) + self.url_combo.setMaxCount(self.get_conf('max_history_entries')) tip = _('Write a package name here, e.g. pandas') self.url_combo.lineEdit().setPlaceholderText(tip) self.url_combo.lineEdit().setToolTip(tip) self.webview.setup() - self.webview.set_zoom_factor(self.get_option('zoom_factor')) + self.webview.set_zoom_factor(self.get_conf('zoom_factor')) # Layout spacing = 10 @@ -194,7 +193,7 @@ def get_focus_widget(self): self.url_combo.lineEdit().selectAll() return self.url_combo - def setup(self, options={}): + def setup(self): # Actions home_action = self.create_action( PydocBrowserActions.Home, @@ -248,9 +247,6 @@ def update_actions(self): refresh_action.setVisible(not self._is_running) stop_action.setVisible(self._is_running) - def on_option_update(self, option, value): - pass - # --- Private API # ------------------------------------------------------------------------ def _start(self): diff --git a/spyder/plugins/outlineexplorer/widgets.py b/spyder/plugins/outlineexplorer/widgets.py index f4bce737789..2b0b4770860 100644 --- a/spyder/plugins/outlineexplorer/widgets.py +++ b/spyder/plugins/outlineexplorer/widgets.py @@ -914,6 +914,8 @@ def __init__(self, parent=None, show_fullpath=True, show_all_files=True, group_cells=True, show_comments=True, sort_files_alphabetically=False, display_variables=False, follow_cursor=True, options_button=None): + # TODO: Remove once the OutlineExplorer is migrated + self.CONF_SECTION = parent.CONF_SECTION QWidget.__init__(self, parent) self.treewidget = OutlineExplorerTreeWidget( diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py index 8d2e1df01c0..83d65efa0ea 100644 --- a/spyder/plugins/plots/widgets/figurebrowser.py +++ b/spyder/plugins/plots/widgets/figurebrowser.py @@ -73,7 +73,7 @@ def get_unique_figname(dirname, root, ext, start_at_zero=False): return osp.join(dirname, figname) -class FigureBrowser(QWidget): +class FigureBrowser(QWidget, SpyderWidgetMixin): """ Widget to browse the figures that were sent by the kernel to the IPython console to be plotted inline. @@ -138,7 +138,7 @@ class FigureBrowser(QWidget): """ def __init__(self, parent=None, background_color=None): - super().__init__(parent=parent) + super().__init__(parent=parent, class_parent=parent) self.shellwidget = None self.is_visible = True self.figviewer = None @@ -321,7 +321,7 @@ class FigureViewer(QScrollArea, SpyderWidgetMixin): """This signal is emitted when a new figure is loaded.""" def __init__(self, parent=None, background_color=None): - super().__init__(parent) + super().__init__(parent, class_parent=parent) self.setAlignment(Qt.AlignCenter) self.viewport().setObjectName("figviewport") self.viewport().setStyleSheet( diff --git a/spyder/plugins/plots/widgets/main_widget.py b/spyder/plugins/plots/widgets/main_widget.py index 48eebf39c86..a67c30e0911 100644 --- a/spyder/plugins/plots/widgets/main_widget.py +++ b/spyder/plugins/plots/widgets/main_widget.py @@ -13,8 +13,10 @@ from qtpy.QtWidgets import QHBoxLayout, QSpinBox, QStackedWidget # Local imports +from spyder.api.decorators import on_conf_change from spyder.api.translations import get_translation from spyder.api.widgets import PluginMainWidgetMenus, PluginMainWidget +from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.config.gui import is_dark_interface from spyder.plugins.plots.widgets.figurebrowser import FigureBrowser from spyder.utils.misc import getcwd_or_home @@ -58,7 +60,7 @@ class PlotsWidgetMainToolbarSections: # --- Widgets # ---------------------------------------------------------------------------- -class PlotsStackedWidget(QStackedWidget): +class PlotsStackedWidget(QStackedWidget, SpyderWidgetMixin): # Signals sig_thumbnail_menu_requested = Signal(QPoint, object) """ @@ -108,7 +110,7 @@ class PlotsStackedWidget(QStackedWidget): """ def __init__(self, parent): - super().__init__(parent=parent) + super().__init__(parent=parent, class_parent=parent) def addWidget(self, widget): """Override Qt method.""" @@ -149,9 +151,8 @@ class PlotsWidget(PluginMainWidget): Start redirect (True) or stop redirect (False). """ - def __init__(self, name=None, plugin=None, parent=None, - options=DEFAULT_OPTIONS): - super().__init__(name, plugin, parent, options) + def __init__(self, name=None, plugin=None, parent=None): + super().__init__(name, plugin, parent) # Widgets self._stack = PlotsStackedWidget(parent=self) @@ -182,7 +183,7 @@ def __init__(self, name=None, plugin=None, parent=None, self._stack.sig_zoom_changed.connect(self.zoom_disp.setValue) self._stack.sig_figure_loaded.connect(self.update_actions) self._stack.sig_save_dir_changed.connect( - lambda val: self.set_option('save_dir', val)) + lambda val: self.set_conf('save_dir', val)) # --- PluginMainWidget API # ------------------------------------------------------------------------ @@ -197,28 +198,31 @@ def get_focus_widget(self): return widget - def setup(self, options): + def setup(self): # Menu actions self.mute_action = self.create_action( name=PlotsWidgetActions.ToggleMuteInlinePlotting, text=_("Mute inline plotting"), tip=_("Mute inline plotting in the ipython console."), - toggled=lambda val: self.set_option('mute_inline_plotting', val), - initial=options['mute_inline_plotting'], + toggled=True, + initial=self.get_conf('mute_inline_plotting'), + option='mute_inline_plotting' ) self.outline_action = self.create_action( name=PlotsWidgetActions.ToggleShowPlotOutline, text=_("Show plot outline"), tip=_("Show the plot outline."), - toggled=lambda val: self.set_option('show_plot_outline', val), - initial=options['show_plot_outline'], + toggled=True, + initial=self.get_conf('show_plot_outline'), + option='show_plot_outline' ) self.fit_action = self.create_action( name=PlotsWidgetActions.ToggleAutoFitPlotting, text=_("Fit plots to window"), tip=_("Automatically fit plots to Plot pane size."), - toggled=lambda val: self.set_option('auto_fit_plotting', val), - initial=options['auto_fit_plotting'], + toggled=True, + initial=self.get_conf('auto_fit_plotting'), + option='auto_fit_plotting' ) # Toolbar actions @@ -349,12 +353,14 @@ def update_actions(self): # Disable zoom buttons if autofit if value: - value = not self.get_option('auto_fit_plotting') + value = not self.get_conf('auto_fit_plotting') self.get_action(PlotsWidgetActions.ZoomIn).setEnabled(value) self.get_action(PlotsWidgetActions.ZoomOut).setEnabled(value) self.zoom_disp.setEnabled(value) - def on_option_update(self, option, value): + @on_conf_change(option=['auto_fit_plotting', 'mute_inline_plotting', + 'show_plot_outline', 'save_dir']) + def on_section_conf_change(self, option, value): for index in range(self.count()): widget = self._stack.widget(index) if widget: @@ -467,10 +473,16 @@ def set_shellwidget(self, shellwidget): shelwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget The shell widget. """ + option_keys = [('auto_fit_plotting', True), + ('mute_inline_plotting', True), + ('show_plot_outline', True), + ('save_dir', getcwd_or_home())] + + conf_values = {k: self.get_conf(k, d) for k, d in option_keys} shellwidget_id = id(shellwidget) if shellwidget_id in self._shellwidgets: fig_browser = self._shellwidgets[shellwidget_id] - fig_browser.setup(self._options) + fig_browser.setup(conf_values) self.set_current_widget(fig_browser) def show_figure_menu(self, qpoint): diff --git a/spyder/plugins/preferences/widgets/container.py b/spyder/plugins/preferences/widgets/container.py index fc1f2375590..a414217c292 100644 --- a/spyder/plugins/preferences/widgets/container.py +++ b/spyder/plugins/preferences/widgets/container.py @@ -78,7 +78,7 @@ def is_dialog_open(self): return self.dialog is not None and self.dialog.isVisible() # ---- PluginMainContainer API - def setup(self, options=None): + def setup(self): pass def update_actions(self): diff --git a/spyder/plugins/profiler/widgets/main_widget.py b/spyder/plugins/profiler/widgets/main_widget.py index ce1fa806e69..3d9b52542e4 100644 --- a/spyder/plugins/profiler/widgets/main_widget.py +++ b/spyder/plugins/profiler/widgets/main_widget.py @@ -162,9 +162,9 @@ class ProfilerWidget(PluginMainWidget): sig_finished = Signal() """This signal is emitted to inform the profile profiling has finished.""" - def __init__(self, name=None, plugin=None, parent=None, - options=DEFAULT_OPTIONS): - super().__init__(name, plugin, parent, options) + def __init__(self, name=None, plugin=None, parent=None): + super().__init__(name, plugin, parent) + self.set_conf('text_color', MAIN_TEXT_COLOR) # Attributes self._last_wdir = None @@ -173,7 +173,7 @@ def __init__(self, name=None, plugin=None, parent=None, self.error_output = None self.output = None self.running = False - self.text_color = self.get_option('text_color') + self.text_color = self.get_conf('text_color') # Widgets self.process = None @@ -198,7 +198,7 @@ def get_title(self): def get_focus_widget(self): return self.datatree - def setup(self, options): + def setup(self): self.start_action = self.create_action( ProfilerWidgetActions.Run, text=_("Run profiler"), @@ -310,9 +310,6 @@ def update_actions(self): self.start_action.setEnabled(bool(self.filecombo.currentText())) - def on_option_update(self, option, value): - pass - # --- Private API # ------------------------------------------------------------------------ def _kill_if_running(self): @@ -667,7 +664,7 @@ class ProfilerDataTree(QTreeWidget, SpyderWidgetMixin): sig_edit_goto_requested = Signal(str, int, str) def __init__(self, parent=None): - super().__init__(parent) + super().__init__(parent, class_parent=parent) self.header_list = [_('Function/Module'), _('Total Time'), _('Diff'), _('Local Time'), _('Diff'), _('Calls'), _('Diff'), _('File:line')] diff --git a/spyder/plugins/projects/plugin.py b/spyder/plugins/projects/plugin.py index 6559842c222..1ab162f823f 100644 --- a/spyder/plugins/projects/plugin.py +++ b/spyder/plugins/projects/plugin.py @@ -240,13 +240,11 @@ def register_plugin(self): lambda v: self.main.editor.set_current_project_path()) # Connect to file explorer to keep single click to open files in sync - self.main.explorer.sig_option_changed.connect( - self.set_single_click_to_open - ) - + # TODO: Remove this once projects is migrated + CONF.observe_configuration(self, 'explorer', 'single_click_to_open') self.register_project_type(self, EmptyProject) - def set_single_click_to_open(self, option, value): + def on_configuration_change(self, option, section, value): """Set single click to open files and directories.""" if option == 'single_click_to_open': self.explorer.treewidget.set_single_click_to_open(value) diff --git a/spyder/plugins/projects/widgets/explorer.py b/spyder/plugins/projects/widgets/explorer.py index 043fc6b0e66..3b1dda8fffa 100644 --- a/spyder/plugins/projects/widgets/explorer.py +++ b/spyder/plugins/projects/widgets/explorer.py @@ -174,6 +174,8 @@ class ProjectExplorerWidget(QWidget): def __init__(self, parent, name_filters=[], show_hscrollbar=True, options_button=None, single_click_to_open=False): + # TODO: Remove once Projects is Migrated + self.CONF_SECTION = parent.CONF_SECTION QWidget.__init__(self, parent) self.name_filters = name_filters @@ -189,7 +191,7 @@ def __init__(self, parent, name_filters=[], show_hscrollbar=True, 'single_click_to_open': single_click_to_open, 'file_associations': {}, } - self.treewidget.setup(options) + self.treewidget.setup() self.treewidget.setup_view() self.treewidget.hide() self.treewidget.sig_open_file_requested.connect( diff --git a/spyder/plugins/pylint/main_widget.py b/spyder/plugins/pylint/main_widget.py index 00e7872dee2..2c78a297005 100644 --- a/spyder/plugins/pylint/main_widget.py +++ b/spyder/plugins/pylint/main_widget.py @@ -28,6 +28,7 @@ QSizePolicy, QTreeWidgetItem, QVBoxLayout, QWidget) # Local imports +from spyder.api.decorators import on_conf_change from spyder.api.translations import get_translation from spyder.api.widgets import PluginMainWidget from spyder.config.base import get_conf_path, running_in_mac_app @@ -241,9 +242,8 @@ class PylintWidget(PluginMainWidget): level. """ - def __init__(self, name=None, plugin=None, parent=None, - options=DEFAULT_OPTIONS): - super().__init__(name, plugin, parent, options) + def __init__(self, name=None, plugin=None, parent=None): + super().__init__(name, plugin, parent) # Attributes self._process = None @@ -251,7 +251,7 @@ def __init__(self, name=None, plugin=None, parent=None, self.error_output = None self.filename = None self.rdata = [] - self.curr_filenames = self.get_option("history_filenames") + self.curr_filenames = self.get_conf("history_filenames") self.code_analysis_action = None self.browse_action = None @@ -384,7 +384,7 @@ def _kill_process(self): def _update_combobox_history(self): """Change the number of files listed in the history combobox.""" - max_entries = self.get_option("max_entries") + max_entries = self.get_conf("max_entries") if self.filecombo.count() > max_entries: num_elements = self.filecombo.count() diff = num_elements - max_entries @@ -411,7 +411,7 @@ def _save_history(self): list_save_files.append(fname) self.curr_filenames = list_save_files[:MAX_HISTORY_ENTRIES] - self.set_option("history_filenames", self.curr_filenames) + self.set_conf("history_filenames", self.curr_filenames) else: self.curr_filenames = [] @@ -423,7 +423,7 @@ def get_title(self): def get_focus_widget(self): return self.treewidget - def setup(self, options): + def setup(self): change_history_depth_action = self.create_action( PylintWidgetActions.ChangeHistory, text=_("History..."), @@ -526,7 +526,8 @@ def setup(self, options): # Signals self.filecombo.valid.connect(self.code_analysis_action.setEnabled) - def on_option_update(self, option, value): + @on_conf_change(option=['max_entries', 'history_filenames']) + def on_conf_update(self, option, value): if option == "max_entries": self._update_combobox_history() elif option == "history_filenames": @@ -574,15 +575,15 @@ def change_history_depth(self, value=None): dialog.setInputMode(QInputDialog.IntInput) dialog.setIntRange(MIN_HISTORY_ENTRIES, MAX_HISTORY_ENTRIES) dialog.setIntStep(1) - dialog.setIntValue(self.get_option("max_entries")) + dialog.setIntValue(self.get_conf("max_entries")) # Connect slot dialog.intValueSelected.connect( - lambda value: self.set_option("max_entries", value)) + lambda value: self.set_conf("max_entries", value)) dialog.show() else: - self.set_option("max_entries", value) + self.set_conf("max_entries", value) def get_filename(self): """ @@ -625,7 +626,7 @@ def set_filename(self, filename): self.filecombo.setCurrentIndex(0) num_elements = self.filecombo.count() - if num_elements > self.get_option("max_entries"): + if num_elements > self.get_conf("max_entries"): self.filecombo.removeItem(num_elements - 1) self.filecombo.selected() @@ -692,7 +693,7 @@ def set_data(self, filename, data): self.rdata.insert(0, (filename, data)) - while len(self.rdata) > self.get_option("max_entries"): + while len(self.rdata) > self.get_conf("max_entries"): self.rdata.pop(-1) with open(self.DATAPATH, "wb") as fh: @@ -782,7 +783,7 @@ def get_pylintrc_path(self, filename): # Working directory getcwd_or_home(), # Project directory - self.get_option("project_dir"), + self.get_conf("project_dir"), # Home directory osp.expanduser("~"), ] diff --git a/spyder/plugins/statusbar/container.py b/spyder/plugins/statusbar/container.py index 63b1840d56f..8b3e79eb70c 100644 --- a/spyder/plugins/statusbar/container.py +++ b/spyder/plugins/statusbar/container.py @@ -12,6 +12,7 @@ from qtpy.QtCore import Signal # Local imports +from spyder.api.decorators import on_conf_change from spyder.api.widgets import PluginMainContainer from spyder.plugins.statusbar.widgets.status import ( ClockStatus, CPUStatus, MemoryStatus @@ -35,7 +36,7 @@ class StatusBarContainer(PluginMainContainer): status bar. """ - def setup(self, options): + def setup(self): # Basic status widgets self.mem_status = MemoryStatus(parent=self) self.cpu_status = CPUStatus(parent=self) @@ -57,5 +58,33 @@ def on_option_update(self, option, value): elif option == 'show_status_bar': self.sig_show_status_bar_requested.emit(value) + @on_conf_change(option='memory_usage/enable') + def enable_mem_status(self, value): + self.mem_status.setVisible(value) + + @on_conf_change(option='memory_usage/timeout') + def set_mem_interval(self, value): + self.mem_status.set_interval(value) + + @on_conf_change(option='cpu_usage/enable') + def enable_cpu_status(self, value): + self.cpu_status.setVisible(value) + + @on_conf_change(option='cpu_usage/timeout') + def set_cpu_interval(self, value): + self.cpu_status.set_interval(value) + + @on_conf_change(option='clock/enable') + def enable_clock_status(self, value): + self.clock_status.setVisible(value) + + @on_conf_change(option='clock/timeout') + def set_clock_interval(self, value): + self.clock_status.set_interval(value) + + @on_conf_change(option='show_status_bar') + def show_status_bar(self, value): + self.sig_show_status_bar_requested.emit(value) + def update_actions(self): pass diff --git a/spyder/plugins/toolbar/container.py b/spyder/plugins/toolbar/container.py index b81be90b61f..06380102220 100644 --- a/spyder/plugins/toolbar/container.py +++ b/spyder/plugins/toolbar/container.py @@ -49,8 +49,8 @@ class ToolbarContainer(PluginMainContainer): 'toolbars_visible': True, } - def __init__(self, name, plugin, parent=None, options=DEFAULT_OPTIONS): - super().__init__(name, plugin, parent=parent, options=options) + def __init__(self, name, plugin, parent=None): + super().__init__(name, plugin, parent=parent) self._APPLICATION_TOOLBARS = OrderedDict() self._ADDED_TOOLBARS = OrderedDict() @@ -65,7 +65,7 @@ def _save_visible_toolbars(self): for toolbar in self._visible_toolbars: toolbars.append(toolbar.objectName()) - self.set_option('last_visible_toolbars', toolbars) + self.set_conf('last_visible_toolbars', toolbars) def _get_visible_toolbars(self): """Collect the visible toolbars.""" @@ -80,8 +80,8 @@ def _get_visible_toolbars(self): @Slot() def _show_toolbars(self): """Show/Hide toolbars.""" - value = not self.get_option("toolbars_visible") - self.set_option("toolbars_visible", value) + value = not self.get_conf("toolbars_visible") + self.set_conf("toolbars_visible", value) if value: self._save_visible_toolbars() else: @@ -114,7 +114,7 @@ def on_option_update(self, options, value): pass def update_actions(self): - if self.get_option("toolbars_visible"): + if self.get_conf("toolbars_visible"): text = _("Hide toolbars") tip = _("Hide toolbars") else: @@ -277,7 +277,7 @@ def get_application_toolbars(self): def load_last_visible_toolbars(self): """Load the last visible toolbars from our preferences..""" - toolbars_names = self.get_option('last_visible_toolbars') + toolbars_names = self.get_conf('last_visible_toolbars') if toolbars_names: toolbars_dict = {} diff --git a/spyder/plugins/workingdirectory/container.py b/spyder/plugins/workingdirectory/container.py index b4b536e6428..dd8486ab37d 100644 --- a/spyder/plugins/workingdirectory/container.py +++ b/spyder/plugins/workingdirectory/container.py @@ -17,6 +17,7 @@ from qtpy.QtCore import Signal, Slot # Local imports +from spyder.api.decorators import on_conf_change from spyder.api.translations import get_translation from spyder.api.widgets import PluginMainContainer from spyder.api.widgets.toolbars import ApplicationToolbar @@ -78,10 +79,10 @@ class WorkingDirectoryContainer(PluginMainContainer): # ---- PluginMainContainer API # ------------------------------------------------------------------------ - def setup(self, options): + def setup(self): # Variables - self.history = self.get_option('history') + self.history = self.get_conf('history') self.histindex = None # Widgets @@ -89,7 +90,7 @@ def setup(self, options): self.toolbar = WorkingDirectoryToolbar(self, title) self.pathedit = PathComboBox( self, - adjust_to_contents=self.get_option('working_dir_adjusttocontents'), + adjust_to_contents=self.get_conf('working_dir_adjusttocontents'), ) # Widget Setup @@ -103,7 +104,7 @@ def setup(self, options): "created in the editor" ) ) - self.pathedit.setMaxCount(self.get_option('working_dir_history')) + self.pathedit.setMaxCount(self.get_conf('working_dir_history')) self.pathedit.selected_text = self.pathedit.currentText() # Signals @@ -156,9 +157,9 @@ def update_actions(self): and self.histindex < len(self.history) - 1 ) - def on_option_update(self, option, value): - if option == 'history': - self.history = value + @on_conf_change(option='history') + def on_history_update(self, value): + self.history = value # --- API # ------------------------------------------------------------------------ @@ -172,12 +173,12 @@ def get_workdir(self): str: The current working directory. """ - if self.get_option('startup/use_fixed_directory'): - workdir = self.get_option('startup/fixed_directory') - elif self.get_option('console/use_project_or_home_directory'): + if self.get_conf('startup/use_fixed_directory', ''): + workdir = self.get_conf('startup/fixed_directory') + elif self.get_conf('console/use_project_or_home_directory', ''): workdir = get_home_dir() else: - workdir = self.get_option('console/fixed_directory') + workdir = self.get_conf('console/fixed_directory', '') if not osp.isdir(workdir): workdir = get_home_dir() @@ -294,13 +295,13 @@ def set_history(self, history): history: list List of string paths. """ - self.change_option('history', history) + self.set_conf('history', history) if history: self.pathedit.addItems(history) - if self.get_option('workdir') is None: + if self.get_conf('workdir') is None: workdir = self.get_workdir() else: - workdir = self.get_option('workdir') + workdir = self.get_conf('workdir') self.chdir(workdir) diff --git a/spyder/utils/qthelpers.py b/spyder/utils/qthelpers.py index d5b66b0ec46..8d36dd05348 100644 --- a/spyder/utils/qthelpers.py +++ b/spyder/utils/qthelpers.py @@ -7,12 +7,14 @@ """Qt utilities.""" # Standard library imports +import functools from math import pi import logging import os import os.path as osp import re import sys +import types # Third party imports from qtpy.compat import from_qvariant, to_qvariant @@ -299,14 +301,13 @@ def toggle_actions(actions, enable): def create_action(parent, text, shortcut=None, icon=None, tip=None, toggled=None, triggered=None, data=None, menurole=None, - context=Qt.WindowShortcut): + context=Qt.WindowShortcut, option=None, section=None): """Create a QAction""" action = SpyderAction(text, parent) if triggered is not None: action.triggered.connect(triggered) if toggled is not None: - action.toggled.connect(toggled) - action.setCheckable(True) + setup_toggled_action(action, toggled, section, option) if icon is not None: if is_text_string(icon): icon = get_icon(icon) @@ -342,6 +343,36 @@ def create_action(parent, text, shortcut=None, icon=None, tip=None, return action +def setup_toggled_action(action, toggled, section, option): + toggled = wrap_toggled(toggled, section, option) + action.toggled.connect(toggled) + action.setCheckable(True) + if section is not None and option is not None: + CONF.observe_configuration(action, section, option) + add_configuration_update(action) + + +def wrap_toggled(toggled, section, option): + """""" + if section is not None and option is not None: + @functools.wraps(toggled) + def wrapped_toggled(value): + CONF.set(section, option, value, recursive_notification=True) + toggled(value) + return wrapped_toggled + return toggled + + +def add_configuration_update(action): + """Add on_configuration_change to a SpyderAction that depends on CONF.""" + def on_configuration_change(self, _option, _section, value): + self.blockSignals(True) + self.setChecked(value) + self.blockSignals(False) + method = types.MethodType(on_configuration_change, action) + setattr(action, 'on_configuration_change', method) + + def add_shortcut_to_tooltip(action, context, name): """Add the shortcut associated with a given action to its tooltip""" if not hasattr(action, '_tooltip_backup'): diff --git a/spyder/widgets/browser.py b/spyder/widgets/browser.py index e87d37e612a..f93114203c9 100644 --- a/spyder/widgets/browser.py +++ b/spyder/widgets/browser.py @@ -89,8 +89,9 @@ class WebView(QWebEngineView, SpyderWidgetMixin): Web view. """ - def __init__(self, parent, handle_links=True): - super().__init__(parent) + def __init__(self, parent, handle_links=True, class_parent=None): + class_parent = parent if class_parent is None else class_parent + super().__init__(parent, class_parent=class_parent) self.zoom_factor = 1. self.context_menu = None @@ -492,9 +493,9 @@ class FrameWebView(QFrame): linkClicked = Signal(QUrl) def __init__(self, parent): - QFrame.__init__(self, parent) + super().__init__(parent) - self._webview = WebView(self) + self._webview = WebView(self, class_parent=parent) layout = QHBoxLayout() layout.addWidget(self._webview) @@ -509,7 +510,13 @@ def __init__(self, parent): self._webview.linkClicked.connect(self.linkClicked) def __getattr__(self, name): - return getattr(self._webview, name) + if name == '_webview': + return super().__getattr__(name) + + if hasattr(self._webview, name): + return getattr(self._webview, name) + else: + return super().__getattr__(name) @property def web_widget(self): diff --git a/spyder/widgets/onecolumntree.py b/spyder/widgets/onecolumntree.py index 2892246a490..f6bfa392440 100644 --- a/spyder/widgets/onecolumntree.py +++ b/spyder/widgets/onecolumntree.py @@ -38,7 +38,7 @@ class OneColumnTree(QTreeWidget, SpyderWidgetMixin): DEFAULT_OPTIONS = {} def __init__(self, parent): - super().__init__(parent) + super().__init__(parent, class_parent=parent) self.__expanded_state = None