Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Restore widget shortcuts to Preferences and allow to change them on the fly (Shortcuts) #23024

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelogs/Spyder-6.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### API changes

* Add `plugin_name` kwarg to the `register_shortcut_for_widget` method of
`SpyderShortcutsMixin`.
* The `add_configuration_observer` method was added to `SpyderConfigurationObserver`.
* Add `items_elide_mode` kwarg to the constructors of `SpyderComboBox` and
`SpyderComboBoxWithIcons`.
* The `sig_item_in_popup_changed` and `sig_popup_is_hidden` signals were added
Expand Down
71 changes: 61 additions & 10 deletions spyder/api/config/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

# Standard library imports
import logging
from typing import Any, Union, Optional
from typing import Any, Callable, Optional, Union
import warnings

# Local imports
Expand Down Expand Up @@ -239,8 +239,10 @@ def __init__(self):
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}')
logger.debug(
f'{self} is observing option "{option}" in section '
f'"{section}"'
)
CONF.observe_configuration(self, section, option)

def __del__(self):
Expand All @@ -257,12 +259,7 @@ def _gather_observers(self):
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
self._add_listener(method_name, option, section)

def _merge_none_observers(self):
"""Replace observers that declared section as None by CONF_SECTION."""
Expand All @@ -280,6 +277,27 @@ def _merge_none_observers(self):
self._configuration_listeners[self.CONF_SECTION] = section_selectors
self._configuration_listeners.pop(None, None)

def _add_listener(
self, func: Callable, option: ConfigurationKey, section: str
):
"""
Add a callable as listener of the option `option` on section `section`.

Parameters
----------
func: Callable
Function/method that will be called when `option` changes.
option: ConfigurationKey
Configuration option to observe.
section: str
Name of the section where `option` is contained.
"""
section_listeners = self._configuration_listeners.get(section, {})
option_listeners = section_listeners.get(option, [])
option_listeners.append(func)
section_listeners[option] = option_listeners
self._configuration_listeners[section] = section_listeners

def on_configuration_change(self, option: ConfigurationKey, section: str,
value: Any):
"""
Expand All @@ -298,8 +316,41 @@ def on_configuration_change(self, option: ConfigurationKey, section: str,
section_receivers = self._configuration_listeners.get(section, {})
option_receivers = section_receivers.get(option, [])
for receiver in option_receivers:
method = getattr(self, receiver)
method = (
receiver if callable(receiver) else getattr(self, receiver)
)
if receiver in self._multi_option_listeners:
method(option, value)
else:
method(value)

def add_configuration_observer(
self, func: Callable, option: str, section: Optional[str] = None
):
"""
Add a callable to observe the option `option` on section `section`.

Parameters
----------
func: Callable
Function that will be called when `option` changes.
option: ConfigurationKey
Configuration option to observe.
section: str
Name of the section where `option` is contained.

Notes
-----
- This is only necessary if you need to add a callable that is not a
class method to observe an option. Otherwise, you simply need to
decorate your method with
:function:`spyder.api.config.decorators.on_conf_change`.
"""
if section is None:
section = self.CONF_SECTION

logger.debug(
f'{self} is observing "{option}" option on section "{section}"'
)
self._add_listener(func, option, section)
CONF.observe_configuration(self, section, option)
44 changes: 22 additions & 22 deletions spyder/api/plugins/new_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,8 @@ class SpyderPluginV2(QObject, SpyderActionMixin, SpyderConfigurationObserver,
The window state.
"""

# --- Private attributes -------------------------------------------------
# ------------------------------------------------------------------------
# ---- Private attributes
# -------------------------------------------------------------------------
# Define configuration name map for plugin to split configuration
# among several files. See spyder/config/main.py
_CONF_NAME_MAP = None
Expand Down Expand Up @@ -361,8 +361,8 @@ def __init__(self, parent, configuration=None):
plugin_path = osp.join(self.get_path(), self.IMG_PATH)
IMAGE_PATH_MANAGER.add_image_path(plugin_path)

