Skip to content

Commit c34f19c

Browse files
authored
Merge pull request #23024 from ccordoba12/issue-22516
PR: Restore widget shortcuts to Preferences and allow to change them on the fly (Shortcuts)
2 parents 9312dfd + 7b1a3fe commit c34f19c

File tree

32 files changed

+693
-156
lines changed

32 files changed

+693
-156
lines changed

changelogs/Spyder-6.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
### API changes
66

7+
* Add `plugin_name` kwarg to the `register_shortcut_for_widget` method of
8+
`SpyderShortcutsMixin`.
9+
* The `add_configuration_observer` method was added to `SpyderConfigurationObserver`.
710
* Add `items_elide_mode` kwarg to the constructors of `SpyderComboBox` and
811
`SpyderComboBoxWithIcons`.
912
* The `sig_item_in_popup_changed` and `sig_popup_is_hidden` signals were added

conftest.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,12 @@ def pytest_collection_modifyitems(config, items):
109109

110110

111111
@pytest.fixture(autouse=True)
112-
def reset_conf_before_test():
112+
def reset_conf_before_test(request):
113+
# To prevent running this fixture for a specific test, you need to use this
114+
# marker.
115+
if 'no_reset_conf' in request.keywords:
116+
return
117+
113118
from spyder.config.manager import CONF
114119
CONF.reset_to_defaults(notification=False)
115120

spyder/api/config/mixins.py

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
# Standard library imports
1212
import logging
13-
from typing import Any, Union, Optional
13+
from typing import Any, Callable, Optional, Union
1414
import warnings
1515

1616
# Local imports
@@ -239,8 +239,10 @@ def __init__(self):
239239
section = self.CONF_SECTION if section is None else section
240240
observed_options = self._configuration_listeners[section]
241241
for option in observed_options:
242-
logger.debug(f'{self} is observing {option} '
243-
f'in section {section}')
242+
logger.debug(
243+
f'{self} is observing option "{option}" in section '
244+
f'"{section}"'
245+
)
244246
CONF.observe_configuration(self, section, option)
245247

246248
def __del__(self):
@@ -257,12 +259,7 @@ def _gather_observers(self):
257259
self._multi_option_listeners |= {method_name}
258260

259261
for section, option in info:
260-
section_listeners = self._configuration_listeners.get(
261-
section, {})
262-
option_listeners = section_listeners.get(option, [])
263-
option_listeners.append(method_name)
264-
section_listeners[option] = option_listeners
265-
self._configuration_listeners[section] = section_listeners
262+
self._add_listener(method_name, option, section)
266263

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

280+
def _add_listener(
281+
self, func: Callable, option: ConfigurationKey, section: str
282+
):
283+
"""
284+
Add a callable as listener of the option `option` on section `section`.
285+
286+
Parameters
287+
----------
288+
func: Callable
289+
Function/method that will be called when `option` changes.
290+
option: ConfigurationKey
291+
Configuration option to observe.
292+
section: str
293+
Name of the section where `option` is contained.
294+
"""
295+
section_listeners = self._configuration_listeners.get(section, {})
296+
option_listeners = section_listeners.get(option, [])
297+
option_listeners.append(func)
298+
section_listeners[option] = option_listeners
299+
self._configuration_listeners[section] = section_listeners
300+
283301
def on_configuration_change(self, option: ConfigurationKey, section: str,
284302
value: Any):
285303
"""
@@ -298,8 +316,41 @@ def on_configuration_change(self, option: ConfigurationKey, section: str,
298316
section_receivers = self._configuration_listeners.get(section, {})
299317
option_receivers = section_receivers.get(option, [])
300318
for receiver in option_receivers:
301-
method = getattr(self, receiver)
319+
method = (
320+
receiver if callable(receiver) else getattr(self, receiver)
321+
)
302322
if receiver in self._multi_option_listeners:
303323
method(option, value)
304324
else:
305325
method(value)
326+
327+
def add_configuration_observer(
328+
self, func: Callable, option: str, section: Optional[str] = None
329+
):
330+
"""
331+
Add a callable to observe the option `option` on section `section`.
332+
333+
Parameters
334+
----------
335+
func: Callable
336+
Function that will be called when `option` changes.
337+
option: ConfigurationKey
338+
Configuration option to observe.
339+
section: str
340+
Name of the section where `option` is contained.
341+
342+
Notes
343+
-----
344+
- This is only necessary if you need to add a callable that is not a
345+
class method to observe an option. Otherwise, you simply need to
346+
decorate your method with
347+
:function:`spyder.api.config.decorators.on_conf_change`.
348+
"""
349+
if section is None:
350+
section = self.CONF_SECTION
351+
352+
logger.debug(
353+
f'{self} is observing "{option}" option on section "{section}"'
354+
)
355+
self._add_listener(func, option, section)
356+
CONF.observe_configuration(self, section, option)

spyder/api/plugins/new_api.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,8 @@ class SpyderPluginV2(QObject, SpyderActionMixin, SpyderConfigurationObserver,
298298
The window state.
299299
"""
300300

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

