Skip to content

Commit

Permalink
Merge pull request #21101 from ccordoba12/plugins-table
Browse files Browse the repository at this point in the history
PR: Improve UI of Plugins page in Preferences
  • Loading branch information
ccordoba12 authored Jul 28, 2023
2 parents 2ea17b7 + 617c15c commit 68cdfdf
Show file tree
Hide file tree
Showing 47 changed files with 882 additions and 229 deletions.
1 change: 1 addition & 0 deletions binder/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies:
- pyqtwebengine <5.16
- python-lsp-black >=1.2.0,<3.0.0
- python-lsp-server >=1.7.4,<1.8.0
- pyuca >=1.2
- pyxdg >=0.26
- pyzmq >=22.1.0
- qdarkstyle >=3.0.2,<3.2.0
Expand Down
24 changes: 21 additions & 3 deletions changelogs/Spyder-6.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# History of changes for Spyder 6

## Version 6.0alpha1 (2023-06-19)
## Version 6.0.0 (unreleased)

### New features

Expand All @@ -17,17 +17,35 @@

### Important fixes

* Environment variables declared in `~/.bashrc` or `~/.zhrc` are detected and
passed to the IPython console.
* Restore ability to load Hdf5 and Dicom files through the Variable Explorer
(this was working in Spyder 4 and before).

### UX/UI improvements

* The file switcher can open files present in the current project.
* The interface font used by the entire application can be configured in
`Preferences > Appearance`
* Files can be opened in the editor by pasting their path in the Working
Directory toolbar.

### New API features

