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

Merged
merged 19 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bcbf1e2
API: Improve docstrings of SpyderShortcutsMixin
ccordoba12 Nov 15, 2024
474c185
Shortcuts: Use a dataclass to represent shortcut data
ccordoba12 Nov 15, 2024
dd9d7e8
Shortcuts: Restore showing shortcuts for widgets in Preferences
ccordoba12 Nov 15, 2024
4b73b75
API: Allow to add callables to observe an option in our config system
ccordoba12 Nov 19, 2024
c308cb9
Config: Notify when a shortcut is changed in our config system
ccordoba12 Nov 20, 2024
011bd0e
API: Add an observer for each shortcut registered for a widget
ccordoba12 Nov 20, 2024
9a54ef2
Testing: Check that shortcuts in CodeEditor are updated on the fly
ccordoba12 Nov 20, 2024
76a2d3b
Testing: Remove setting CONF_SECTION for EditorStack/EditorSplitter
ccordoba12 Nov 20, 2024
a34a26c
Testing: Fix tests that use keyboard shortcuts
ccordoba12 Nov 20, 2024
bba018e
API: Add plugin_name kwarg to register_shortcut_for_widget
ccordoba12 Nov 21, 2024
b8d84e8
Config: Update widget shortcuts on the fly after resetting all shortcuts
ccordoba12 Nov 25, 2024
b96896b
Shortcuts: Fix saving shortcuts that use plugin_name from Preferences
ccordoba12 Nov 25, 2024
77a5024
Switcher: Fix shortcut context of its actions to be global
ccordoba12 Nov 25, 2024
a2b03a9
API: Fix several block comments so they're displayed in Spyder's Outline
ccordoba12 Nov 25, 2024
819e5ab
Testing: Check that resetting shortcuts restore widget ones on the fly
ccordoba12 Nov 26, 2024
d68bfcf
Shortcuts: Fix getting/saving shortcuts with capitalized names
ccordoba12 Nov 29, 2024
47ee224
Testing: Check registering widget shortcuts for external plugins
ccordoba12 Nov 29, 2024
dbb5fb8
Config: Validate shortcut options when setting defaults
ccordoba12 Nov 30, 2024
7b1a3fe
Testing: Check that an error is raised for invalid shortcut options
ccordoba12 Nov 30, 2024
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
7 changes: 6 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,12 @@ def pytest_collection_modifyitems(config, items):


@pytest.fixture(autouse=True)
def reset_conf_before_test():
def reset_conf_before_test(request):
# To prevent running this fixture for a specific test, you need to use this
# marker.
if 'no_reset_conf' in request.keywords:
return

from spyder.config.manager import CONF
CONF.reset_to_defaults(notification=False)

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
Loading