364-
# --- Private methods ----------------------------------------------------
365-
# ------------------------------------------------------------------------
364+
# ---- Private methods
365+
# -------------------------------------------------------------------------
366366
def _register(self, omit_conf=False):
367367
"""
368368
Setup and register plugin in Spyder's main window and connect it to
@@ -397,8 +397,8 @@ def _unregister(self):
397397
self.is_compatible = None
398398
self.is_registered = False
399399

400-
# --- API: available methods ---------------------------------------------
401-
# ------------------------------------------------------------------------
400+
# ---- API: available methods
401+
# -------------------------------------------------------------------------
402402
def get_path(self):
403403
"""
404404
Return the plugin's system path.
@@ -765,8 +765,8 @@ def get_command_line_options(self):
765765
sys_argv = [sys.argv[0]] # Avoid options passed to pytest
766766
return get_options(sys_argv)[0]
767767

768-
# --- API: Mandatory methods to define -----------------------------------
769-
# ------------------------------------------------------------------------
768+
# ---- API: Mandatory methods to define
769+
# -------------------------------------------------------------------------
770770
@staticmethod
771771
def get_name():
772772
"""
@@ -832,8 +832,8 @@ def on_initialize(self):
832832
f'The plugin {type(self)} is missing an implementation of '
833833
'on_initialize')
834834

835-
# --- API: Optional methods to override ----------------------------------
836-
# ------------------------------------------------------------------------
835+
# ---- API: Optional methods to override
836+
# -------------------------------------------------------------------------
837837
@staticmethod
838838
def check_compatibility():
839839
"""
@@ -952,14 +952,14 @@ class SpyderDockablePlugin(SpyderPluginV2):
952952
"""
953953
A Spyder plugin to enhance functionality with a dockable widget.
954954
"""
955-
# --- API: Mandatory attributes ------------------------------------------
956-
# ------------------------------------------------------------------------
955+
# ---- API: Mandatory attributes
956+
# -------------------------------------------------------------------------
957957
# This is the main widget of the dockable plugin.
958958
# It needs to be a subclass of PluginMainWidget.
959959
WIDGET_CLASS = None
960960

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

975-
# --- API: Available signals ---------------------------------------------
976-
# ------------------------------------------------------------------------
975+
# ---- API: Available signals
976+
# -------------------------------------------------------------------------
977977
sig_focus_changed = Signal()
978978
"""
979979
This signal is emitted to inform the focus of this plugin has changed.
@@ -1010,8 +1010,8 @@ class SpyderDockablePlugin(SpyderPluginV2):
10101010
needs its ancestor to be updated.
10111011
"""
10121012

1013-
# --- Private methods ----------------------------------------------------
1014-
# ------------------------------------------------------------------------
1013+
# ---- Private methods
1014+
# -------------------------------------------------------------------------
10151015
def __init__(self, parent, configuration):
10161016
if not issubclass(self.WIDGET_CLASS, PluginMainWidget):
10171017
raise SpyderAPIError(
@@ -1053,8 +1053,8 @@ def __init__(self, parent, configuration):
10531053
widget.sig_update_ancestor_requested.connect(
10541054
self.sig_update_ancestor_requested)
10551055

1056-
# --- API: available methods ---------------------------------------------
1057-
# ------------------------------------------------------------------------
1056+
# ---- API: available methods
1057+
# -------------------------------------------------------------------------
10581058
def before_long_process(self, message):
10591059
"""
10601060
Show a message in main window's status bar, change the mouse pointer
@@ -1116,8 +1116,8 @@ def set_ancestor(self, ancestor_widget):
11161116
"""
11171117
self.get_widget().set_ancestor(ancestor_widget)
11181118

1119-
# --- Convenience methods from the widget exposed on the plugin
1120-
# ------------------------------------------------------------------------
1119+
# ---- Convenience methods from the widget exposed on the plugin
1120+
# -------------------------------------------------------------------------
11211121
@property
11221122
def dockwidget(self):
11231123
return self.get_widget().dockwidget

0 commit comments

Comments
 (0)