* Generalize Run plugin to support generic inputs and executors. This allows
* `SpyderPluginV2.get_description` must be a static method now and
`SpyderPluginV2.get_icon` a class or static method. This is necessary to
display the list of available plugins in Preferences in a more user-friendly
way (see PR spyder-ide/spyder#21101).
* Generalize the Run plugin to support generic inputs and executors. This allows
plugins to declare what kind of inputs (i.e. file, cell or selection) they
can execute and how they will display the result.
* Add a new plugin for the files and symbols switcher.
* Add a new plugin called `Switcher` for the files and symbols switcher.
* Declare a proper API for the Projects plugin.

----

## Version 6.0alpha1 (2023-06-19)

### Issues Closed

* [Issue 20885](https://github.com/spyder-ide/spyder/issues/20885) - Error when opening files due to a corrupted config file ([PR 20886](https://github.com/spyder-ide/spyder/pull/20886) by [@dalthviz](https://github.com/dalthviz))
Expand Down
1 change: 1 addition & 0 deletions requirements/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies:
- pyqtwebengine <5.16
- python-lsp-black >=1.2.0,<3.0.0
- python-lsp-server >=1.7.4,<1.8.0
- pyuca >=1.2
- pyzmq >=22.1.0
- qdarkstyle >=3.0.2,<3.2.0
- qstylizer >=0.2.2
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,12 @@ def run(self):
'pygments>=2.0',
'pylint>=2.5.0,<3.0',
'pylint-venv>=3.0.2',
'python-lsp-black>=1.2.0,<3.0.0',
'pyls-spyder>=0.4.0',
'pyqt5<5.16',
'pyqtwebengine<5.16',
'python-lsp-black>=1.2.0,<3.0.0',
'python-lsp-server[all]>=1.7.4,<1.8.0',
'pyuca>=1.2',
'pyxdg>=0.26;platform_system=="Linux"',
'pyzmq>=22.1.0',
'qdarkstyle>=3.0.2,<3.2.0',
Expand Down
2 changes: 1 addition & 1 deletion spyder/api/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@
updated.
"""

VERSION_INFO = (0, 9, 1)
VERSION_INFO = (0, 10, 0)
__version__ = '.'.join(map(str, VERSION_INFO))
108 changes: 59 additions & 49 deletions spyder/api/plugin_registration/_confpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,33 @@
"""Plugin registry configuration page."""

# Third party imports
from qtpy.QtWidgets import (QGroupBox, QVBoxLayout, QCheckBox,
QGridLayout, QLabel)
from pyuca import Collator
from qtpy.QtWidgets import QVBoxLayout, QLabel

# Local imports
from spyder.api.plugins import SpyderPlugin
from spyder.api.preferences import PluginConfigPage
from spyder.config.base import _
from spyder.config.manager import CONF
from spyder.widgets.elementstable import ElementsTable


class PluginsConfigPage(PluginConfigPage):

def setup_page(self):
newcb = self.create_checkbox
self.plugins_checkboxes = {}

header_label = QLabel(
_("Here you can turn on/off any internal or external Spyder plugin "
"to disable functionality that is not desired or to have a lighter "
"experience. Unchecked plugins in this page will be unloaded "
"immediately and will not be loaded the next time Spyder starts."))
_("Disable a Spyder plugin (external or built-in) to prevent it "
"from loading until re-enabled here, to simplify the interface "
"or in case it causes problems.")
)
header_label.setWordWrap(True)

# ------------------ Internal plugin status group ---------------------
internal_layout = QGridLayout()
self.internal_plugins_group = QGroupBox(_("Internal plugins"))
# To save the plugin elements
internal_elements = []
external_elements = []

i = 0
# ------------------ Internal plugins ---------------------------------
for plugin_name in self.plugin.all_internal_plugins:
(conf_section_name,
PluginClass) = self.plugin.all_internal_plugins[plugin_name]
Expand All @@ -42,58 +42,68 @@ def setup_page(self):
# Do not list core plugins that can not be disabled
continue

plugin_loc_name = None
if hasattr(PluginClass, 'get_name'):
plugin_loc_name = PluginClass.get_name()
elif hasattr(PluginClass, 'get_plugin_title'):
plugin_loc_name = PluginClass.get_plugin_title()
plugin_state = self.get_option(
'enable', section=conf_section_name, default=True)
cb = newcb('', 'enable', default=True, section=conf_section_name,
restart=True)

internal_elements.append(
dict(
title=PluginClass.get_name(),
description=PluginClass.get_description(),
icon=PluginClass.get_icon(),
widget=cb,
additional_info=_("Built-in")
)
)

plugin_state = CONF.get(conf_section_name, 'enable', True)
cb = newcb(plugin_loc_name, 'enable', default=True,
section=conf_section_name, restart=True)
internal_layout.addWidget(cb, i // 2, i % 2)
self.plugins_checkboxes[plugin_name] = (cb, plugin_state)
i += 1

self.internal_plugins_group.setLayout(internal_layout)

# ------------------ External plugin status group ---------------------
external_layout = QGridLayout()
self.external_plugins_group = QGroupBox(_("External plugins"))

i = 0
# Temporal fix to avoid disabling external plugins.
# for more info see spyder#17464
show_external_plugins_group = False
for i, plugin_name in enumerate(self.plugin.all_external_plugins):
# ------------------ External plugins ---------------------------------
for plugin_name in self.plugin.all_external_plugins:
(conf_section_name,
PluginClass) = self.plugin.all_external_plugins[plugin_name]

if not getattr(PluginClass, 'CAN_BE_DISABLED', True):
# Do not list external plugins that can not be disabled
continue

plugin_loc_name = None
if hasattr(PluginClass, 'get_name'):
plugin_loc_name = PluginClass.get_name()
elif hasattr(PluginClass, 'get_plugin_title'):
plugin_loc_name = PluginClass.get_plugin_title()
plugin_state = self.get_option(
f'{conf_section_name}/enable',
section=self.plugin._external_plugins_conf_section,
default=True
)
cb = newcb('', f'{conf_section_name}/enable', default=True,
section=self.plugin._external_plugins_conf_section,
restart=True)

external_elements.append(
dict(
title=PluginClass.get_name(),
description=PluginClass.get_description(),
icon=PluginClass.get_icon(),
widget=cb
)
)

cb = newcb(plugin_loc_name, 'enable', default=True,
section=conf_section_name, restart=True)
external_layout.addWidget(cb, i // 2, i % 2)
plugin_state = CONF.get(conf_section_name, 'enable', True)
self.plugins_checkboxes[plugin_name] = (cb, plugin_state)
i += 1

self.external_plugins_group.setLayout(external_layout)
# Sort elements by title for easy searching
collator = Collator()
internal_elements.sort(key=lambda e: collator.sort_key(e['title']))
external_elements.sort(key=lambda e: collator.sort_key(e['title']))

# Build plugins table, showing external plugins first.
plugins_table = ElementsTable(
self, external_elements + internal_elements
)

# Layout
layout = QVBoxLayout()
layout.addWidget(header_label)
layout.addWidget(self.internal_plugins_group)
if show_external_plugins_group:
layout.addWidget(self.external_plugins_group)
layout.addStretch(1)
layout.addSpacing(15)
layout.addWidget(plugins_table)
layout.addSpacing(15)
self.setLayout(layout)

def apply_settings(self):
Expand All @@ -109,7 +119,7 @@ def apply_settings(self):
elif plugin_name in self.plugin.all_external_plugins:
(__,
PluginClass) = self.plugin.all_external_plugins[plugin_name]
external = True
external = True # noqa

# TODO: Once we can test that all plugins can be restarted
# without problems during runtime, we can enable the
Expand Down
5 changes: 4 additions & 1 deletion spyder/api/plugin_registration/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

# Standard library imports
import logging
from typing import Dict, List, Union, Type, Any, Set, Optional, Tuple
from typing import Dict, List, Union, Type, Any, Set, Optional

# Third-party library imports
from qtpy.QtCore import QObject, Signal
Expand Down Expand Up @@ -124,6 +124,9 @@ def __init__(self):
# Dictionary that contains all the external plugins (enabled or not)
self.all_external_plugins = {} # type: Dict[str, Tuple[str, Type[SpyderPluginClass]]]

# This is used to allow disabling external plugins through Preferences
self._external_plugins_conf_section = "external_plugins"

# ------------------------- PRIVATE API -----------------------------------
def _update_dependents(self, plugin: str, dependent_plugin: str, key: str):
"""Add `dependent_plugin` to the list of dependents of `plugin`."""
Expand Down
14 changes: 10 additions & 4 deletions spyder/api/plugins/new_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,11 +750,12 @@ def get_name():
Notes
-----
This is a method to be able to update localization without a restart.
This method needs to be decorated with `staticmethod`.
"""
raise NotImplementedError('A plugin name must be defined!')

def get_description(self):
@staticmethod
def get_description():
"""
Return the plugin localized description.
Expand All @@ -765,18 +766,23 @@ def get_description(self):
Notes
-----
This is a method to be able to update localization without a restart.
This method needs to be decorated with `staticmethod`.
"""
raise NotImplementedError('A plugin description must be defined!')

def get_icon(self):
@classmethod
def get_icon(cls):
"""
Return the plugin associated icon.
Returns
-------
QIcon
QIcon instance
Notes
-----
This method needs to be decorated with `classmethod` or `staticmethod`.
"""
raise NotImplementedError('A plugin icon must be defined!')

Expand Down
23 changes: 20 additions & 3 deletions spyder/app/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,30 +717,47 @@ def setup(self):
all_plugins = external_plugins.copy()
all_plugins.update(internal_plugins.copy())

# Determine 'enable' config for the plugins that have it
# Determine 'enable' config for plugins that have it.
enabled_plugins = {}
registry_internal_plugins = {}
registry_external_plugins = {}

for plugin in all_plugins.values():
plugin_name = plugin.NAME
# Disable panes that use web widgets (currently Help and Online
# Disable plugins that use web widgets (currently Help and Online
# Help) if the user asks for it.
# See spyder-ide/spyder#16518
if self._cli_options.no_web_widgets:
if "help" in plugin_name:
continue

plugin_main_attribute_name = (
self._INTERNAL_PLUGINS_MAPPING[plugin_name]
if plugin_name in self._INTERNAL_PLUGINS_MAPPING
else plugin_name)

if plugin_name in internal_plugins:
registry_internal_plugins[plugin_name] = (
plugin_main_attribute_name, plugin)
enable_option = "enable"
enable_section = plugin_main_attribute_name
else:
registry_external_plugins[plugin_name] = (
plugin_main_attribute_name, plugin)

# This is a workaround to allow disabling external plugins.
# Because of the way the current config implementation works,
# an external plugin config option (e.g. 'enable') can only be
# read after the plugin is loaded. But here we're trying to
# decide if the plugin should be loaded if it's enabled. So,
# for now we read (and save, see the config page associated to
# PLUGIN_REGISTRY) that option in our internal config options.
# See spyder-ide/spyder#17464 for more details.
enable_option = f"{plugin_main_attribute_name}/enable"
enable_section = PLUGIN_REGISTRY._external_plugins_conf_section

try:
if self.get_conf("enable", section=plugin_main_attribute_name):
if self.get_conf(enable_option, section=enable_section):
enabled_plugins[plugin_name] = plugin
PLUGIN_REGISTRY.set_plugin_enabled(plugin_name)
except (cp.NoOptionError, cp.NoSectionError):
Expand Down
Loading

0 comments on commit 68cdfdf

Please sign in to comment.