# --- Private methods ----------------------------------------------------
# ------------------------------------------------------------------------
# ---- Private methods
# -------------------------------------------------------------------------
def _register(self, omit_conf=False):
"""
Setup and register plugin in Spyder's main window and connect it to
Expand Down Expand Up @@ -397,8 +397,8 @@ def _unregister(self):
self.is_compatible = None
self.is_registered = False

# --- API: available methods ---------------------------------------------
# ------------------------------------------------------------------------
# ---- API: available methods
# -------------------------------------------------------------------------
def get_path(self):
"""
Return the plugin's system path.
Expand Down Expand Up @@ -765,8 +765,8 @@ def get_command_line_options(self):
sys_argv = [sys.argv[0]] # Avoid options passed to pytest
return get_options(sys_argv)[0]

# --- API: Mandatory methods to define -----------------------------------
# ------------------------------------------------------------------------
# ---- API: Mandatory methods to define
# -------------------------------------------------------------------------
@staticmethod
def get_name():
"""
Expand Down Expand Up @@ -832,8 +832,8 @@ def on_initialize(self):
f'The plugin {type(self)} is missing an implementation of '
'on_initialize')

# --- API: Optional methods to override ----------------------------------
# ------------------------------------------------------------------------
# ---- API: Optional methods to override
# -------------------------------------------------------------------------
@staticmethod
def check_compatibility():
"""
Expand Down Expand Up @@ -952,14 +952,14 @@ class SpyderDockablePlugin(SpyderPluginV2):
"""
A Spyder plugin to enhance functionality with a dockable widget.
"""
# --- API: Mandatory attributes ------------------------------------------
# ------------------------------------------------------------------------
# ---- API: Mandatory attributes
# -------------------------------------------------------------------------
# This is the main widget of the dockable plugin.
# It needs to be a subclass of PluginMainWidget.
WIDGET_CLASS = None

# --- API: Optional attributes -------------------------------------------
# ------------------------------------------------------------------------
# ---- API: Optional attributes
# -------------------------------------------------------------------------
# Define a list of plugins next to which we want to to tabify this plugin.
# Example: ['Plugins.Editor']
TABIFY = []
Expand All @@ -972,8 +972,8 @@ class SpyderDockablePlugin(SpyderPluginV2):
# the action to switch is called a second time.
RAISE_AND_FOCUS = False

# --- API: Available signals ---------------------------------------------
# ------------------------------------------------------------------------
# ---- API: Available signals
# -------------------------------------------------------------------------
sig_focus_changed = Signal()
"""
This signal is emitted to inform the focus of this plugin has changed.
Expand Down Expand Up @@ -1010,8 +1010,8 @@ class SpyderDockablePlugin(SpyderPluginV2):
needs its ancestor to be updated.
"""

# --- Private methods ----------------------------------------------------
# ------------------------------------------------------------------------
# ---- Private methods
# -------------------------------------------------------------------------
def __init__(self, parent, configuration):
if not issubclass(self.WIDGET_CLASS, PluginMainWidget):
raise SpyderAPIError(
Expand Down Expand Up @@ -1053,8 +1053,8 @@ def __init__(self, parent, configuration):
widget.sig_update_ancestor_requested.connect(
self.sig_update_ancestor_requested)

# --- API: available methods ---------------------------------------------
# ------------------------------------------------------------------------
# ---- API: available methods
# -------------------------------------------------------------------------
def before_long_process(self, message):
"""
Show a message in main window's status bar, change the mouse pointer
Expand Down Expand Up @@ -1116,8 +1116,8 @@ def set_ancestor(self, ancestor_widget):
"""
self.get_widget().set_ancestor(ancestor_widget)

# --- Convenience methods from the widget exposed on the plugin
# ------------------------------------------------------------------------
# ---- Convenience methods from the widget exposed on the plugin
# -------------------------------------------------------------------------
@property
def dockwidget(self):
return self.get_widget().dockwidget
Expand Down
Loading