From a8ed626a527d7613f17b2d672e3b4db144bd778f Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 28 Jul 2023 10:41:31 -0500 Subject: [PATCH 01/34] Preferences: Add a max width to all pages That way we avoid them to take the entire width of the config dialog. --- spyder/plugins/preferences/api.py | 23 +++++++--- .../preferences/widgets/configdialog.py | 46 ++++++++++++++----- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/spyder/plugins/preferences/api.py b/spyder/plugins/preferences/api.py index f0626125476..11dfe5d6938 100644 --- a/spyder/plugins/preferences/api.py +++ b/spyder/plugins/preferences/api.py @@ -17,12 +17,12 @@ from qtpy.compat import (getexistingdirectory, getopenfilename, from_qvariant, to_qvariant) from qtpy.QtCore import Qt, Signal, Slot, QRegExp, QSize -from qtpy.QtGui import QColor, QRegExpValidator, QTextOption, QPixmap +from qtpy.QtGui import QColor, QRegExpValidator, QTextOption from qtpy.QtWidgets import (QButtonGroup, QCheckBox, QComboBox, QDoubleSpinBox, QFileDialog, QFontComboBox, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPlainTextEdit, QPushButton, QRadioButton, - QSpinBox, QTabWidget, QVBoxLayout, QWidget, QSizePolicy) + QSpinBox, QTabWidget, QVBoxLayout, QWidget) # Local imports from spyder.config.base import _ @@ -30,7 +30,6 @@ from spyder.config.user import NoDefault from spyder.py3compat import to_text_string from spyder.utils.icon_manager import ima -from spyder.utils.image_path_manager import get_image_path from spyder.utils.misc import getcwd_or_home from spyder.widgets.colors import ColorLayout from spyder.widgets.comboboxes import FileComboBox @@ -144,11 +143,16 @@ def save_to_conf(self): class SpyderConfigPage(ConfigPage, ConfigAccessMixin): """Plugin configuration dialog box page widget""" CONF_SECTION = None + MAX_WIDTH = 620 def __init__(self, parent): - ConfigPage.__init__(self, parent, - apply_callback=lambda: - self._apply_settings_tabs(self.changed_options)) + ConfigPage.__init__( + self, + parent, + apply_callback=lambda: self._apply_settings_tabs( + self.changed_options) + ) + self.checkboxes = {} self.radiobuttons = {} self.lineedits = {} @@ -166,6 +170,13 @@ def __init__(self, parent): self.main = parent.main self.tabs = None + # Maximum width + self.setMaximumWidth(self.MAX_WIDTH) + + def sizeHint(self): + """Default page size.""" + return QSize(self.MAX_WIDTH, 400) + def _apply_settings_tabs(self, options): if self.tabs is not None: for i in range(self.tabs.count()): diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index c2d8e2db660..1422d72896e 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -10,7 +10,7 @@ from qtpy.QtWidgets import (QDialog, QDialogButtonBox, QHBoxLayout, QListView, QListWidget, QListWidgetItem, QPushButton, QScrollArea, QSplitter, - QStackedWidget, QVBoxLayout) + QStackedWidget, QVBoxLayout, QWidget) # Local imports from spyder.config.base import _, load_lang_conf @@ -18,8 +18,16 @@ from spyder.utils.icon_manager import ima +class PageScrollArea(QScrollArea): + """Scroll area for preference pages.""" + + def widget(self): + """Return the page widget inside the scroll area.""" + return super().widget().page + + class ConfigDialog(QDialog): - """Spyder configuration ('Preferences') dialog box""" + """Preferences dialog.""" # Signals check_settings = Signal() @@ -146,22 +154,36 @@ def current_page_changed(self, index): self.apply_btn.setVisible(widget.apply_callback is not None) self.apply_btn.setEnabled(widget.is_modified) - def add_page(self, widget): - self.check_settings.connect(widget.check_settings) - widget.show_this_page.connect(lambda row=self.contents_widget.count(): - self.contents_widget.setCurrentRow(row)) - widget.apply_button_enabled.connect(self.apply_btn.setEnabled) - scrollarea = QScrollArea(self) + def add_page(self, page): + # Signals + self.check_settings.connect(page.check_settings) + page.show_this_page.connect(lambda row=self.contents_widget.count(): + self.contents_widget.setCurrentRow(row)) + page.apply_button_enabled.connect(self.apply_btn.setEnabled) + + # Container widget so that we can center the page + container = QWidget(self) + layout = QHBoxLayout() + layout.addWidget(page) + layout.setAlignment(Qt.AlignHCenter) + container.setLayout(layout) + container.page = page + + # Add container to a scroll area in case the page contents don't fit + # in the dialog + scrollarea = PageScrollArea(self) scrollarea.setWidgetResizable(True) - scrollarea.setWidget(widget) + scrollarea.setWidget(container) self.pages_widget.addWidget(scrollarea) + + # Add entry to the list widget on the left item = QListWidgetItem(self.contents_widget) try: - item.setIcon(widget.get_icon()) + item.setIcon(page.get_icon()) except TypeError: pass - item.setText(widget.get_name()) - item.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) + item.setText(page.get_name()) + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) item.setSizeHint(QSize(0, 25)) def check_all_settings(self): From b4ab2941506b9ab4d0c7a7a5498d564d3e627281 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 29 Jul 2023 09:30:53 -0500 Subject: [PATCH 02/34] Preferences: Fix layout of Appearance page after last changes --- spyder/plugins/appearance/confpage.py | 13 ++++++------- spyder/plugins/preferences/api.py | 4 +--- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/spyder/plugins/appearance/confpage.py b/spyder/plugins/appearance/confpage.py index 4c3943ddf99..e3ba9918526 100644 --- a/spyder/plugins/appearance/confpage.py +++ b/spyder/plugins/appearance/confpage.py @@ -20,6 +20,7 @@ from spyder.plugins.appearance.widgets import SchemeEditor from spyder.utils import syntaxhighlighters from spyder.utils.palette import QStylePalette +from spyder.utils.stylesheet import AppStyle from spyder.widgets.simplecodeeditor import SimpleCodeEditor @@ -77,6 +78,7 @@ def setup_page(self): self.reset_button = QPushButton(_("Reset to defaults")) self.preview_editor = SimpleCodeEditor(self) + self.preview_editor.setMinimumWidth(210) self.stacked_widget = QStackedWidget(self) self.scheme_editor_dialog = SchemeEditor(parent=self, stack=self.stacked_widget) @@ -132,12 +134,10 @@ def setup_page(self): fonts_grid_layout = QGridLayout() fonts_grid_layout.addWidget(self.plain_text_font.fontlabel, 0, 0) fonts_grid_layout.addWidget(self.plain_text_font.fontbox, 0, 1) - fonts_grid_layout.addWidget(self.plain_text_font.sizelabel, 0, 2) - fonts_grid_layout.addWidget(self.plain_text_font.sizebox, 0, 3) + fonts_grid_layout.addWidget(self.plain_text_font.sizebox, 0, 2) fonts_grid_layout.addWidget(self.app_font.fontlabel, 2, 0) fonts_grid_layout.addWidget(self.app_font.fontbox, 2, 1) - fonts_grid_layout.addWidget(self.app_font.sizelabel, 2, 2) - fonts_grid_layout.addWidget(self.app_font.sizebox, 2, 3) + fonts_grid_layout.addWidget(self.app_font.sizebox, 2, 2) fonts_grid_layout.setRowStretch(fonts_grid_layout.rowCount(), 1) fonts_layout = QVBoxLayout() @@ -161,8 +161,7 @@ def setup_page(self): # Combined layout combined_layout = QGridLayout() - combined_layout.setRowStretch(0, 1) - combined_layout.setColumnStretch(1, 100) + combined_layout.setHorizontalSpacing(AppStyle.MarginSize * 5) combined_layout.addLayout(options_layout, 0, 0) combined_layout.addWidget(preview_group, 0, 1) @@ -349,7 +348,7 @@ def update_preview(self, index=None, scheme_name=None): def update_app_font_group(self, state): """Update app font group enabled state.""" - subwidgets = ['fontlabel', 'sizelabel', 'fontbox', 'sizebox'] + subwidgets = ['fontlabel', 'fontbox', 'sizebox'] if state: for widget in subwidgets: diff --git a/spyder/plugins/preferences/api.py b/spyder/plugins/preferences/api.py index 11dfe5d6938..a6a24b78c6d 100644 --- a/spyder/plugins/preferences/api.py +++ b/spyder/plugins/preferences/api.py @@ -849,7 +849,6 @@ def create_fontgroup(self, option=None, text=None, title=None, if fontfilters is not None: fontbox.setFontFilters(fontfilters) - sizelabel = QLabel(" " + _("Size")) sizebox = QSpinBox() sizebox.setRange(7, 100) sizebox.restart_required = restart @@ -858,7 +857,7 @@ def create_fontgroup(self, option=None, text=None, title=None, self.fontboxes[(fontbox, sizebox)] = option layout = QHBoxLayout() - for subwidget in (fontlabel, fontbox, sizelabel, sizebox): + for subwidget in (fontlabel, fontbox, sizebox): layout.addWidget(subwidget) layout.addStretch(1) @@ -876,7 +875,6 @@ def create_fontgroup(self, option=None, text=None, title=None, else: widget = QWidget(self) widget.fontlabel = fontlabel - widget.sizelabel = sizelabel widget.fontbox = fontbox widget.sizebox = sizebox widget.setLayout(layout) From db007c2d867a43add28201ccce8705b63c1dca75 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 31 Jul 2023 17:39:29 -0500 Subject: [PATCH 03/34] Preferences: Replace splitter for a grid layout in config dialog --- .../preferences/widgets/configdialog.py | 60 +++++++++++++------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index 1422d72896e..d107444db0f 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -7,15 +7,16 @@ # Third party imports import qstylizer.style from qtpy.QtCore import QSize, Qt, Signal, Slot -from qtpy.QtWidgets import (QDialog, QDialogButtonBox, QHBoxLayout, - QListView, QListWidget, QListWidgetItem, - QPushButton, QScrollArea, QSplitter, - QStackedWidget, QVBoxLayout, QWidget) +from qtpy.QtWidgets import ( + QDialog, QDialogButtonBox, QGridLayout, QHBoxLayout, QListView, + QListWidget, QListWidgetItem, QPushButton, QScrollArea, QStackedWidget, + QVBoxLayout, QWidget) # Local imports from spyder.config.base import _, load_lang_conf from spyder.config.manager import CONF from spyder.utils.icon_manager import ima +from spyder.utils.palette import QStylePalette class PageScrollArea(QScrollArea): @@ -40,9 +41,8 @@ def __init__(self, parent=None): self.main = parent # Widgets - self.pages_widget = QStackedWidget() - self.pages_widget.setMinimumWidth(600) - self.contents_widget = QListWidget() + self.pages_widget = QStackedWidget(self) + self.contents_widget = QListWidget(self) self.button_reset = QPushButton(_('Reset to defaults')) bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Apply | @@ -50,7 +50,6 @@ def __init__(self, parent=None): self.apply_btn = bbox.button(QDialogButtonBox.Apply) self.ok_btn = bbox.button(QDialogButtonBox.Ok) - # Widgets setup # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to @@ -58,29 +57,35 @@ def __init__(self, parent=None): self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle(_('Preferences')) self.setWindowIcon(ima.icon('configure')) + + # Widgets setup + self.pages_widget.setMinimumWidth(600) + self.contents_widget.setMovement(QListView.Static) self.contents_widget.setSpacing(1) self.contents_widget.setCurrentRow(0) self.contents_widget.setMinimumWidth(220) - self.contents_widget.setMinimumHeight(400) + self.contents_widget.setObjectName('configdialog-contents') # Layout - hsplitter = QSplitter() - hsplitter.addWidget(self.contents_widget) - hsplitter.addWidget(self.pages_widget) - hsplitter.setStretchFactor(0, 1) - hsplitter.setStretchFactor(1, 2) + contents_and_pages_layout = QGridLayout() + contents_and_pages_layout.addWidget(self.contents_widget, 0, 0) + contents_and_pages_layout.addWidget(self.pages_widget, 0, 1) + contents_and_pages_layout.setContentsMargins(0, 0, 0, 0) + contents_and_pages_layout.setColumnStretch(0, 1) + contents_and_pages_layout.setColumnStretch(1, 3) + contents_and_pages_layout.setHorizontalSpacing(0) btnlayout = QHBoxLayout() btnlayout.addWidget(self.button_reset) btnlayout.addStretch(1) btnlayout.addWidget(bbox) - vlayout = QVBoxLayout() - vlayout.addWidget(hsplitter) - vlayout.addLayout(btnlayout) + layout = QVBoxLayout() + layout.addLayout(contents_and_pages_layout) + layout.addLayout(btnlayout) - self.setLayout(vlayout) + self.setLayout(layout) # Stylesheet self.setStyleSheet(self._stylesheet) @@ -172,6 +177,7 @@ def add_page(self, page): # Add container to a scroll area in case the page contents don't fit # in the dialog scrollarea = PageScrollArea(self) + scrollarea.setObjectName('configdialog-scrollarea') scrollarea.setWidgetResizable(True) scrollarea.setWidget(container) self.pages_widget.addWidget(scrollarea) @@ -208,4 +214,22 @@ def _stylesheet(self): alignment='left' ) + # Remove right border radius of contents area + css['QListView#configdialog-contents'].setValues( + borderTopRightRadius='0px', + borderBottomRightRadius='0px', + ) + + # Remove border color on focus of contents area + css['QListView#configdialog-contents:focus'].setValues( + border=f'1px solid {QStylePalette.COLOR_BACKGROUND_4}', + ) + + # Remove left border and border radius of all page scroll areas + css['QScrollArea#configdialog-scrollarea'].setValues( + borderLeft='0px', + borderTopLeftRadius='0px', + borderBottomLeftRadius='0px', + ) + return css.toString() From e38f331010d1bd849815e8063fb949c156916ba4 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 31 Jul 2023 18:53:27 -0500 Subject: [PATCH 04/34] Fix setting and restoring Preferences dialog size Also, remove dead code related to that --- spyder/app/mainwindow.py | 7 +---- spyder/config/main.py | 13 +++++++-- spyder/plugins/completion/tests/conftest.py | 3 -- spyder/plugins/layout/plugin.py | 29 +++++-------------- spyder/plugins/preferences/plugin.py | 11 +++---- spyder/plugins/preferences/tests/conftest.py | 8 ----- .../preferences/widgets/configdialog.py | 4 +-- .../plugins/preferences/widgets/container.py | 29 +++++++++++++------ 8 files changed, 44 insertions(+), 60 deletions(-) diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py index c522dd4ffe7..4d733cdafe7 100644 --- a/spyder/app/mainwindow.py +++ b/spyder/app/mainwindow.py @@ -223,7 +223,6 @@ def signal_handler(signum, frame=None): self.thirdparty_plugins = [] # Preferences - self.prefs_dialog_size = None self.prefs_dialog_instance = None # Actions @@ -1301,11 +1300,7 @@ def apply_panes_settings(self): @Slot() def show_preferences(self): """Edit Spyder preferences.""" - self.preferences.open_dialog(self.prefs_dialog_size) - - def set_prefs_size(self, size): - """Save preferences dialog size.""" - self.prefs_dialog_size = size + self.preferences.open_dialog() # ---- Open files server # ------------------------------------------------------------------------- diff --git a/spyder/config/main.py b/spyder/config/main.py index 2f9c49e390d..f8153d32503 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -74,7 +74,6 @@ 'window/position': (10, 10), 'window/is_maximized': True, 'window/is_fullscreen': False, - 'window/prefs_dialog_size': (1050, 530), 'use_custom_margin': True, 'custom_margin': 0, 'use_custom_cursor_blinking': False, @@ -292,6 +291,11 @@ 'follow_cursor': True, 'display_variables': False }), + ('preferences', + { + 'enable': True, + 'dialog_size': (980, 715), + }), ('project_explorer', { 'name_filters': NAME_FILTERS, @@ -567,7 +571,6 @@ 'current_version', 'historylog_filename', 'window/position', - 'window/prefs_dialog_size', 'window/size', 'window/state', ] @@ -611,6 +614,10 @@ 'scrollbar_position', ], ), + ('preferences', [ + 'dialog_size', + ], + ), ('project_explorer', [ 'current_project_path', 'expanded_state', @@ -653,4 +660,4 @@ # or if you want to *rename* options, then you need to do a MAJOR update in # version, e.g. from 3.0.0 to 4.0.0 # 3. You don't need to touch this value if you're just adding a new option -CONF_VERSION = '79.0.0' +CONF_VERSION = '80.0.0' diff --git a/spyder/plugins/completion/tests/conftest.py b/spyder/plugins/completion/tests/conftest.py index c617e6d16b0..30936d70460 100644 --- a/spyder/plugins/completion/tests/conftest.py +++ b/spyder/plugins/completion/tests/conftest.py @@ -61,9 +61,6 @@ def get_plugin(self, plugin_name, error=True): if plugin_name in PLUGIN_REGISTRY: return PLUGIN_REGISTRY.get_plugin(plugin_name) - def set_prefs_size(self, size): - pass - @pytest.fixture(scope="module") def qtbot_module(qapp, request): diff --git a/spyder/plugins/layout/plugin.py b/spyder/plugins/layout/plugin.py index ca0423563be..a12c56af2b4 100644 --- a/spyder/plugins/layout/plugin.py +++ b/spyder/plugins/layout/plugin.py @@ -416,9 +416,9 @@ def quick_layout_switch(self, index_or_layout_id): container = self.get_container() try: settings = self.load_window_settings( - 'layout_{}/'.format(index_or_layout_id), section=section) - (hexstate, window_size, prefs_dialog_size, pos, is_maximized, - is_fullscreen) = settings + 'layout_{}/'.format(index_or_layout_id), section=section + ) + hexstate, window_size, pos, is_maximized, is_fullscreen = settings # The defaults layouts will always be regenerated unless there was # an overwrite, either by rewriting with same name, or by deleting @@ -474,8 +474,6 @@ def load_window_settings(self, prefix, default=False, section='main'): """ get_func = self.get_conf_default if default else self.get_conf window_size = get_func(prefix + 'size', section=section) - prefs_dialog_size = get_func( - prefix + 'prefs_dialog_size', section=section) if default: hexstate = None @@ -499,8 +497,7 @@ def load_window_settings(self, prefix, default=False, section='main'): is_maximized = get_func(prefix + 'is_maximized', section=section) is_fullscreen = get_func(prefix + 'is_fullscreen', section=section) - return (hexstate, window_size, prefs_dialog_size, pos, is_maximized, - is_fullscreen) + return (hexstate, window_size, pos, is_maximized, is_fullscreen) def get_window_settings(self): """ @@ -518,25 +515,19 @@ def get_window_settings(self): is_maximized = self.main.isMaximized() pos = (self.window_position.x(), self.window_position.y()) - prefs_dialog_size = (self.prefs_dialog_size.width(), - self.prefs_dialog_size.height()) hexstate = qbytearray_to_str( self.main.saveState(version=WINDOW_STATE_VERSION) ) - return (hexstate, window_size, prefs_dialog_size, pos, is_maximized, - is_fullscreen) + return (hexstate, window_size, pos, is_maximized, is_fullscreen) - def set_window_settings(self, hexstate, window_size, prefs_dialog_size, - pos, is_maximized, is_fullscreen): + def set_window_settings(self, hexstate, window_size, pos, is_maximized, + is_fullscreen): """ Set window settings Symetric to the 'get_window_settings' accessor. """ main = self.main main.setUpdatesEnabled(False) - self.prefs_dialog_size = QSize(prefs_dialog_size[0], - prefs_dialog_size[1]) # width,height - main.set_prefs_size(self.prefs_dialog_size) self.window_size = QSize(window_size[0], window_size[1]) # width, height self.window_position = QPoint(pos[0], pos[1]) # x,y @@ -585,18 +576,12 @@ def save_current_window_settings(self, prefix, section='main', # Fixes spyder-ide/spyder#13882 win_size = self.main.size() pos = self.main.pos() - prefs_size = self.prefs_dialog_size self.set_conf( prefix + 'size', (win_size.width(), win_size.height()), section=section, ) - self.set_conf( - prefix + 'prefs_dialog_size', - (prefs_size.width(), prefs_size.height()), - section=section, - ) self.set_conf( prefix + 'is_maximized', self.main.isMaximized(), diff --git a/spyder/plugins/preferences/plugin.py b/spyder/plugins/preferences/plugin.py index f6f4e8c32f0..c1dcf425d12 100644 --- a/spyder/plugins/preferences/plugin.py +++ b/spyder/plugins/preferences/plugin.py @@ -255,11 +255,11 @@ def merge_configurations( f'by {new_value}') return current_value - def open_dialog(self, prefs_dialog_size): + def open_dialog(self): container = self.get_container() container.create_dialog( - self.config_pages, self.config_tabs, prefs_dialog_size, - self.get_main()) + self.config_pages, self.config_tabs, self.get_main() + ) # ---------------- Public Spyder API required methods --------------------- @staticmethod @@ -276,10 +276,7 @@ def get_icon(cls) -> QIcon: def on_initialize(self): container = self.get_container() - main = self.get_main() - - container.sig_show_preferences_requested.connect( - lambda: self.open_dialog(main.prefs_dialog_size)) + container.sig_show_preferences_requested.connect(self.open_dialog) container.sig_reset_preferences_requested.connect(self.reset) @on_plugin_available(plugin=Plugins.MainMenu) diff --git a/spyder/plugins/preferences/tests/conftest.py b/spyder/plugins/preferences/tests/conftest.py index bc140e604f9..01832d75ab5 100644 --- a/spyder/plugins/preferences/tests/conftest.py +++ b/spyder/plugins/preferences/tests/conftest.py @@ -69,9 +69,6 @@ def get_plugin(self, plugin_name, error=True): if plugin_name in PLUGIN_REGISTRY: return PLUGIN_REGISTRY.get_plugin(plugin_name) - def set_prefs_size(self, size): - pass - class ConfigDialogTester(QWidget): def __init__(self, parent, main_class, @@ -81,9 +78,6 @@ def __init__(self, parent, main_class, if self._main is None: self._main = MainWindowMock(self) - def set_prefs_size(self, size): - pass - def register_plugin(self, plugin_name, external=False): plugin = PLUGIN_REGISTRY.get_plugin(plugin_name) plugin._register() @@ -97,8 +91,6 @@ def get_plugin(self, plugin_name, error=True): types.MethodType(register_plugin, self._main)) setattr(self._main, 'get_plugin', types.MethodType(get_plugin, self._main)) - setattr(self._main, 'set_prefs_size', - types.MethodType(set_prefs_size, self._main)) PLUGIN_REGISTRY.reset() PLUGIN_REGISTRY.sig_plugin_ready.connect(self._main.register_plugin) diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index d107444db0f..8631e4ede22 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -32,7 +32,7 @@ class ConfigDialog(QDialog): # Signals check_settings = Signal() - size_change = Signal(QSize) + sig_size_changed = Signal(QSize) sig_reset_preferences_requested = Signal() def __init__(self, parent=None): @@ -203,7 +203,7 @@ def resizeEvent(self, event): main application """ QDialog.resizeEvent(self, event) - self.size_change.emit(self.size()) + self.sig_size_changed.emit(self.size()) @property def _stylesheet(self): diff --git a/spyder/plugins/preferences/widgets/container.py b/spyder/plugins/preferences/widgets/container.py index ded205f17b2..022d4400651 100644 --- a/spyder/plugins/preferences/widgets/container.py +++ b/spyder/plugins/preferences/widgets/container.py @@ -30,10 +30,10 @@ class PreferencesContainer(PluginMainContainer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.dialog = None - self.dialog_index = None + self.dialog_index = 0 + self._dialog_size = None - def create_dialog(self, config_pages, config_tabs, prefs_dialog_size, - main_window): + def create_dialog(self, config_pages, config_tabs, main_window): def _dialog_finished(result_code): """Restore preferences dialog instance variable.""" @@ -41,6 +41,7 @@ def _dialog_finished(result_code): self.dialog.disconnect(None, None, None) else: self.dialog.disconnect() + self.dialog = None if self.dialog is None: @@ -48,8 +49,9 @@ def _dialog_finished(result_code): dlg = ConfigDialog(main_window) self.dialog = dlg - if prefs_dialog_size is not None: - dlg.resize(prefs_dialog_size) + if self._dialog_size is None: + self._dialog_size = self.get_conf('dialog_size') + dlg.resize(*self._dialog_size) for page_name in config_pages: (api, ConfigPage, plugin) = config_pages[page_name] @@ -65,28 +67,32 @@ def _dialog_finished(result_code): page.add_tab(Tab) dlg.add_page(page) - if self.dialog_index is not None: - dlg.set_current_index(self.dialog_index) - + dlg.set_current_index(self.dialog_index) dlg.show() dlg.check_all_settings() dlg.finished.connect(_dialog_finished) dlg.pages_widget.currentChanged.connect( self.__preference_page_changed) - dlg.size_change.connect(main_window.set_prefs_size) + dlg.sig_size_changed.connect(self._set_dialog_size) dlg.sig_reset_preferences_requested.connect( self.sig_reset_preferences_requested) else: + self.dialog.resize(*self._dialog_size) self.dialog.show() self.dialog.activateWindow() self.dialog.raise_() self.dialog.setFocus() + # ---- Private API def __preference_page_changed(self, index): """Preference page index has changed.""" self.dialog_index = index + def _set_dialog_size(self, size): + self._dialog_size = (size.width(), size.height()) + + # ---- Public API def is_preferences_open(self): """Check if preferences is open.""" return self.dialog is not None and self.dialog.isVisible() @@ -119,3 +125,8 @@ def setup(self): def update_actions(self): pass + + def on_close(self): + # Save dialog size to use it in the next Spyder session + if isinstance(self._dialog_size, tuple): + self.set_conf('dialog_size', self._dialog_size) From 088b3b9bde36917b2a9e3a70904e9ecbcd338143 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 2 Aug 2023 08:42:21 -0500 Subject: [PATCH 05/34] Stylesheet: Change QGroupBox style in the application --- spyder/utils/stylesheet.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spyder/utils/stylesheet.py b/spyder/utils/stylesheet.py index ddb1d280a6d..48bb79b498b 100644 --- a/spyder/utils/stylesheet.py +++ b/spyder/utils/stylesheet.py @@ -249,6 +249,19 @@ def _customize_stylesheet(self): minHeight=f'{combobox_min_height - 0.2}em' ) + # Change QGroupBox style to avoid the "boxes within boxes" antipattern + # in Preferences + css.QGroupBox.setValues( + border='0px', + marginBottom='15px', + fontSize=f'{font_size + 1}pt', + ) + + css['QGroupBox::title'].setValues( + paddingTop='-0.3em', + left='0px', + ) + APP_STYLESHEET = AppStylesheet() From 92b6717fd22dc42f790998d513ad2db4e7864c5a Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 2 Aug 2023 12:42:01 -0500 Subject: [PATCH 06/34] Preferences: Add a modern style for its dialog - Add background color to the entries area to visually distinguish it from the options one. - Increase entries font size in one point. - Remove border from and add padding to options area. --- spyder/config/main.py | 2 +- .../preferences/widgets/configdialog.py | 64 ++++++++++++++----- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/spyder/config/main.py b/spyder/config/main.py index f8153d32503..e89d750e51e 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -294,7 +294,7 @@ ('preferences', { 'enable': True, - 'dialog_size': (980, 715), + 'dialog_size': (990, 715), }), ('project_explorer', { diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index 8631e4ede22..f8b11c7310a 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -13,6 +13,7 @@ QVBoxLayout, QWidget) # Local imports +from spyder.api.config.fonts import SpyderFontType, SpyderFontsMixin from spyder.config.base import _, load_lang_conf from spyder.config.manager import CONF from spyder.utils.icon_manager import ima @@ -27,7 +28,7 @@ def widget(self): return super().widget().page -class ConfigDialog(QDialog): +class ConfigDialog(QDialog, SpyderFontsMixin): """Preferences dialog.""" # Signals @@ -62,9 +63,9 @@ def __init__(self, parent=None): self.pages_widget.setMinimumWidth(600) self.contents_widget.setMovement(QListView.Static) - self.contents_widget.setSpacing(1) + self.contents_widget.setSpacing(3) self.contents_widget.setCurrentRow(0) - self.contents_widget.setMinimumWidth(220) + self.contents_widget.setMinimumWidth(300) self.contents_widget.setObjectName('configdialog-contents') # Layout @@ -83,6 +84,7 @@ def __init__(self, parent=None): layout = QVBoxLayout() layout.addLayout(contents_and_pages_layout) + layout.addSpacing(3) layout.addLayout(btnlayout) self.setLayout(layout) @@ -167,10 +169,15 @@ def add_page(self, page): page.apply_button_enabled.connect(self.apply_btn.setEnabled) # Container widget so that we can center the page - container = QWidget(self) layout = QHBoxLayout() layout.addWidget(page) layout.setAlignment(Qt.AlignHCenter) + + # The smaller margin to the right is necessary to compensate for the + # space added by the vertical scrollbar + layout.setContentsMargins(27, 27, 15, 27) + + container = QWidget(self) container.setLayout(layout) container.page = page @@ -182,15 +189,23 @@ def add_page(self, page): scrollarea.setWidget(container) self.pages_widget.addWidget(scrollarea) - # Add entry to the list widget on the left + # Add plugin entry item to contents widget item = QListWidgetItem(self.contents_widget) + item.setText(page.get_name()) + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + + # In case a plugin doesn't have an icon try: item.setIcon(page.get_icon()) except TypeError: pass - item.setText(page.get_name()) - item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - item.setSizeHint(QSize(0, 25)) + + # Increase font size of items + font_size = self.get_font( + SpyderFontType.Interface, font_size_delta=1).pointSize() + font = item.font() + font.setPointSize(font_size) + item.setFont(font) def check_all_settings(self): """This method is called to check all configuration page settings @@ -214,22 +229,39 @@ def _stylesheet(self): alignment='left' ) - # Remove right border radius of contents area + # Set style of contents area css['QListView#configdialog-contents'].setValues( - borderTopRightRadius='0px', - borderBottomRightRadius='0px', + padding='9px 0px', + backgroundColor=QStylePalette.COLOR_BACKGROUND_2, + border=f'1px solid {QStylePalette.COLOR_BACKGROUND_2}', ) # Remove border color on focus of contents area css['QListView#configdialog-contents:focus'].setValues( - border=f'1px solid {QStylePalette.COLOR_BACKGROUND_4}', + border=f'1px solid {QStylePalette.COLOR_BACKGROUND_2}', + ) + + # Add margin and padding for items in contents area + css['QListView#configdialog-contents::item'].setValues( + padding='6px', + margin='0px 9px' ) - # Remove left border and border radius of all page scroll areas + # Set border radius and background color for hover, active and inactive + # states of items + css['QListView#configdialog-contents::item:hover'].setValues( + borderRadius=f'{QStylePalette.SIZE_BORDER_RADIUS}', + ) + + for state in ['item:selected:active', 'item:selected:!active']: + css[f'QListView#configdialog-contents::{state}'].setValues( + borderRadius=f'{QStylePalette.SIZE_BORDER_RADIUS}', + backgroundColor=QStylePalette.COLOR_BACKGROUND_4 + ) + + # Remove border of all scroll areas for pages css['QScrollArea#configdialog-scrollarea'].setValues( - borderLeft='0px', - borderTopLeftRadius='0px', - borderBottomLeftRadius='0px', + border='0px', ) return css.toString() From b3cc0346b978e90760383eac6f6c13726259602f Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 2 Aug 2023 17:38:39 -0500 Subject: [PATCH 07/34] Preferences: Remove unneeded margins in "Completions and linting" tabs --- spyder/api/preferences.py | 5 +++++ spyder/plugins/completion/confpage.py | 1 + .../providers/languageserver/conftabs/otherlanguages.py | 4 ++-- spyder/plugins/completion/providers/snippets/conftabs.py | 6 +++--- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/spyder/api/preferences.py b/spyder/api/preferences.py index 83c1a0a8213..4bd184e3920 100644 --- a/spyder/api/preferences.py +++ b/spyder/api/preferences.py @@ -65,6 +65,11 @@ def __getattr__(self, attr): else: return super().__getattr__(attr) + def setLayout(self, layout): + """Remove default margins by default.""" + layout.setContentsMargins(0, 0, 0, 0) + super().setLayout(layout) + class PluginConfigPage(SpyderConfigPage): """ diff --git a/spyder/plugins/completion/confpage.py b/spyder/plugins/completion/confpage.py index 1ff6ff239c2..09265a6fbcc 100644 --- a/spyder/plugins/completion/confpage.py +++ b/spyder/plugins/completion/confpage.py @@ -87,6 +87,7 @@ def disable_completion_after_characters(state): disable_completion_after_characters) layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.completions_group) layout.addWidget(self.providers_group) layout.addStretch(1) diff --git a/spyder/plugins/completion/providers/languageserver/conftabs/otherlanguages.py b/spyder/plugins/completion/providers/languageserver/conftabs/otherlanguages.py index 32b74a65088..514adb05394 100644 --- a/spyder/plugins/completion/providers/languageserver/conftabs/otherlanguages.py +++ b/spyder/plugins/completion/providers/languageserver/conftabs/otherlanguages.py @@ -73,10 +73,10 @@ def __init__(self, parent): # Combined layout servers_layout = QVBoxLayout() - servers_layout.addSpacing(-10) servers_layout.addWidget(servers_label) + servers_layout.addSpacing(9) servers_layout.addWidget(table_group) - servers_layout.addSpacing(10) + servers_layout.addSpacing(9) servers_layout.addLayout(buttons_layout) self.setLayout(servers_layout) diff --git a/spyder/plugins/completion/providers/snippets/conftabs.py b/spyder/plugins/completion/providers/snippets/conftabs.py index 1e42c573d90..ac2a504f479 100644 --- a/spyder/plugins/completion/providers/snippets/conftabs.py +++ b/spyder/plugins/completion/providers/snippets/conftabs.py @@ -14,10 +14,9 @@ # Third party imports from qtpy.compat import getsavefilename, getopenfilename -from qtpy.QtCore import Qt, Slot +from qtpy.QtCore import Qt from qtpy.QtWidgets import (QComboBox, QGroupBox, QGridLayout, QLabel, - QMessageBox, QPushButton, QTabWidget, QVBoxLayout, - QWidget, QFileDialog) + QMessageBox, QPushButton, QVBoxLayout, QFileDialog) # Local imports from spyder.config.base import _ @@ -103,6 +102,7 @@ def __init__(self, parent): # Snippets layout snippets_layout = QVBoxLayout() snippets_layout.addWidget(snippets_info_label) + snippets_layout.addSpacing(9) snippets_layout.addWidget(snippet_lang_group) snippets_layout.addWidget(snippet_table_group) snippets_layout.addLayout(sn_buttons_layout) From c7f6101fd1fbbffff70399dfebd681ff8585655e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 2 Aug 2023 19:50:41 -0500 Subject: [PATCH 08/34] Preferences: Set for its tabs the same style used for dockwidget tabbars Introduce a new SpecialTabBarStyleSheet class for that. --- .../preferences/widgets/configdialog.py | 13 ++-- spyder/utils/stylesheet.py | 72 ++++++++++++++----- 2 files changed, 62 insertions(+), 23 deletions(-) diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index f8b11c7310a..9e381b7be7a 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -5,7 +5,6 @@ # (see spyder/__init__.py for details) # Third party imports -import qstylizer.style from qtpy.QtCore import QSize, Qt, Signal, Slot from qtpy.QtWidgets import ( QDialog, QDialogButtonBox, QGridLayout, QHBoxLayout, QListView, @@ -18,6 +17,7 @@ from spyder.config.manager import CONF from spyder.utils.icon_manager import ima from spyder.utils.palette import QStylePalette +from spyder.utils.stylesheet import SPECIAL_TABBAR_STYLESHEET class PageScrollArea(QScrollArea): @@ -222,11 +222,14 @@ def resizeEvent(self, event): @property def _stylesheet(self): - css = qstylizer.style.StyleSheet() + # Use special tabbar stylesheet for as the base one and then extend it. + tabs_stylesheet = SPECIAL_TABBAR_STYLESHEET.get_copy() + css = tabs_stylesheet.get_stylesheet() - # Show tabs aligned to the left - css['QTabWidget::tab-bar'].setValues( - alignment='left' + # Remove border and add padding for content inside tabs + css['QTabWidget::pane'].setValues( + border='0px', + padding='6px' ) # Set style of contents area diff --git a/spyder/utils/stylesheet.py b/spyder/utils/stylesheet.py index 48bb79b498b..d4fa15f9a37 100644 --- a/spyder/utils/stylesheet.py +++ b/spyder/utils/stylesheet.py @@ -506,6 +506,10 @@ def set_stylesheet(self): alignment='center' ) + css['QTabWidget::tab-bar'].setValues( + alignment='center' + ) + # Style for selected tabs css['QTabBar::tab:selected'].setValues( color=( @@ -522,11 +526,8 @@ def set_stylesheet(self): ) -class HorizontalDockTabBarStyleSheet(BaseDockTabBarStyleSheet): - """ - This implements the design for dockwidget tabs discussed on issue - spyder-ide/ux-improvements#4. - """ +class SpecialTabBarStyleSheet(BaseDockTabBarStyleSheet): + """Style for special horizontal tabbars.""" SCROLL_BUTTONS_BORDER_POS = 'right' @@ -539,18 +540,12 @@ def set_stylesheet(self): # Basic style css['QTabBar::tab'].setValues( - # No margins to left/right but top/bottom to separate tabbar from - # the dockwidget areas. - # Notes: - # * Top margin is half the one at the bottom so that we can show - # a bottom margin on dockwidgets that are not tabified. - # * The other half is added through the _margin_bottom attribute of - # PluginMainWidget. - margin=f'{margin_size}px 0px {2 * margin_size}px 0px', + # Only add margin to the bottom + margin=f'0px 0px {2 * margin_size}px 0px', # Border radius is added for specific tabs (see below) borderRadius='0px', # Remove a colored border added by QDarkStyle - borderTop='0px', + borderBottom='0px', # Add right border to make it work as our tabs separator borderRight=f'1px solid {self.color_tabs_separator}', # Padding for text inside tabs @@ -576,18 +571,15 @@ def set_stylesheet(self): borderLeft=f'1px solid {self.color_tabs_separator}', ) - # First and last tabs have rounded borders. Also, add margin to avoid - # them touch the left and right areas, respectively. + # First and last tabs have rounded borders css['QTabBar::tab:first'].setValues( borderTopLeftRadius='4px', borderBottomLeftRadius='4px', - marginLeft=f'{2 * margin_size}px', ) css['QTabBar::tab:last'].setValues( borderTopRightRadius='4px', borderBottomRightRadius='4px', - marginRight=f'{2 * margin_size}px', ) # Last tab doesn't need to show the separator @@ -597,6 +589,49 @@ def set_stylesheet(self): borderRightColor=f'{QStylePalette.COLOR_BACKGROUND_4}' ) + # Set bottom margin for scroll buttons. + css['QTabBar QToolButton'].setValues( + marginBottom=f'{2 * margin_size}px', + ) + + +class HorizontalDockTabBarStyleSheet(SpecialTabBarStyleSheet): + """ + This implements the design for dockwidget tabs discussed on issue + spyder-ide/ux-improvements#4. + """ + + def set_stylesheet(self): + super().set_stylesheet() + + # Main constants + css = self.get_stylesheet() + margin_size = AppStyle.MarginSize + + # Tabs style + css['QTabBar::tab'].setValues( + # No margins to left/right but top/bottom to separate tabbar from + # the dockwidget areas. + # Notes: + # * Top margin is half the one at the bottom so that we can show + # a bottom margin on dockwidgets that are not tabified. + # * The other half is added through the _margin_bottom attribute of + # PluginMainWidget. + margin=f'{margin_size}px 0px {2 * margin_size}px 0px', + # Remove a colored border added by QDarkStyle + borderTop='0px', + ) + + # Add margin to first and last tabs to avoid them touching the left and + # right dockwidget areas, respectively. + css['QTabBar::tab:first'].setValues( + marginLeft=f'{2 * margin_size}px', + ) + + css['QTabBar::tab:last'].setValues( + marginRight=f'{2 * margin_size}px', + ) + # Make top and bottom margins for scroll buttons even. # This is necessary since the tabbar top margin is half the one at the # bottom (see the notes in the 'QTabBar::tab' style above). @@ -684,6 +719,7 @@ def set_stylesheet(self): PANES_TABBAR_STYLESHEET = PanesTabBarStyleSheet() HORIZONTAL_DOCK_TABBAR_STYLESHEET = HorizontalDockTabBarStyleSheet() VERTICAL_DOCK_TABBAR_STYLESHEET = VerticalDockTabBarStyleSheet() +SPECIAL_TABBAR_STYLESHEET = SpecialTabBarStyleSheet() # ============================================================================= From d6e7a3f2b169f9ed4f756d00caf131eed36941c3 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 2 Aug 2023 20:12:23 -0500 Subject: [PATCH 09/34] Preferences: Show wait cursor when opening its dialog --- spyder/plugins/preferences/plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spyder/plugins/preferences/plugin.py b/spyder/plugins/preferences/plugin.py index c1dcf425d12..227275f090b 100644 --- a/spyder/plugins/preferences/plugin.py +++ b/spyder/plugins/preferences/plugin.py @@ -257,9 +257,11 @@ def merge_configurations( def open_dialog(self): container = self.get_container() + self.before_long_process('') container.create_dialog( self.config_pages, self.config_tabs, self.get_main() ) + self.after_long_process() # ---------------- Public Spyder API required methods --------------------- @staticmethod From 9c05e3aad98037a9c55a3648e9d92fc3607a4a29 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 2 Aug 2023 22:20:32 -0500 Subject: [PATCH 10/34] Preferences: Small style fixes in several pages --- .../completion/providers/snippets/conftabs.py | 1 + spyder/plugins/editor/confpage.py | 17 ++++++++++++----- spyder/plugins/ipythonconsole/confpage.py | 2 +- spyder/plugins/preferences/api.py | 3 ++- .../plugins/preferences/widgets/configdialog.py | 2 +- spyder/plugins/run/confpage.py | 3 ++- 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/spyder/plugins/completion/providers/snippets/conftabs.py b/spyder/plugins/completion/providers/snippets/conftabs.py index ac2a504f479..2f4ed2d7c4d 100644 --- a/spyder/plugins/completion/providers/snippets/conftabs.py +++ b/spyder/plugins/completion/providers/snippets/conftabs.py @@ -56,6 +56,7 @@ def __init__(self, parent): self.change_language_snippets) snippet_lang_group = QGroupBox(_('Language')) + snippet_lang_group.setStyleSheet('margin-bottom: 3px') snippet_lang_layout = QVBoxLayout() snippet_lang_layout.addWidget(self.snippets_language_cb) snippet_lang_group.setLayout(snippet_lang_layout) diff --git a/spyder/plugins/editor/confpage.py b/spyder/plugins/editor/confpage.py index e25162d507d..03ac2aac18a 100644 --- a/spyder/plugins/editor/confpage.py +++ b/spyder/plugins/editor/confpage.py @@ -209,9 +209,15 @@ def enable_tabwidth_spin(index): # --- Advanced tab --- # -- Templates + templates_group = QGroupBox(_('Templates')) template_btn = self.create_button(_("Edit template for new files"), self.plugin.edit_template) + templates_layout = QVBoxLayout() + templates_layout.addSpacing(3) + templates_layout.addWidget(template_btn) + templates_group.setLayout(templates_layout) + # -- Autosave autosave_group = QGroupBox(_('Autosave')) autosave_checkbox = newcb( @@ -279,7 +285,7 @@ def enable_tabwidth_spin(index): "mixed end-of-line characters (this may " "raise syntax errors in the consoles " "on Windows platforms), Spyder may fix the " - "file automatically.")) + "file automatically.
")) eol_label.setWordWrap(True) check_eol_box = newcb(_("Fix automatically and show warning " "message box"), @@ -318,10 +324,11 @@ def enable_tabwidth_spin(index): self.tabs = QTabWidget() self.tabs.addTab(self.create_tab(display_widget), _("Display")) self.tabs.addTab(self.create_tab(sourcecode_widget), _("Source code")) - self.tabs.addTab(self.create_tab(template_btn, autosave_group, - docstring_group, annotations_group, - eol_group), - _("Advanced settings")) + self.tabs.addTab( + self.create_tab(templates_group, autosave_group, docstring_group, + annotations_group, eol_group), + _("Advanced settings") + ) vlayout = QVBoxLayout() vlayout.addWidget(self.tabs) diff --git a/spyder/plugins/ipythonconsole/confpage.py b/spyder/plugins/ipythonconsole/confpage.py index 19f37b029e3..86dd2c25aec 100644 --- a/spyder/plugins/ipythonconsole/confpage.py +++ b/spyder/plugins/ipythonconsole/confpage.py @@ -257,7 +257,7 @@ def setup_page(self): "<Space> for some completions; " "e.g. np.sin(<Space>np.<Tab>" " works while np.sin(np.<Tab> " - " doesn't.")) + " doesn't.
")) greedy_label.setWordWrap(True) greedy_box = newcb(_("Use greedy completion in the IPython console"), "greedy_completer", diff --git a/spyder/plugins/preferences/api.py b/spyder/plugins/preferences/api.py index a6a24b78c6d..84c14693b7d 100644 --- a/spyder/plugins/preferences/api.py +++ b/spyder/plugins/preferences/api.py @@ -891,8 +891,9 @@ def create_button(self, text, callback): def create_tab(self, *widgets): """Create simple tab widget page: widgets added in a vertical layout""" - widget = QWidget() + widget = QWidget(self) layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) for widg in widgets: layout.addWidget(widg) layout.addStretch(1) diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index 9e381b7be7a..598f2609c3b 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -229,7 +229,7 @@ def _stylesheet(self): # Remove border and add padding for content inside tabs css['QTabWidget::pane'].setValues( border='0px', - padding='6px' + padding='9px', ) # Set style of contents area diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index b0ae2157851..05427c42aba 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -230,8 +230,9 @@ def setup_page(self): vlayout = QVBoxLayout() vlayout.addWidget(about_label) - vlayout.addSpacing(10) + vlayout.addSpacing(9) vlayout.addWidget(self.executor_combo) + vlayout.addSpacing(9) vlayout.addWidget(params_group) vlayout.addLayout(sn_buttons_layout) vlayout.addStretch(1) From 4c33c5265e5d3a5263873d7bf1008343a4247e80 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 3 Aug 2023 10:34:43 -0500 Subject: [PATCH 11/34] Preferences: Show tooltips for plugin names whose text is elided --- .../preferences/widgets/configdialog.py | 50 +++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index 598f2609c3b..2657b6d9707 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -6,6 +6,7 @@ # Third party imports from qtpy.QtCore import QSize, Qt, Signal, Slot +from qtpy.QtGui import QFontMetrics from qtpy.QtWidgets import ( QDialog, QDialogButtonBox, QGridLayout, QHBoxLayout, QListView, QListWidget, QListWidgetItem, QPushButton, QScrollArea, QStackedWidget, @@ -36,10 +37,19 @@ class ConfigDialog(QDialog, SpyderFontsMixin): sig_size_changed = Signal(QSize) sig_reset_preferences_requested = Signal() + # Constants + ITEMS_MARGIN = 9 + ITEMS_PADDING = 6 + CONTENTS_MIN_WIDTH = 300 + ICON_SIZE = 24 + def __init__(self, parent=None): QDialog.__init__(self, parent) + # Attributes self.main = parent + self.items_font = self.get_font( + SpyderFontType.Interface, font_size_delta=1) # Widgets self.pages_widget = QStackedWidget(self) @@ -65,8 +75,14 @@ def __init__(self, parent=None): self.contents_widget.setMovement(QListView.Static) self.contents_widget.setSpacing(3) self.contents_widget.setCurrentRow(0) - self.contents_widget.setMinimumWidth(300) + self.contents_widget.setMinimumWidth(self.CONTENTS_MIN_WIDTH) self.contents_widget.setObjectName('configdialog-contents') + self.contents_widget.setIconSize(QSize(self.ICON_SIZE, self.ICON_SIZE)) + + # Don't show horizontal scrollbar because it doesn't look good. Instead + # we show tooltips if the text doesn't fit in contents_widget width. + self.contents_widget.setHorizontalScrollBarPolicy( + Qt.ScrollBarAlwaysOff) # Layout contents_and_pages_layout = QGridLayout() @@ -200,12 +216,26 @@ def add_page(self, page): except TypeError: pass - # Increase font size of items - font_size = self.get_font( - SpyderFontType.Interface, font_size_delta=1).pointSize() - font = item.font() - font.setPointSize(font_size) - item.setFont(font) + # Set font for items + item.setFont(self.items_font) + + # Check if it's necessary to add a tooltip to the item + text_width = QFontMetrics(self.items_font).size( + Qt.TextSingleLine, page.get_name() + ).width() + + actual_width = ( + text_width + 2 * self.ITEMS_MARGIN + self.ICON_SIZE + + self.ITEMS_PADDING + ) + + available_width = ( + self.CONTENTS_MIN_WIDTH - + (2 * self.ITEMS_MARGIN + self.ICON_SIZE + self.ITEMS_PADDING) + ) + + if actual_width > available_width: + item.setToolTip(page.get_name()) def check_all_settings(self): """This method is called to check all configuration page settings @@ -234,7 +264,7 @@ def _stylesheet(self): # Set style of contents area css['QListView#configdialog-contents'].setValues( - padding='9px 0px', + padding=f'{self.ITEMS_MARGIN}px 0px', backgroundColor=QStylePalette.COLOR_BACKGROUND_2, border=f'1px solid {QStylePalette.COLOR_BACKGROUND_2}', ) @@ -246,8 +276,8 @@ def _stylesheet(self): # Add margin and padding for items in contents area css['QListView#configdialog-contents::item'].setValues( - padding='6px', - margin='0px 9px' + padding=f'{self.ITEMS_PADDING}px', + margin=f'0px {self.ITEMS_MARGIN}px' ) # Set border radius and background color for hover, active and inactive From 25af2a664c0a4abbb0b5fa6552f6fedc3cd6caae Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 3 Aug 2023 11:28:52 -0500 Subject: [PATCH 12/34] Editor: Improve config page - Break ungrouped options into groups so users can tell them apart more easily. - Improve names of some options and add tooltips to others. --- spyder/plugins/editor/confpage.py | 90 +++++++++++++++++++------------ 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/spyder/plugins/editor/confpage.py b/spyder/plugins/editor/confpage.py index 03ac2aac18a..39a41872cf8 100644 --- a/spyder/plugins/editor/confpage.py +++ b/spyder/plugins/editor/confpage.py @@ -7,7 +7,7 @@ """Editor config page.""" from qtpy.QtWidgets import (QGridLayout, QGroupBox, QHBoxLayout, QLabel, - QTabWidget, QVBoxLayout, QWidget) + QTabWidget, QVBoxLayout) from spyder.api.config.decorators import on_conf_change from spyder.api.config.mixins import SpyderConfigurationObserver @@ -74,15 +74,16 @@ def setup_page(self): occurrence_spin.slabel.setEnabled( self.get_option('occurrence_highlighting')) - display_g_layout = QGridLayout() - display_g_layout.addWidget(occurrence_box, 0, 0) - display_g_layout.addWidget(occurrence_spin.spinbox, 0, 1) - display_g_layout.addWidget(occurrence_spin.slabel, 0, 2) + occurrence_glayout = QGridLayout() + occurrence_glayout.addWidget(occurrence_box, 0, 0) + occurrence_glayout.addWidget(occurrence_spin.spinbox, 0, 1) + occurrence_glayout.addWidget(occurrence_spin.slabel, 0, 2) - display_h_layout = QHBoxLayout() - display_h_layout.addLayout(display_g_layout) - display_h_layout.addStretch(1) + occurrence_layout = QHBoxLayout() + occurrence_layout.addLayout(occurrence_glayout) + occurrence_layout.addStretch(1) + display_group = QGroupBox(_("Display")) display_layout = QVBoxLayout() display_layout.addWidget(showtabbar_box) display_layout.addWidget(showclassfuncdropdown_box) @@ -91,14 +92,20 @@ def setup_page(self): display_layout.addWidget(linenumbers_box) display_layout.addWidget(breakpoints_box) display_layout.addWidget(blanks_box) - display_layout.addWidget(currentline_box) - display_layout.addWidget(currentcell_box) - display_layout.addWidget(wrap_mode_box) - display_layout.addWidget(scroll_past_end_box) - display_layout.addLayout(display_h_layout) + display_group.setLayout(display_layout) - display_widget = QWidget() - display_widget.setLayout(display_layout) + highlight_group = QGroupBox(_("Highlight")) + highlight_layout = QVBoxLayout() + highlight_layout.addWidget(currentline_box) + highlight_layout.addWidget(currentcell_box) + highlight_layout.addLayout(occurrence_layout) + highlight_group.setLayout(highlight_layout) + + other_group = QGroupBox(_("Other")) + other_layout = QVBoxLayout() + other_layout.addWidget(wrap_mode_box) + other_layout.addWidget(scroll_past_end_box) + other_group.setLayout(other_layout) # --- Source code tab --- closepar_box = newcb( @@ -122,7 +129,7 @@ def setup_page(self): "completion may be triggered using the alternate\n" "shortcut: Ctrl+Space)")) strip_mode_box = newcb( - _("Automatically strip trailing spaces on changed lines"), + _("Automatic stripping of trailing spaces on changed lines"), 'strip_trailing_spaces_on_modify', default=True, tip=_("If enabled, modified lines of code (excluding strings)\n" "will have their trailing whitespace stripped when leaving them.\n" @@ -130,9 +137,11 @@ def setup_page(self): ibackspace_box = newcb( _("Intelligent backspace"), 'intelligent_backspace', + tip=_("Make the backspace key automatically remove the amount of " + "indentation characters set above."), default=True) self.removetrail_box = newcb( - _("Automatically remove trailing spaces when saving files"), + _("Automatic removal of trailing spaces when saving files"), 'always_remove_trailing_spaces', default=False) self.add_newline_box = newcb( @@ -191,21 +200,24 @@ def enable_tabwidth_spin(index): indent_tab_layout.addLayout(indent_tab_grid_layout) indent_tab_layout.addStretch(1) - sourcecode_layout = QVBoxLayout() - sourcecode_layout.addWidget(closepar_box) - sourcecode_layout.addWidget(autounindent_box) - sourcecode_layout.addWidget(add_colons_box) - sourcecode_layout.addWidget(close_quotes_box) - sourcecode_layout.addWidget(tab_mode_box) - sourcecode_layout.addWidget(ibackspace_box) - sourcecode_layout.addWidget(self.removetrail_box) - sourcecode_layout.addWidget(self.add_newline_box) - sourcecode_layout.addWidget(self.remove_trail_newline_box) - sourcecode_layout.addWidget(strip_mode_box) - sourcecode_layout.addLayout(indent_tab_layout) - - sourcecode_widget = QWidget() - sourcecode_widget.setLayout(sourcecode_layout) + automatic_group = QGroupBox(_("Automatic changes")) + automatic_layout = QVBoxLayout() + automatic_layout.addWidget(closepar_box) + automatic_layout.addWidget(autounindent_box) + automatic_layout.addWidget(add_colons_box) + automatic_layout.addWidget(close_quotes_box) + automatic_layout.addWidget(self.removetrail_box) + automatic_layout.addWidget(strip_mode_box) + automatic_layout.addWidget(self.add_newline_box) + automatic_layout.addWidget(self.remove_trail_newline_box) + automatic_group.setLayout(automatic_layout) + + indentation_group = QGroupBox(_("Indentation")) + indentation_layout = QVBoxLayout() + indentation_layout.addLayout(indent_tab_layout) + indentation_layout.addWidget(ibackspace_box) + indentation_layout.addWidget(tab_mode_box) + indentation_group.setLayout(indentation_layout) # --- Advanced tab --- # -- Templates @@ -321,9 +333,17 @@ def enable_tabwidth_spin(index): eol_group.setLayout(eol_layout) # --- Tabs --- - self.tabs = QTabWidget() - self.tabs.addTab(self.create_tab(display_widget), _("Display")) - self.tabs.addTab(self.create_tab(sourcecode_widget), _("Source code")) + self.tabs = QTabWidget(self) + self.tabs.addTab( + self.create_tab(display_group, highlight_group, other_group), + _("Interface") + ) + + self.tabs.addTab( + self.create_tab(automatic_group, indentation_group), + _("Source code") + ) + self.tabs.addTab( self.create_tab(templates_group, autosave_group, docstring_group, annotations_group, eol_group), From 78f54771880eec5eeab93d280064548ac99963ef Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 3 Aug 2023 22:56:32 -0500 Subject: [PATCH 13/34] IPython console: Improve its config page - Make consistent the use of "Interface" and "Display" with other plugins. - Add a new section about "Confirmations" in the "Interface" tab. --- spyder/plugins/ipythonconsole/confpage.py | 95 ++++++++++++++--------- 1 file changed, 59 insertions(+), 36 deletions(-) diff --git a/spyder/plugins/ipythonconsole/confpage.py b/spyder/plugins/ipythonconsole/confpage.py index 86dd2c25aec..e7622a57e2a 100644 --- a/spyder/plugins/ipythonconsole/confpage.py +++ b/spyder/plugins/ipythonconsole/confpage.py @@ -24,36 +24,45 @@ class IPythonConsoleConfigPage(PluginConfigPage): def setup_page(self): newcb = self.create_checkbox - # Interface Group - interface_group = QGroupBox(_("Interface")) + # Display group + display_group = QGroupBox(_("Display")) banner_box = newcb(_("Display initial banner"), 'show_banner', tip=_("This option lets you hide the message " "shown at\nthe top of the console when " "it's opened.")) calltips_box = newcb(_("Show calltips"), 'show_calltips') - ask_box = newcb(_("Ask for confirmation before closing"), - 'ask_before_closing') - reset_namespace_box = newcb( - _("Ask for confirmation before removing all user-defined " - "variables"), - 'show_reset_namespace_warning', - tip=_("This option lets you hide the warning message shown\n" - "when resetting the namespace from Spyder.")) show_time_box = newcb(_("Show elapsed time"), 'show_elapsed_time') + + display_layout = QVBoxLayout() + display_layout .addWidget(banner_box) + display_layout .addWidget(calltips_box) + display_layout.addWidget(show_time_box) + display_group.setLayout(display_layout) + + # Confirmations group + confirmations_group = QGroupBox(_("Confirmations")) + ask_box = newcb( + _("Ask for confirmation before closing"), 'ask_before_closing' + ) + reset_namespace_box = newcb( + _("Ask for confirmation before removing all user-defined " + "variables"), + 'show_reset_namespace_warning', + tip=_("This option lets you hide the warning message shown\n" + "when resetting the namespace from Spyder.") + ) ask_restart_box = newcb( - _("Ask for confirmation before restarting"), - 'ask_before_restart', - tip=_("This option lets you hide the warning message shown\n" - "when restarting the kernel.")) - - interface_layout = QVBoxLayout() - interface_layout.addWidget(banner_box) - interface_layout.addWidget(calltips_box) - interface_layout.addWidget(ask_box) - interface_layout.addWidget(reset_namespace_box) - interface_layout.addWidget(show_time_box) - interface_layout.addWidget(ask_restart_box) - interface_group.setLayout(interface_layout) + _("Ask for confirmation before restarting"), + 'ask_before_restart', + tip=_("This option lets you hide the warning message shown\n" + "when restarting the kernel.") + ) + + confirmations_layout = QVBoxLayout() + confirmations_layout.addWidget(ask_box) + confirmations_layout.addWidget(reset_namespace_box) + confirmations_layout.addWidget(ask_restart_box) + confirmations_group.setLayout(confirmations_layout) comp_group = QGroupBox(_("Completion type")) comp_label = QLabel(_("Decide what type of completion to use")) @@ -61,6 +70,7 @@ def setup_page(self): completers = [(_("Graphical"), 0), (_("Terminal"), 1), (_("Plain"), 2)] comp_box = self.create_combobox(_("Completion:")+" ", completers, 'completion_type') + comp_layout = QVBoxLayout() comp_layout.addWidget(comp_label) comp_layout.addWidget(comp_box) @@ -78,6 +88,7 @@ def setup_page(self): tip=_("Set the maximum number of lines of text shown in the\n" "console before truncation. Specifying -1 disables it\n" "(not recommended!)")) + source_code_layout = QVBoxLayout() source_code_layout.addWidget(buffer_spin) source_code_group.setLayout(source_code_layout) @@ -333,8 +344,6 @@ def setup_page(self): '%i</span>]:'), alignment=Qt.Horizontal) - prompts_layout = QVBoxLayout() - prompts_layout.addWidget(prompts_label) prompts_g_layout = QGridLayout() prompts_g_layout.addWidget(in_prompt_edit.label, 0, 0) prompts_g_layout.addWidget(in_prompt_edit.textbox, 0, 1) @@ -342,6 +351,9 @@ def setup_page(self): prompts_g_layout.addWidget(out_prompt_edit.label, 1, 0) prompts_g_layout.addWidget(out_prompt_edit.textbox, 1, 1) prompts_g_layout.addWidget(out_prompt_edit.help_label, 1, 2) + + prompts_layout = QVBoxLayout() + prompts_layout.addWidget(prompts_label) prompts_layout.addLayout(prompts_g_layout) prompts_group.setLayout(prompts_layout) @@ -356,17 +368,28 @@ def setup_page(self): windows_group.setLayout(windows_layout) # --- Tabs organization --- - self.tabs = QTabWidget() - self.tabs.addTab(self.create_tab(interface_group, comp_group, - source_code_group), _("Display")) - self.tabs.addTab(self.create_tab( - pylab_group, backend_group, inline_group), _("Graphics")) - self.tabs.addTab(self.create_tab( - run_lines_group, run_file_group), _("Startup")) - self.tabs.addTab(self.create_tab( - jedi_group, greedy_group, autocall_group, - sympy_group, prompts_group, - windows_group), _("Advanced settings")) + self.tabs = QTabWidget(self) + self.tabs.addTab( + self.create_tab(display_group, confirmations_group, comp_group, + source_code_group), + _("Interface") + ) + + self.tabs.addTab( + self.create_tab(pylab_group, backend_group, inline_group), + _("Graphics") + ) + + self.tabs.addTab( + self.create_tab(run_lines_group, run_file_group), + _("Startup") + ) + + self.tabs.addTab( + self.create_tab(jedi_group, greedy_group, autocall_group, + sympy_group, prompts_group, windows_group), + _("Advanced settings") + ) vlayout = QVBoxLayout() vlayout.addWidget(self.tabs) From 001db9f4fe597a20334b71fcd7c30b45827c7548 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 4 Aug 2023 18:24:44 -0500 Subject: [PATCH 14/34] Preferences: Increase tabs font size and padding per review --- spyder/plugins/editor/confpage.py | 2 +- spyder/plugins/ipythonconsole/confpage.py | 2 +- spyder/plugins/preferences/widgets/configdialog.py | 12 +++++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/spyder/plugins/editor/confpage.py b/spyder/plugins/editor/confpage.py index 39a41872cf8..4a6afc7acf9 100644 --- a/spyder/plugins/editor/confpage.py +++ b/spyder/plugins/editor/confpage.py @@ -297,7 +297,7 @@ def enable_tabwidth_spin(index): "mixed end-of-line characters (this may " "raise syntax errors in the consoles " "on Windows platforms), Spyder may fix the " - "file automatically.
")) + "file automatically.

")) eol_label.setWordWrap(True) check_eol_box = newcb(_("Fix automatically and show warning " "message box"), diff --git a/spyder/plugins/ipythonconsole/confpage.py b/spyder/plugins/ipythonconsole/confpage.py index e7622a57e2a..0dbf0633a61 100644 --- a/spyder/plugins/ipythonconsole/confpage.py +++ b/spyder/plugins/ipythonconsole/confpage.py @@ -268,7 +268,7 @@ def setup_page(self): "<Space> for some completions; " "e.g. np.sin(<Space>np.<Tab>" " works while np.sin(np.<Tab> " - " doesn't.
")) + " doesn't.

")) greedy_label.setWordWrap(True) greedy_box = newcb(_("Use greedy completion in the IPython console"), "greedy_completer", diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index 2657b6d9707..b6acbbd0a7e 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -256,10 +256,20 @@ def _stylesheet(self): tabs_stylesheet = SPECIAL_TABBAR_STYLESHEET.get_copy() css = tabs_stylesheet.get_stylesheet() + # Set tabs font size to be the same as the one for items + css.QTabBar.setValues( + fontSize=f'{self.items_font.pointSize()}pt', + ) + + # Increase padding around text a bit because we're using an larger font + css['QTabBar::tab'].setValues( + padding='6px 10px', + ) + # Remove border and add padding for content inside tabs css['QTabWidget::pane'].setValues( border='0px', - padding='9px', + padding='15px', ) # Set style of contents area From 3c63e2531f321964d9fe959d238d4f250f0f971a Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 8 Aug 2023 15:22:17 -0500 Subject: [PATCH 15/34] Preferences: Add header sections to its plugin and minor style fixes --- spyder/plugins/preferences/plugin.py | 67 ++++++++++++++++++---------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/spyder/plugins/preferences/plugin.py b/spyder/plugins/preferences/plugin.py index 227275f090b..c78adf318df 100644 --- a/spyder/plugins/preferences/plugin.py +++ b/spyder/plugins/preferences/plugin.py @@ -66,10 +66,16 @@ def __init__(self, parent, configuration=None): self.config_pages = {} self.config_tabs = {} + # ---- Public API + # ------------------------------------------------------------------------- def register_plugin_preferences( - self, plugin: Union[SpyderPluginV2, SpyderPlugin]) -> None: - if (hasattr(plugin, 'CONF_WIDGET_CLASS') and - plugin.CONF_WIDGET_CLASS is not None): + self, + plugin: Union[SpyderPluginV2, SpyderPlugin] + ) -> None: + if ( + hasattr(plugin, 'CONF_WIDGET_CLASS') + and plugin.CONF_WIDGET_CLASS is not None + ): # New API Widget = plugin.CONF_WIDGET_CLASS @@ -96,9 +102,10 @@ def register_plugin_preferences( plugin_tabs = self.config_tabs.get(plugin_name, []) plugin_tabs += tabs_to_add self.config_tabs[plugin_name] = plugin_tabs - - elif (hasattr(plugin, 'CONFIGWIDGET_CLASS') and - plugin.CONFIGWIDGET_CLASS is not None): + elif ( + hasattr(plugin, 'CONFIGWIDGET_CLASS') + and plugin.CONFIGWIDGET_CLASS is not None + ): # Old API Widget = plugin.CONFIGWIDGET_CLASS @@ -106,7 +113,9 @@ def register_plugin_preferences( self.OLD_API, Widget, plugin) def deregister_plugin_preferences( - self, plugin: Union[SpyderPluginV2, SpyderPlugin]): + self, + plugin: Union[SpyderPluginV2, SpyderPlugin] + ) -> None: """Remove a plugin preference page and additional configuration tabs.""" name = (getattr(plugin, 'NAME', None) or getattr(plugin, 'CONF_SECTION', None)) @@ -179,11 +188,13 @@ def check_version_and_merge( self.set_conf( conf_key, new_value, section=conf_section) - - def merge_defaults(self, prev_default: BasicType, - new_default: BasicType, - allow_replacement: bool = False, - allow_deletions: bool = False) -> BasicType: + def merge_defaults( + self, + prev_default: BasicType, + new_default: BasicType, + allow_replacement: bool = False, + allow_deletions: bool = False + ) -> BasicType: """Compare and merge two versioned values.""" prev_type = type(prev_default) new_type = type(new_default) @@ -216,7 +227,10 @@ def merge_defaults(self, prev_default: BasicType, return prev_default def merge_configurations( - self, current_value: BasicType, new_value: BasicType) -> BasicType: + self, + current_value: BasicType, + new_value: BasicType + ) -> BasicType: """ Recursively match and merge a new configuration value into a previous one. @@ -263,7 +277,22 @@ def open_dialog(self): ) self.after_long_process() - # ---------------- Public Spyder API required methods --------------------- + @Slot() + def reset(self): + answer = QMessageBox.warning( + self.main, + _("Warning"), + _("Spyder will restart and reset to default settings:

" + "Do you want to continue?"), + QMessageBox.Yes | QMessageBox.No + ) + + if answer == QMessageBox.Yes: + os.environ['SPYDER_RESET'] = 'True' + self.sig_restart_requested.emit() + + # ---- SpyderPluginV2 API + # ------------------------------------------------------------------------- @staticmethod def get_name() -> str: return _('Preferences') @@ -331,16 +360,6 @@ def on_toolbar_teardown(self): toolbar_id=ApplicationToolbars.Main ) - @Slot() - def reset(self): - answer = QMessageBox.warning(self.main, _("Warning"), - _("Spyder will restart and reset to default settings:

" - "Do you want to continue?"), - QMessageBox.Yes | QMessageBox.No) - if answer == QMessageBox.Yes: - os.environ['SPYDER_RESET'] = 'True' - self.sig_restart_requested.emit() - def on_close(self, cancelable=False): container = self.get_container() if container.is_preferences_open(): From 9f2033b762a7495ea73743fa3bcd890e878b16c0 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 10 Sep 2023 18:36:56 -0500 Subject: [PATCH 16/34] Preferences: Move `api` module to `widgets/config_widgets.py` That's because that module was not really exposing an API but just declaring several widgets. --- spyder/api/preferences.py | 5 ++- .../plugins/appearance/tests/test_confpage.py | 2 +- .../{api.py => widgets/config_widgets.py} | 31 +------------------ 3 files changed, 6 insertions(+), 32 deletions(-) rename spyder/plugins/preferences/{api.py => widgets/config_widgets.py} (97%) diff --git a/spyder/api/preferences.py b/spyder/api/preferences.py index 4bd184e3920..00571841f31 100644 --- a/spyder/api/preferences.py +++ b/spyder/api/preferences.py @@ -16,7 +16,10 @@ from spyder.config.manager import CONF from spyder.config.types import ConfigurationKey from spyder.api.utils import PrefixedTuple -from spyder.plugins.preferences.api import SpyderConfigPage, BaseConfigTab +from spyder.plugins.preferences.widgets.config_widgets import ( + SpyderConfigPage, + BaseConfigTab +) OptionSet = Set[ConfigurationKey] diff --git a/spyder/plugins/appearance/tests/test_confpage.py b/spyder/plugins/appearance/tests/test_confpage.py index d74a2c4e4ff..7da0f208087 100644 --- a/spyder/plugins/appearance/tests/test_confpage.py +++ b/spyder/plugins/appearance/tests/test_confpage.py @@ -12,7 +12,7 @@ # Local imports from spyder.config.manager import CONF from spyder.plugins.appearance.plugin import Appearance -from spyder.plugins.preferences.api import SpyderConfigPage +from spyder.plugins.preferences.widgets.config_widgets import SpyderConfigPage from spyder.plugins.preferences.tests.conftest import ( config_dialog, MainWindowMock) diff --git a/spyder/plugins/preferences/api.py b/spyder/plugins/preferences/widgets/config_widgets.py similarity index 97% rename from spyder/plugins/preferences/api.py rename to spyder/plugins/preferences/widgets/config_widgets.py index 84c14693b7d..b1acbcfa11b 100644 --- a/spyder/plugins/preferences/api.py +++ b/spyder/plugins/preferences/widgets/config_widgets.py @@ -686,7 +686,7 @@ def create_spinbox(self, prefix, suffix, option, default=NoDefault, if min_ is not None: spinbox.setMinimum(min_) if max_ is not None: - spinbox.setMaximum(max_) + spinbox.setMaximum(max_) self.spinboxes[spinbox] = (section, option, default) layout = QHBoxLayout() for subwidget in (plabel, spinbox, slabel): @@ -943,32 +943,3 @@ def add_tab(self, Widget): self.tabs.addTab(self.create_tab(widget), Widget.TITLE) self.load_from_conf() - - -class GeneralConfigPage(SpyderConfigPage): - """Config page that maintains reference to main Spyder window - and allows to specify page name and icon declaratively - """ - CONF_SECTION = None - - NAME = None # configuration page name, e.g. _("General") - ICON = None # name of icon resource (24x24) - - def __init__(self, parent, main): - SpyderConfigPage.__init__(self, parent) - self.main = main - - def get_name(self): - """Configuration page name""" - return self.NAME - - def get_icon(self): - """Loads page icon named by self.ICON""" - return self.ICON - - def apply_settings(self, options): - raise NotImplementedError - - -class PreferencePages: - General = 'main' From bb51667b0d517314e807b456ccbb7c7b30691775 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 11 Sep 2023 10:49:00 -0500 Subject: [PATCH 17/34] Preferences: Organize page entries in different sections The most important sections are shown first, then the other plugin pages are shown alphabetically and finally we show the Plugins page. --- spyder/plugins/layout/plugin.py | 2 +- spyder/plugins/preferences/api.py | 26 ++++++++++++ spyder/plugins/preferences/plugin.py | 41 +++++++++++++++++++ .../preferences/widgets/configdialog.py | 41 +++++++++++++++---- .../plugins/preferences/widgets/container.py | 19 ++++++--- 5 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 spyder/plugins/preferences/api.py diff --git a/spyder/plugins/layout/plugin.py b/spyder/plugins/layout/plugin.py index a12c56af2b4..ddb7ec942b6 100644 --- a/spyder/plugins/layout/plugin.py +++ b/spyder/plugins/layout/plugin.py @@ -33,7 +33,7 @@ HorizontalSplitLayout, MatlabLayout, RLayout, SpyderLayout, VerticalSplitLayout) -from spyder.plugins.preferences.widgets.container import PreferencesActions +from spyder.plugins.preferences.api import PreferencesActions from spyder.plugins.toolbar.api import ( ApplicationToolbars, MainToolbarSections) from spyder.py3compat import qbytearray_to_str # FIXME: diff --git a/spyder/plugins/preferences/api.py b/spyder/plugins/preferences/api.py new file mode 100644 index 00000000000..7f1bf384670 --- /dev/null +++ b/spyder/plugins/preferences/api.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Preferences Plugin API. +""" + +from spyder.api.plugins import Plugins + + +# We consider these to be the plugins with the most important pages. So, we'll +# show those pages as the first entries in the config dialog +MOST_IMPORTANT_PAGES = [ + Plugins.Appearance, + Plugins.Application, + Plugins.Shortcuts, + Plugins.MainInterpreter, +] + + +class PreferencesActions: + Show = 'show_action' + Reset = 'reset_action' diff --git a/spyder/plugins/preferences/plugin.py b/spyder/plugins/preferences/plugin.py index c78adf318df..cb2d5c42013 100644 --- a/spyder/plugins/preferences/plugin.py +++ b/spyder/plugins/preferences/plugin.py @@ -18,6 +18,7 @@ # Third-party imports from packaging.version import parse, Version +from pyuca import Collator from qtpy.QtGui import QIcon from qtpy.QtCore import Slot from qtpy.QtWidgets import QMessageBox @@ -26,17 +27,22 @@ from spyder.api.plugins import Plugins, SpyderPluginV2, SpyderPlugin from spyder.api.plugin_registration.decorators import ( on_plugin_available, on_plugin_teardown) +from spyder.api.plugin_registration.registry import PreferencesAdapter from spyder.config.base import _ from spyder.config.main import CONF_VERSION from spyder.config.user import NoDefault from spyder.plugins.mainmenu.api import ApplicationMenus, ToolsMenuSections +from spyder.plugins.preferences.api import MOST_IMPORTANT_PAGES from spyder.plugins.preferences.widgets.container import ( PreferencesActions, PreferencesContainer) from spyder.plugins.pythonpath.api import PythonpathActions from spyder.plugins.toolbar.api import ApplicationToolbars, MainToolbarSections + +# Logger logger = logging.getLogger(__name__) +# Types BaseType = Union[int, float, bool, complex, str, bytes] IterableType = Union[list, tuple] BasicType = Union[BaseType, IterableType] @@ -65,6 +71,7 @@ def __init__(self, parent, configuration=None): super().__init__(parent, configuration) self.config_pages = {} self.config_tabs = {} + self._config_pages_ordered = False # ---- Public API # ------------------------------------------------------------------------- @@ -271,10 +278,14 @@ def merge_configurations( def open_dialog(self): container = self.get_container() + self.before_long_process('') + + self._reorder_config_pages() container.create_dialog( self.config_pages, self.config_tabs, self.get_main() ) + self.after_long_process() @Slot() @@ -365,3 +376,33 @@ def on_close(self, cancelable=False): if container.is_preferences_open(): container.close_preferences() return True + + # ---- Private API + # ------------------------------------------------------------------------- + def _reorder_config_pages(self): + if self._config_pages_ordered: + return + + plugins_page = [PreferencesAdapter.NAME] + + # Order pages alphabetically by plugin name + pages = [] + for k, v in self.config_pages.items(): + pages.append((k, v[2].get_name())) + + collator = Collator() + pages.sort(key=lambda p: collator.sort_key(p[1])) + + # Get pages from the previous list without including the most important + # ones and the plugins page because they'll be added in a different + # order. + other_pages = [ + page[0] for page in pages + if page[0] not in (MOST_IMPORTANT_PAGES + plugins_page) + ] + + # Show most important pages first and the Plugins page last + ordering = MOST_IMPORTANT_PAGES + other_pages + plugins_page + self.config_pages = {k: self.config_pages[k] for k in ordering} + + self._config_pages_ordered = True diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index b6acbbd0a7e..6e4cf8d9636 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -8,7 +8,7 @@ from qtpy.QtCore import QSize, Qt, Signal, Slot from qtpy.QtGui import QFontMetrics from qtpy.QtWidgets import ( - QDialog, QDialogButtonBox, QGridLayout, QHBoxLayout, QListView, + QDialog, QDialogButtonBox, QFrame, QGridLayout, QHBoxLayout, QListView, QListWidget, QListWidgetItem, QPushButton, QScrollArea, QStackedWidget, QVBoxLayout, QWidget) @@ -38,8 +38,8 @@ class ConfigDialog(QDialog, SpyderFontsMixin): sig_reset_preferences_requested = Signal() # Constants - ITEMS_MARGIN = 9 - ITEMS_PADDING = 6 + ITEMS_MARGIN = 6 + ITEMS_PADDING = 3 CONTENTS_MIN_WIDTH = 300 ICON_SIZE = 24 @@ -112,7 +112,7 @@ def __init__(self, parent=None): self.button_reset.clicked.connect(self.sig_reset_preferences_requested) self.pages_widget.currentChanged.connect(self.current_page_changed) self.contents_widget.currentRowChanged.connect( - self.pages_widget.setCurrentIndex) + self.pages_widget.setCurrentIndex) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) bbox.clicked.connect(self.button_clicked) @@ -131,12 +131,14 @@ def set_current_index(self, index): def get_page(self, index=None): """Return page widget""" if index is None: - widget = self.pages_widget.currentWidget() + page = self.pages_widget.currentWidget() else: - widget = self.pages_widget.widget(index) + page = self.pages_widget.widget(index) - if widget: - return widget.widget() + # Not all pages are config pages (e.g. separators have a simple QWidget + # as their config page). So, we need to check for this. + if page and hasattr(page, 'widget'): + return page.widget() def get_index_by_name(self, name): """Return page index by CONF_SECTION name.""" @@ -159,9 +161,17 @@ def accept(self): """Reimplement Qt method""" for index in range(self.pages_widget.count()): configpage = self.get_page(index) + + # This can be the case for separators, which doesn't have a config + # page. + if configpage is None: + continue + if not configpage.is_valid(): return + configpage.apply_changes() + QDialog.accept(self) def button_clicked(self, button): @@ -177,6 +187,21 @@ def current_page_changed(self, index): self.apply_btn.setVisible(widget.apply_callback is not None) self.apply_btn.setEnabled(widget.is_modified) + def add_separator(self): + """Add a horizontal line to separate different sections.""" + # Solution taken from https://stackoverflow.com/a/24819554/438386 + item = QListWidgetItem(self.contents_widget) + item.setFlags(Qt.NoItemFlags) + item.setSizeHint(QSize(9, 9)) + + hline = QFrame(self.contents_widget) + hline.setFrameShape(QFrame.HLine) + self.contents_widget.setItemWidget(item, hline) + + # This is necessary to keep in sync the contents_widget and + # pages_widget indexes. + self.pages_widget.addWidget(QWidget(self)) + def add_page(self, page): # Signals self.check_settings.connect(page.check_settings) diff --git a/spyder/plugins/preferences/widgets/container.py b/spyder/plugins/preferences/widgets/container.py index 022d4400651..4ff82e76e79 100644 --- a/spyder/plugins/preferences/widgets/container.py +++ b/spyder/plugins/preferences/widgets/container.py @@ -10,16 +10,16 @@ from qtpy import PYSIDE2 # Local imports +from spyder.api.plugin_registration.registry import PreferencesAdapter from spyder.api.translations import _ from spyder.api.widgets.main_container import PluginMainContainer +from spyder.plugins.preferences.api import ( + MOST_IMPORTANT_PAGES, + PreferencesActions +) from spyder.plugins.preferences.widgets.configdialog import ConfigDialog -class PreferencesActions: - Show = 'show_action' - Reset = 'reset_action' - - class PreferencesContainer(PluginMainContainer): sig_reset_preferences_requested = Signal() """Request a reset of preferences.""" @@ -54,6 +54,10 @@ def _dialog_finished(result_code): dlg.resize(*self._dialog_size) for page_name in config_pages: + # Add separator before the Plugins page + if page_name == PreferencesAdapter.NAME: + dlg.add_separator() + (api, ConfigPage, plugin) = config_pages[page_name] if api == 'new': page = ConfigPage(plugin, dlg) @@ -67,6 +71,11 @@ def _dialog_finished(result_code): page.add_tab(Tab) dlg.add_page(page) + # Add separator after the last element of the most important + # pages + if page_name == MOST_IMPORTANT_PAGES[-1]: + dlg.add_separator() + dlg.set_current_index(self.dialog_index) dlg.show() dlg.check_all_settings() From c7894fcb9ebf5eee2bb8723df497f40be864868e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 12 Sep 2023 10:29:30 -0500 Subject: [PATCH 18/34] Preferences: Improve how we add tooltips to contents_widget elided items Also, reduce the size of that area. --- .../preferences/widgets/configdialog.py | 134 ++++++++++++++---- spyder/utils/stylesheet.py | 3 + 2 files changed, 107 insertions(+), 30 deletions(-) diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index 6e4cf8d9636..f217d12b93b 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -4,9 +4,12 @@ # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) +# Standard library imports +import sys + # Third party imports from qtpy.QtCore import QSize, Qt, Signal, Slot -from qtpy.QtGui import QFontMetrics +from qtpy.QtGui import QFontMetricsF from qtpy.QtWidgets import ( QDialog, QDialogButtonBox, QFrame, QGridLayout, QHBoxLayout, QListView, QListWidget, QListWidgetItem, QPushButton, QScrollArea, QStackedWidget, @@ -18,7 +21,7 @@ from spyder.config.manager import CONF from spyder.utils.icon_manager import ima from spyder.utils.palette import QStylePalette -from spyder.utils.stylesheet import SPECIAL_TABBAR_STYLESHEET +from spyder.utils.stylesheet import AppStyle, SPECIAL_TABBAR_STYLESHEET class PageScrollArea(QScrollArea): @@ -38,10 +41,10 @@ class ConfigDialog(QDialog, SpyderFontsMixin): sig_reset_preferences_requested = Signal() # Constants - ITEMS_MARGIN = 6 - ITEMS_PADDING = 3 - CONTENTS_MIN_WIDTH = 300 - ICON_SIZE = 24 + ITEMS_MARGIN = 2 * AppStyle.MarginSize + ITEMS_PADDING = AppStyle.MarginSize + CONTENTS_MAX_WIDTH = 230 + ICON_SIZE = 20 def __init__(self, parent=None): QDialog.__init__(self, parent) @@ -49,7 +52,9 @@ def __init__(self, parent=None): # Attributes self.main = parent self.items_font = self.get_font( - SpyderFontType.Interface, font_size_delta=1) + SpyderFontType.Interface, font_size_delta=1 + ) + self._is_shown = False # Widgets self.pages_widget = QStackedWidget(self) @@ -75,9 +80,9 @@ def __init__(self, parent=None): self.contents_widget.setMovement(QListView.Static) self.contents_widget.setSpacing(3) self.contents_widget.setCurrentRow(0) - self.contents_widget.setMinimumWidth(self.CONTENTS_MIN_WIDTH) self.contents_widget.setObjectName('configdialog-contents') self.contents_widget.setIconSize(QSize(self.ICON_SIZE, self.ICON_SIZE)) + self.contents_widget.setFixedWidth(self.CONTENTS_MAX_WIDTH) # Don't show horizontal scrollbar because it doesn't look good. Instead # we show tooltips if the text doesn't fit in contents_widget width. @@ -106,7 +111,8 @@ def __init__(self, parent=None): self.setLayout(layout) # Stylesheet - self.setStyleSheet(self._stylesheet) + self._css = self._generate_stylesheet() + self.setStyleSheet(self._css.toString()) # Signals and slots self.button_reset.clicked.connect(self.sig_reset_preferences_requested) @@ -120,6 +126,8 @@ def __init__(self, parent=None): # Ensures that the config is present on spyder first run CONF.set('main', 'interface_language', load_lang_conf()) + # ---- Public API + # ------------------------------------------------------------------------- def get_current_index(self): """Return current page index""" return self.contents_widget.currentRow() @@ -244,29 +252,31 @@ def add_page(self, page): # Set font for items item.setFont(self.items_font) - # Check if it's necessary to add a tooltip to the item - text_width = QFontMetrics(self.items_font).size( - Qt.TextSingleLine, page.get_name() - ).width() - - actual_width = ( - text_width + 2 * self.ITEMS_MARGIN + self.ICON_SIZE + - self.ITEMS_PADDING - ) - - available_width = ( - self.CONTENTS_MIN_WIDTH - - (2 * self.ITEMS_MARGIN + self.ICON_SIZE + self.ITEMS_PADDING) - ) - - if actual_width > available_width: - item.setToolTip(page.get_name()) - def check_all_settings(self): """This method is called to check all configuration page settings after configuration dialog has been shown""" self.check_settings.emit() + # ---- Qt methods + # ------------------------------------------------------------------------- + def showEvent(self, event): + """Adjustments when the widget is shown.""" + if not self._is_shown: + self._add_tooltips() + self._adjust_items_margin() + + self._is_shown = True + + super().showEvent(event) + + # This is necessary to paint the separators as expected when there + # are elided items in contents_widget. + self.blockSignals(True) + height = self.height() + self.resize(self.width(), height + 1) + self.resize(self.width(), height - 1) + self.blockSignals(False) + def resizeEvent(self, event): """ Reimplement Qt method to be able to save the widget's size from the @@ -275,8 +285,72 @@ def resizeEvent(self, event): QDialog.resizeEvent(self, event) self.sig_size_changed.emit(self.size()) - @property - def _stylesheet(self): + # ---- Private API + # ------------------------------------------------------------------------- + def _add_tooltips(self): + """ + Check if it's necessary to add tooltips to the contents_widget items. + """ + contents_width = self.contents_widget.width() + metrics = QFontMetricsF(self.items_font) + + for i in range(self.contents_widget.count()): + item = self.contents_widget.item(i) + + # Item width + item_width = self.contents_widget.visualItemRect(item).width() + + # Set tooltip + if item_width >= contents_width: + item.setToolTip(item.text()) + else: + # This covers the case when item_width is too close to + # contents_width without the scrollbar being visible, which + # can't be detected by Qt with the check above. + scrollbar = self.contents_widget.verticalScrollBar() + + if scrollbar.isVisible(): + if sys.platform == 'darwin': + # This is a crude heuristic to detect if we need to add + # tooltips on Mac. However, it's the best we can do + # (the approach for other OSes below ends up adding + # tooltips to all items) and it works for all our + # localized languages. + text_width = metrics.boundingRect(item.text()).width() + if text_width + 70 > item_width - 5: + item.setToolTip(item.text()) + else: + if item_width > (contents_width - scrollbar.width()): + item.setToolTip(item.text()) + + def _adjust_items_margin(self): + """ + Adjust margins of contents_widget items depending on if its vertical + scrollbar is visible. + + Notes + ----- + We need to do this only in Mac because Qt doesn't account for the + scrollbar width in most widgets. + """ + if sys.platform == 'darwin': + scrollbar = self.contents_widget.verticalScrollBar() + extra_margin = ( + AppStyle.MacScrollBarWidth if scrollbar.isVisible() else 0 + ) + item_margin = ( + f'0px {self.ITEMS_MARGIN + extra_margin}px ' + f'0px {self.ITEMS_MARGIN}px' + ) + + self._css['QListView#configdialog-contents::item'].setValues( + margin=item_margin + ) + + self.setStyleSheet(self._css.toString()) + + def _generate_stylesheet(self): + """Generate stylesheet for this widget as qstylizer object.""" # Use special tabbar stylesheet for as the base one and then extend it. tabs_stylesheet = SPECIAL_TABBAR_STYLESHEET.get_copy() css = tabs_stylesheet.get_stylesheet() @@ -332,4 +406,4 @@ def _stylesheet(self): border='0px', ) - return css.toString() + return css diff --git a/spyder/utils/stylesheet.py b/spyder/utils/stylesheet.py index d4fa15f9a37..fb5d697f803 100644 --- a/spyder/utils/stylesheet.py +++ b/spyder/utils/stylesheet.py @@ -47,6 +47,9 @@ class AppStyle: FindMinWidth = 400 FindHeight = 26 + # To have it for quick access because it's needed a lot in Mac + MacScrollBarWidth = 16 + # ============================================================================= # ---- Base stylesheet class From f7062b0360a5e1f495da6d8e1c918c8a27f54ffc Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 12 Sep 2023 13:03:15 -0500 Subject: [PATCH 19/34] Preferences: Move Qt method to the right section --- .../preferences/widgets/configdialog.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index f217d12b93b..6ea42060346 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -164,24 +164,6 @@ def get_index_by_name(self, name): else: return None - @Slot() - def accept(self): - """Reimplement Qt method""" - for index in range(self.pages_widget.count()): - configpage = self.get_page(index) - - # This can be the case for separators, which doesn't have a config - # page. - if configpage is None: - continue - - if not configpage.is_valid(): - return - - configpage.apply_changes() - - QDialog.accept(self) - def button_clicked(self, button): if button is self.apply_btn: # Apply button was clicked @@ -259,6 +241,24 @@ def check_all_settings(self): # ---- Qt methods # ------------------------------------------------------------------------- + @Slot() + def accept(self): + """Reimplement Qt method""" + for index in range(self.pages_widget.count()): + configpage = self.get_page(index) + + # This can be the case for separators, which doesn't have a config + # page. + if configpage is None: + continue + + if not configpage.is_valid(): + return + + configpage.apply_changes() + + QDialog.accept(self) + def showEvent(self, event): """Adjustments when the widget is shown.""" if not self._is_shown: From 678d6bb381b37af75b3277a92da9a7289175e9fc Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 16 Sep 2023 20:58:30 -0400 Subject: [PATCH 20/34] Widgets: Fix bug in ElementsTable when hovering This happened only when the table has no widgets --- spyder/widgets/elementstable.py | 41 ++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/spyder/widgets/elementstable.py b/spyder/widgets/elementstable.py index f9caf7c819a..6f5433e4929 100644 --- a/spyder/widgets/elementstable.py +++ b/spyder/widgets/elementstable.py @@ -155,16 +155,16 @@ def __init__(self, parent: Optional[QWidget], elements: List[Element]): self.elements = elements # Check for additional features - with_icons = self._with_feature('icon') - with_addtional_info = self._with_feature('additional_info') - with_widgets = self._with_feature('widget') + self._with_icons = self._with_feature('icon') + self._with_addtional_info = self._with_feature('additional_info') + self._with_widgets = self._with_feature('widget') # To keep track of the current row widget (e.g. a checkbox) in order to # change its background color when its row is hovered. self._current_row = -1 self._current_row_widget = None - # To do adjustments when the widget is shown only once + # To make adjustments when the widget is shown self._is_shown = False # This is used to paint the entire row's background color when its @@ -173,7 +173,11 @@ def __init__(self, parent: Optional[QWidget], elements: List[Element]): # Set model self.model = ElementsModel( - self, self.elements, with_icons, with_addtional_info, with_widgets + self, + self.elements, + self._with_icons, + self._with_addtional_info, + self._with_widgets ) self.setModel(self.model) @@ -186,7 +190,7 @@ def __init__(self, parent: Optional[QWidget], elements: List[Element]): # Adjustments for the additional info column self._info_column_width = 0 - if with_addtional_info: + if self._with_addtional_info: info_delegate = HTMLDelegate(self, margin=10, align_vcenter=True) self.setItemDelegateForColumn( self.model.columns['additional_info'], info_delegate) @@ -201,7 +205,7 @@ def __init__(self, parent: Optional[QWidget], elements: List[Element]): # Adjustments for the widgets column self._widgets_column_width = 0 - if with_widgets: + if self._with_widgets: widgets_delegate = HTMLDelegate(self, margin=0) self.setItemDelegateForColumn( self.model.columns['widgets'], widgets_delegate) @@ -244,7 +248,7 @@ def __init__(self, parent: Optional[QWidget], elements: List[Element]): self.verticalHeader().hide() # Set icons size - if with_icons: + if self._with_icons: self.setIconSize(QSize(32, 32)) # Hide grid to only paint horizontal lines with css @@ -265,18 +269,19 @@ def _on_hover_index_changed(self, index): if row != self._current_row: self._current_row = row - # Remove background color of previous row widget - if self._current_row_widget is not None: - self._current_row_widget.setStyleSheet("") + if self._with_widgets: + # Remove background color of previous row widget + if self._current_row_widget is not None: + self._current_row_widget.setStyleSheet("") - # Set background for the new row widget - new_row_widget = self.elements[row]["row_widget"] - new_row_widget.setStyleSheet( - f"background-color: {QStylePalette.COLOR_BACKGROUND_3}" - ) + # Set background for the new row widget + new_row_widget = self.elements[row]["row_widget"] + new_row_widget.setStyleSheet( + f"background-color: {QStylePalette.COLOR_BACKGROUND_3}" + ) - # Set new current row widget - self._current_row_widget = new_row_widget + # Set new current row widget + self._current_row_widget = new_row_widget def _set_stylesheet(self, leave=False): """Set stylesheet when entering or leaving the widget.""" From 9fd5d0a70fd31a40f403968b2e7c8ae756b00a14 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 16 Sep 2023 21:12:33 -0400 Subject: [PATCH 21/34] Widgets: Fix right margin of ElementsTable last column on Mac --- spyder/widgets/elementstable.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/spyder/widgets/elementstable.py b/spyder/widgets/elementstable.py index 6f5433e4929..5b1d0b74c49 100644 --- a/spyder/widgets/elementstable.py +++ b/spyder/widgets/elementstable.py @@ -10,6 +10,7 @@ """ # Standard library imports +import sys from typing import List, Optional, TypedDict # Third-party imports @@ -22,6 +23,7 @@ from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType from spyder.utils.icon_manager import ima from spyder.utils.palette import QStylePalette +from spyder.utils.stylesheet import AppStyle from spyder.widgets.helperwidgets import HoverRowsTableView, HTMLDelegate @@ -302,12 +304,25 @@ def _set_layout(self): This is necessary to make the table look good at different sizes. """ + # We need to make these extra adjustments for Mac so that the last + # column is not too close to the right border + extra_width = 0 + if sys.platform == 'darwin': + if self.verticalScrollBar().isVisible(): + extra_width = ( + AppStyle.MacScrollBarWidth + + (15 if self._with_widgets else 5) + ) + else: + extra_width = 10 if self._with_widgets else 5 + # Resize title column so that the table fits into the available # horizontal space. if self._info_column_width > 0 or self._widgets_column_width > 0: title_column_width = ( self.horizontalHeader().size().width() - - (self._info_column_width + self._widgets_column_width) + (self._info_column_width + self._widgets_column_width + + extra_width) ) self.horizontalHeader().resizeSection( From f501efd4ec3a9f607d3d92551fa65628f553dd9d Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 16 Sep 2023 21:46:41 -0400 Subject: [PATCH 22/34] Widgets: Remove setting ElementsTable layout on QEvent.LayoutRequest - It seems to not be necessary anymore. - Besides, it was being called on enter and leave events, which was a waste of resources. --- spyder/widgets/elementstable.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/spyder/widgets/elementstable.py b/spyder/widgets/elementstable.py index 5b1d0b74c49..cf7a4408eab 100644 --- a/spyder/widgets/elementstable.py +++ b/spyder/widgets/elementstable.py @@ -15,7 +15,7 @@ # Third-party imports import qstylizer.style -from qtpy.QtCore import QAbstractTableModel, QEvent, QModelIndex, QSize, Qt +from qtpy.QtCore import QAbstractTableModel, QModelIndex, QSize, Qt from qtpy.QtGui import QIcon from qtpy.QtWidgets import QAbstractItemView, QCheckBox, QHBoxLayout, QWidget @@ -372,13 +372,6 @@ def resizeEvent(self, event): self._set_layout() super().resizeEvent(event) - def event(self, event): - # This is necessary to readjust the layout when the parent widget is - # maximized. - if event.type() == QEvent.LayoutRequest: - self._set_layout() - return super().event(event) - def test_elements_table(): from spyder.utils.qthelpers import qapplication From fd694e8febe2796c766dcf1f9d8de98e4c87fac3 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 18 Sep 2023 11:59:37 -0500 Subject: [PATCH 23/34] Preferences: Standardize how tabs are created in config pages - Now it's only necessary to call the create_tab method of SpyderConfigPage to add tabs. Creating the tab widget and setting its layout is done automatically by that method. - This will allow us to easily control the properties of those tab widgets in a single place. --- spyder/plugins/application/confpage.py | 26 ++++---- spyder/plugins/editor/confpage.py | 26 +++----- spyder/plugins/explorer/confpage.py | 15 ++--- spyder/plugins/ipythonconsole/confpage.py | 34 +++++------ .../preferences/widgets/config_widgets.py | 59 +++++++++++++------ .../plugins/preferences/widgets/container.py | 4 +- spyder/plugins/run/confpage.py | 11 +--- 7 files changed, 84 insertions(+), 91 deletions(-) diff --git a/spyder/plugins/application/confpage.py b/spyder/plugins/application/confpage.py index 11981e1ff43..b55a2083f58 100644 --- a/spyder/plugins/application/confpage.py +++ b/spyder/plugins/application/confpage.py @@ -18,8 +18,8 @@ from qtpy.compat import from_qvariant from qtpy.QtCore import Qt from qtpy.QtWidgets import (QApplication, QButtonGroup, QGridLayout, QGroupBox, - QHBoxLayout, QLabel, QMessageBox, QTabWidget, - QVBoxLayout, QWidget) + QHBoxLayout, QLabel, QMessageBox, QVBoxLayout, + QWidget) from spyder.config.base import (_, DISABLED_LANGUAGES, LANGUAGE_CODES, is_conda_based_app, save_lang_conf) @@ -236,21 +236,19 @@ def set_open_file(state): screen_resolution_layout.addLayout(screen_resolution_inner_layout) screen_resolution_group.setLayout(screen_resolution_layout) + if sys.platform == "darwin" and not is_conda_based_app(): - interface_tab = self.create_tab(screen_resolution_group, - interface_group, macOS_group) + self.create_tab( + _("Interface"), + [screen_resolution_group, interface_group, macOS_group] + ) else: - interface_tab = self.create_tab(screen_resolution_group, - interface_group) - - self.tabs = QTabWidget() - self.tabs.addTab(interface_tab, _("Interface")) - self.tabs.addTab(self.create_tab(advanced_widget), - _("Advanced settings")) + self.create_tab( + _("Interface"), + [screen_resolution_group, interface_group] + ) - vlayout = QVBoxLayout() - vlayout.addWidget(self.tabs) - self.setLayout(vlayout) + self.create_tab(_("Advanced settings"), advanced_widget) def apply_settings(self, options): if 'high_dpi_custom_scale_factors' in options: diff --git a/spyder/plugins/editor/confpage.py b/spyder/plugins/editor/confpage.py index 4a6afc7acf9..7358ccfaf5b 100644 --- a/spyder/plugins/editor/confpage.py +++ b/spyder/plugins/editor/confpage.py @@ -7,7 +7,7 @@ """Editor config page.""" from qtpy.QtWidgets import (QGridLayout, QGroupBox, QHBoxLayout, QLabel, - QTabWidget, QVBoxLayout) + QVBoxLayout) from spyder.api.config.decorators import on_conf_change from spyder.api.config.mixins import SpyderConfigurationObserver @@ -333,27 +333,19 @@ def enable_tabwidth_spin(index): eol_group.setLayout(eol_layout) # --- Tabs --- - self.tabs = QTabWidget(self) - self.tabs.addTab( - self.create_tab(display_group, highlight_group, other_group), - _("Interface") + self.create_tab( + _("Interface"), + [display_group, highlight_group, other_group] ) - self.tabs.addTab( - self.create_tab(automatic_group, indentation_group), - _("Source code") - ) + self.create_tab(_("Source code"), [automatic_group, indentation_group]) - self.tabs.addTab( - self.create_tab(templates_group, autosave_group, docstring_group, - annotations_group, eol_group), - _("Advanced settings") + self.create_tab( + _("Advanced settings"), + [templates_group, autosave_group, docstring_group, + annotations_group, eol_group] ) - vlayout = QVBoxLayout() - vlayout.addWidget(self.tabs) - self.setLayout(vlayout) - @on_conf_change( option=('provider_configuration', 'lsp', 'values', 'format_on_save'), section='completions' diff --git a/spyder/plugins/explorer/confpage.py b/spyder/plugins/explorer/confpage.py index e52a1e5994c..5e9bd9cb969 100644 --- a/spyder/plugins/explorer/confpage.py +++ b/spyder/plugins/explorer/confpage.py @@ -7,8 +7,8 @@ """File explorer configuration page.""" # Third party imports -from qtpy.QtWidgets import (QTabWidget, QVBoxLayout, QWidget, QGroupBox, - QLabel, QPushButton) +from qtpy.QtWidgets import (QGroupBox, QLabel, QPushButton, QVBoxLayout, + QWidget) # Local imports from spyder.api.preferences import PluginConfigPage @@ -86,15 +86,8 @@ def setup_page(self): layout_file.addWidget(self.edit_file_associations) associations_widget.setLayout(layout_file) - self.tabs = QTabWidget() - self.tabs.addTab(self.create_tab(general_widget), _("General")) - self.tabs.addTab(self.create_tab(associations_widget), - _("File associations")) - - tab_layout = QVBoxLayout() - tab_layout.addWidget(self.tabs) - - self.setLayout(tab_layout) + self.create_tab(_("General"), general_widget) + self.create_tab(_("File associations"), associations_widget) # Signals file_associations.sig_data_changed.connect(self.update_associations) diff --git a/spyder/plugins/ipythonconsole/confpage.py b/spyder/plugins/ipythonconsole/confpage.py index 0dbf0633a61..e0f70b95305 100644 --- a/spyder/plugins/ipythonconsole/confpage.py +++ b/spyder/plugins/ipythonconsole/confpage.py @@ -12,7 +12,7 @@ # Third party imports from qtpy.QtCore import Qt from qtpy.QtWidgets import (QGridLayout, QGroupBox, QHBoxLayout, QLabel, - QTabWidget, QVBoxLayout) + QVBoxLayout) # Local imports from spyder.api.translations import _ @@ -368,29 +368,23 @@ def setup_page(self): windows_group.setLayout(windows_layout) # --- Tabs organization --- - self.tabs = QTabWidget(self) - self.tabs.addTab( - self.create_tab(display_group, confirmations_group, comp_group, - source_code_group), - _("Interface") + self.create_tab( + _("Interface"), + [display_group, confirmations_group, comp_group, source_code_group] ) - self.tabs.addTab( - self.create_tab(pylab_group, backend_group, inline_group), - _("Graphics") + self.create_tab( + _("Graphics"), + [pylab_group, backend_group, inline_group] ) - self.tabs.addTab( - self.create_tab(run_lines_group, run_file_group), - _("Startup") + self.create_tab( + _("Startup"), + [run_lines_group, run_file_group] ) - self.tabs.addTab( - self.create_tab(jedi_group, greedy_group, autocall_group, - sympy_group, prompts_group, windows_group), - _("Advanced settings") + self.create_tab( + _("Advanced settings"), + [jedi_group, greedy_group, autocall_group, sympy_group, + prompts_group, windows_group] ) - - vlayout = QVBoxLayout() - vlayout.addWidget(self.tabs) - self.setLayout(vlayout) diff --git a/spyder/plugins/preferences/widgets/config_widgets.py b/spyder/plugins/preferences/widgets/config_widgets.py index b1acbcfa11b..ae0fa52bb9f 100644 --- a/spyder/plugins/preferences/widgets/config_widgets.py +++ b/spyder/plugins/preferences/widgets/config_widgets.py @@ -889,16 +889,41 @@ def create_button(self, text, callback): self.CONF_SECTION, opt)) return btn - def create_tab(self, *widgets): - """Create simple tab widget page: widgets added in a vertical layout""" - widget = QWidget(self) + def create_tab(self, name, widgets): + """ + Create a tab widget page. + + Parameters + ---------- + name: str + Name of the tab + widgets: list or QWidget + List of widgets to add to the tab. This can be also a single + widget. + + Notes + ----- + * Widgets are added in a vertical layout. + """ + if self.tabs is None: + self.tabs = QTabWidget(self) + + vlayout = QVBoxLayout() + vlayout.addWidget(self.tabs) + self.setLayout(vlayout) + + if not isinstance(widgets, list): + widgets = [widgets] + + tab = QWidget(self) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) - for widg in widgets: - layout.addWidget(widg) + for w in widgets: + layout.addWidget(w) layout.addStretch(1) - widget.setLayout(layout) - return widget + tab.setLayout(layout) + + self.tabs.addTab(tab, name) def prompt_restart_required(self): """Prompt the user with a request to restart.""" @@ -922,24 +947,20 @@ def restart(self): """Restart Spyder.""" self.main.restart(close_immediately=True) - def add_tab(self, Widget): + def _add_tab(self, Widget): widget = Widget(self) + if self.tabs is None: # In case a preference page does not have any tabs, we need to # add a tab with the widgets that already exist and then add the # new tab. - self.tabs = QTabWidget() layout = self.layout() - main_widget = QWidget() + main_widget = QWidget(self) main_widget.setLayout(layout) - self.tabs.addTab(self.create_tab(main_widget), - _('General')) - self.tabs.addTab(self.create_tab(widget), - Widget.TITLE) - vlayout = QVBoxLayout() - vlayout.addWidget(self.tabs) - self.setLayout(vlayout) + + self.create_tab(_('General'), main_widget) + self.create_tab(Widget.TITLE, widget) else: - self.tabs.addTab(self.create_tab(widget), - Widget.TITLE) + self.create_tab(Widget.TITLE, widget) + self.load_from_conf() diff --git a/spyder/plugins/preferences/widgets/container.py b/spyder/plugins/preferences/widgets/container.py index 4ff82e76e79..261cc04bf35 100644 --- a/spyder/plugins/preferences/widgets/container.py +++ b/spyder/plugins/preferences/widgets/container.py @@ -63,12 +63,12 @@ def _dialog_finished(result_code): page = ConfigPage(plugin, dlg) page.initialize() for Tab in config_tabs.get(page_name, []): - page.add_tab(Tab) + page._add_tab(Tab) dlg.add_page(page) else: page = plugin._create_configwidget(dlg, main_window) for Tab in config_tabs.get(page_name, []): - page.add_tab(Tab) + page._add_tab(Tab) dlg.add_page(page) # Add separator after the last element of the most important diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index 05427c42aba..c661c4fcf4d 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -16,7 +16,7 @@ from qtpy.QtCore import Qt from qtpy.QtWidgets import (QGroupBox, QLabel, QVBoxLayout, QComboBox, QTableView, QAbstractItemView, QPushButton, - QGridLayout, QHeaderView, QTabWidget, QWidget) + QGridLayout, QHeaderView, QWidget) # Local imports from spyder.api.preferences import PluginConfigPage @@ -239,13 +239,8 @@ def setup_page(self): executor_widget = QWidget() executor_widget.setLayout(vlayout) - self.tabs = QTabWidget() - self.tabs.addTab(self.create_tab(executor_widget), _("Run executors")) - self.tabs.addTab( - self.create_tab(run_widget), _("Editor interactions")) - main_layout = QVBoxLayout() - main_layout.addWidget(self.tabs) - self.setLayout(main_layout) + self.create_tab(_("Run executors"), executor_widget) + self.create_tab(_("Editor interactions"), run_widget) def executor_index_changed(self, index: int): # Save previous executor configuration From 175953dad2d1201876267d615028f7990e183b99 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 18 Sep 2023 12:02:17 -0500 Subject: [PATCH 24/34] Set scroll buttons and prevent eliding text on dock and preference tabs - This is necessary for Mac. - Also, fix style of tabbar scroll buttons on Windows and Mac. --- spyder/plugins/preferences/widgets/config_widgets.py | 2 ++ spyder/plugins/preferences/widgets/configdialog.py | 10 +++++++++- spyder/utils/stylesheet.py | 5 +++-- spyder/widgets/dock.py | 1 + 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/spyder/plugins/preferences/widgets/config_widgets.py b/spyder/plugins/preferences/widgets/config_widgets.py index ae0fa52bb9f..5e33731f27a 100644 --- a/spyder/plugins/preferences/widgets/config_widgets.py +++ b/spyder/plugins/preferences/widgets/config_widgets.py @@ -907,6 +907,8 @@ def create_tab(self, name, widgets): """ if self.tabs is None: self.tabs = QTabWidget(self) + self.tabs.setUsesScrollButtons(True) + self.tabs.setElideMode(Qt.ElideNone) vlayout = QVBoxLayout() vlayout.addWidget(self.tabs) diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index 6ea42060346..054b46405ee 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -21,7 +21,8 @@ from spyder.config.manager import CONF from spyder.utils.icon_manager import ima from spyder.utils.palette import QStylePalette -from spyder.utils.stylesheet import AppStyle, SPECIAL_TABBAR_STYLESHEET +from spyder.utils.stylesheet import ( + AppStyle, MAC, SPECIAL_TABBAR_STYLESHEET, WIN) class PageScrollArea(QScrollArea): @@ -360,6 +361,13 @@ def _generate_stylesheet(self): fontSize=f'{self.items_font.pointSize()}pt', ) + # Make tabbar scroll button a bit bigger on Windows and Mac (this has + # no effect on Linux). + if WIN or MAC: + css['QTabBar QToolButton'].setValues( + padding=f'{tabs_stylesheet.SCROLL_BUTTONS_PADDING - 1}px', + ) + # Increase padding around text a bit because we're using an larger font css['QTabBar::tab'].setValues( padding='6px 10px', diff --git a/spyder/utils/stylesheet.py b/spyder/utils/stylesheet.py index fb5d697f803..1bad5ab49a6 100644 --- a/spyder/utils/stylesheet.py +++ b/spyder/utils/stylesheet.py @@ -482,7 +482,7 @@ def set_stylesheet(self): # Make scroll button icons smaller on Windows and Mac if WIN or MAC: css[f'QTabBar{self.OBJECT_NAME} QToolButton'].setValues( - padding='7px', + padding=f'{5 if WIN else 7}px', ) @@ -490,6 +490,7 @@ class BaseDockTabBarStyleSheet(BaseTabBarStyleSheet): """Base style for dockwidget tabbars.""" SCROLL_BUTTONS_BORDER_WIDTH = '2px' + SCROLL_BUTTONS_PADDING = 7 if WIN else 9 def set_stylesheet(self): super().set_stylesheet() @@ -525,7 +526,7 @@ def set_stylesheet(self): # Make scroll button icons smaller on Windows and Mac if WIN or MAC: css['QTabBar QToolButton'].setValues( - padding='5px', + padding=f'{self.SCROLL_BUTTONS_PADDING}px', ) diff --git a/spyder/widgets/dock.py b/spyder/widgets/dock.py index 181c7982542..8a53c0e9c6b 100644 --- a/spyder/widgets/dock.py +++ b/spyder/widgets/dock.py @@ -38,6 +38,7 @@ def __init__(self, dock_tabbar, main): self._set_tabbar_stylesheet() self.dock_tabbar.setElideMode(Qt.ElideNone) + self.dock_tabbar.setUsesScrollButtons(True) def eventFilter(self, obj, event): """Filter mouse press events. From 5d4c23d484a70a65a1dffdc44f7ab0f777901092 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 21 Oct 2023 12:20:42 -0500 Subject: [PATCH 25/34] Testing: Fix tests related to Preferences --- spyder/app/tests/conftest.py | 6 +++--- .../plugins/maininterpreter/tests/test_confpage.py | 2 +- spyder/plugins/preferences/plugin.py | 6 ++++-- spyder/plugins/preferences/tests/conftest.py | 4 ++-- spyder/plugins/preferences/widgets/configdialog.py | 12 ++++++++---- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/spyder/app/tests/conftest.py b/spyder/app/tests/conftest.py index a73f377906d..8ca00b58d80 100755 --- a/spyder/app/tests/conftest.py +++ b/spyder/app/tests/conftest.py @@ -224,14 +224,14 @@ def preferences_dialog_helper(qtbot, main_window, section): shell = main_window.ipyconsole.get_current_shellwidget() qtbot.waitUntil( lambda: shell.spyder_kernel_ready and shell._prompt_html is not None, - timeout=SHELL_TIMEOUT) + timeout=SHELL_TIMEOUT + ) main_window.show_preferences() preferences = main_window.preferences container = preferences.get_container() - qtbot.waitUntil(lambda: container.dialog is not None, - timeout=5000) + qtbot.waitUntil(lambda: container.dialog is not None, timeout=5000) dlg = container.dialog index = dlg.get_index_by_name(section) page = dlg.get_page(index) diff --git a/spyder/plugins/maininterpreter/tests/test_confpage.py b/spyder/plugins/maininterpreter/tests/test_confpage.py index 57f5012b502..38653648b60 100644 --- a/spyder/plugins/maininterpreter/tests/test_confpage.py +++ b/spyder/plugins/maininterpreter/tests/test_confpage.py @@ -46,7 +46,7 @@ def test_load_time(qtbot): # Create page and measure time to do it t0 = time.time() - preferences.open_dialog(None) + preferences.open_dialog() load_time = time.time() - t0 container = preferences.get_container() diff --git a/spyder/plugins/preferences/plugin.py b/spyder/plugins/preferences/plugin.py index cb2d5c42013..25a9d5a642e 100644 --- a/spyder/plugins/preferences/plugin.py +++ b/spyder/plugins/preferences/plugin.py @@ -28,7 +28,7 @@ from spyder.api.plugin_registration.decorators import ( on_plugin_available, on_plugin_teardown) from spyder.api.plugin_registration.registry import PreferencesAdapter -from spyder.config.base import _ +from spyder.config.base import _, running_under_pytest from spyder.config.main import CONF_VERSION from spyder.config.user import NoDefault from spyder.plugins.mainmenu.api import ApplicationMenus, ToolsMenuSections @@ -281,7 +281,9 @@ def open_dialog(self): self.before_long_process('') - self._reorder_config_pages() + if not running_under_pytest(): + self._reorder_config_pages() + container.create_dialog( self.config_pages, self.config_tabs, self.get_main() ) diff --git a/spyder/plugins/preferences/tests/conftest.py b/spyder/plugins/preferences/tests/conftest.py index 01832d75ab5..b3333176c45 100644 --- a/spyder/plugins/preferences/tests/conftest.py +++ b/spyder/plugins/preferences/tests/conftest.py @@ -121,7 +121,7 @@ def global_config_dialog(qtbot): qtbot.addWidget(mainwindow) preferences = Preferences(mainwindow, CONF) - preferences.open_dialog(None) + preferences.open_dialog() container = preferences.get_container() dlg = container.dialog @@ -140,7 +140,7 @@ def config_dialog(qtbot, request, mocker): qtbot.addWidget(main_ref) preferences = main_ref._main.get_plugin(Plugins.Preferences) - preferences.open_dialog(None) + preferences.open_dialog() container = preferences.get_container() dlg = container.dialog diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index 054b46405ee..629403937d6 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -152,13 +152,17 @@ def get_page(self, index=None): def get_index_by_name(self, name): """Return page index by CONF_SECTION name.""" for idx in range(self.pages_widget.count()): - widget = self.pages_widget.widget(idx) - widget = widget.widget() + page = self.get_page(idx) + + # This is the case for separators + if page is None: + continue + try: # New API - section = widget.plugin.NAME + section = page.plugin.NAME except AttributeError: - section = widget.CONF_SECTION + section = page.CONF_SECTION if section == name: return idx From 99356869e30ba666154debb8e28f6d75ab7b43e5 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 21 Oct 2023 12:41:40 -0500 Subject: [PATCH 26/34] Preferences: Set min height for config pages - This prevents text from being clipped when there's not enough space available. - Also, fix similar error in the editor config page. --- spyder/plugins/editor/confpage.py | 6 +++++- spyder/plugins/preferences/widgets/config_widgets.py | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/spyder/plugins/editor/confpage.py b/spyder/plugins/editor/confpage.py index 4f1e9d37008..4deba3f01ea 100644 --- a/spyder/plugins/editor/confpage.py +++ b/spyder/plugins/editor/confpage.py @@ -32,6 +32,7 @@ class EditorConfigPage(PluginConfigPage, SpyderConfigurationObserver): def __init__(self, plugin, parent): PluginConfigPage.__init__(self, plugin, parent) SpyderConfigurationObserver.__init__(self) + self.removetrail_box = None self.add_newline_box = None self.remove_trail_newline_box = None @@ -43,6 +44,9 @@ def get_icon(self): return ima.icon('edit') def setup_page(self): + # We need to set this so that the text of some options is not clipped + self.setMinimumHeight(670) + newcb = self.create_checkbox # --- Display tab --- @@ -306,7 +310,7 @@ def enable_tabwidth_spin(index): "mixed end-of-line characters (this may " "raise syntax errors in the consoles " "on Windows platforms), Spyder may fix the " - "file automatically.

")) + "file automatically.")) eol_label.setWordWrap(True) check_eol_box = newcb(_("Fix automatically and show warning " "message box"), diff --git a/spyder/plugins/preferences/widgets/config_widgets.py b/spyder/plugins/preferences/widgets/config_widgets.py index 5e33731f27a..77335122af0 100644 --- a/spyder/plugins/preferences/widgets/config_widgets.py +++ b/spyder/plugins/preferences/widgets/config_widgets.py @@ -144,6 +144,7 @@ class SpyderConfigPage(ConfigPage, ConfigAccessMixin): """Plugin configuration dialog box page widget""" CONF_SECTION = None MAX_WIDTH = 620 + MIN_HEIGHT = 550 def __init__(self, parent): ConfigPage.__init__( @@ -170,12 +171,13 @@ def __init__(self, parent): self.main = parent.main self.tabs = None - # Maximum width + # Set dimensions self.setMaximumWidth(self.MAX_WIDTH) + self.setMinimumHeight(self.MIN_HEIGHT) def sizeHint(self): """Default page size.""" - return QSize(self.MAX_WIDTH, 400) + return QSize(self.MAX_WIDTH, self.MIN_HEIGHT) def _apply_settings_tabs(self, options): if self.tabs is not None: From 189806eccd3a0fb1db767c000d5992cfc4f89b73 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 21 Oct 2023 20:56:31 -0400 Subject: [PATCH 27/34] Add superqt as a new dependency Also, improve description of some dependencies --- binder/environment.yml | 1 + requirements/main.yml | 1 + setup.py | 1 + spyder/dependencies.py | 10 +++++++--- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/binder/environment.yml b/binder/environment.yml index aad38f7b7ea..f0b65fee015 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -47,6 +47,7 @@ dependencies: - setuptools >=49.6.0 - sphinx >=0.6.6 - spyder-kernels >=3.0.0b2,<3.0.0b3 +- superqt >=0.6.1,<1.0.0 - textdistance >=4.2.0 - three-merge >=0.1.1 - watchdog >=0.10.3 diff --git a/requirements/main.yml b/requirements/main.yml index e07fd6ce6d4..40c1bb7266e 100644 --- a/requirements/main.yml +++ b/requirements/main.yml @@ -43,6 +43,7 @@ dependencies: - setuptools >=49.6.0 - sphinx >=0.6.6 - spyder-kernels >=3.0.0b2,<3.0.0b3 + - superqt >=0.6.1,<1.0.0 - textdistance >=4.2.0 - three-merge >=0.1.1 - watchdog >=0.10.3 diff --git a/setup.py b/setup.py index 4a019d307ca..05ca9c1543a 100644 --- a/setup.py +++ b/setup.py @@ -244,6 +244,7 @@ def run(self): 'setuptools>=49.6.0', 'sphinx>=0.6.6', 'spyder-kernels>=3.0.0b2,<3.0.0b3', + 'superqt>=0.6.1,<1.0.0', 'textdistance>=4.2.0', 'three-merge>=0.1.1', 'watchdog>=0.10.3' diff --git a/spyder/dependencies.py b/spyder/dependencies.py index 22258b30dff..a115eb63dd7 100644 --- a/spyder/dependencies.py +++ b/spyder/dependencies.py @@ -71,11 +71,11 @@ SETUPTOOLS_REQVER = '>=49.6.0' SPHINX_REQVER = '>=0.6.6' SPYDER_KERNELS_REQVER = '>=3.0.0b2,<3.0.0b3' +SUPERQT_REQVER = '>=0.6.1,<1.0.0' TEXTDISTANCE_REQVER = '>=4.2.0' THREE_MERGE_REQVER = '>=0.1.1' WATCHDOG_REQVER = '>=0.10.3' - # Optional dependencies CYTHON_REQVER = '>=0.21' MATPLOTLIB_REQVER = '>=3.0.0' @@ -234,11 +234,11 @@ 'required_version': QTPY_REQVER}, {'modname': "rtree", 'package_name': "rtree", - 'features': _("Fast access to code snippets regions"), + 'features': _("Fast access to code snippet regions"), 'required_version': RTREE_REQVER}, {'modname': "setuptools", 'package_name': "setuptools", - 'features': _("Determine package version"), + 'features': _("Determine package versions"), 'required_version': SETUPTOOLS_REQVER}, {'modname': "sphinx", 'package_name': "sphinx", @@ -248,6 +248,10 @@ 'package_name': "spyder-kernels", 'features': _("Jupyter kernels for the Spyder console"), 'required_version': SPYDER_KERNELS_REQVER}, + {'modname': "superqt", + 'package_name': "superqt", + 'features': _("Special widgets and utilities for PyQt applications"), + 'required_version': SUPERQT_REQVER}, {'modname': 'textdistance', 'package_name': "textdistance", 'features': _('Compute distances between strings'), From 4fb839f471341d2b1c0ae7ecb014bcfcc9b96c94 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 21 Oct 2023 21:02:10 -0400 Subject: [PATCH 28/34] Preferences: Debounce operations when its dialog is resized Also, use signals_blocked utility of superqt to simplify a bit of code. --- .../preferences/widgets/configdialog.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index 629403937d6..a517dd1cbef 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -14,6 +14,7 @@ QDialog, QDialogButtonBox, QFrame, QGridLayout, QHBoxLayout, QListView, QListWidget, QListWidgetItem, QPushButton, QScrollArea, QStackedWidget, QVBoxLayout, QWidget) +from superqt.utils import qdebounced, signals_blocked # Local imports from spyder.api.config.fonts import SpyderFontType, SpyderFontsMixin @@ -276,19 +277,17 @@ def showEvent(self, event): # This is necessary to paint the separators as expected when there # are elided items in contents_widget. - self.blockSignals(True) - height = self.height() - self.resize(self.width(), height + 1) - self.resize(self.width(), height - 1) - self.blockSignals(False) + with signals_blocked(self): + height = self.height() + self.resize(self.width(), height + 1) + self.resize(self.width(), height - 1) def resizeEvent(self, event): """ - Reimplement Qt method to be able to save the widget's size from the - main application + Reimplement Qt method to perform several operations when resizing. """ QDialog.resizeEvent(self, event) - self.sig_size_changed.emit(self.size()) + self._on_resize_event() # ---- Private API # ------------------------------------------------------------------------- @@ -419,3 +418,10 @@ def _generate_stylesheet(self): ) return css + + @qdebounced(timeout=40) + def _on_resize_event(self): + """Method to run when Qt emits a resize event.""" + self._add_tooltips() + self._adjust_items_margin() + self.sig_size_changed.emit(self.size()) From 85310f8fa7c36dd64903b4e170db8a4478d97042 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 21 Oct 2023 21:16:04 -0400 Subject: [PATCH 29/34] Preferences: Adjust separators width when its dialog is resized This is only necessary on Mac. --- .../preferences/widgets/configdialog.py | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index a517dd1cbef..a18da136642 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -45,7 +45,7 @@ class ConfigDialog(QDialog, SpyderFontsMixin): # Constants ITEMS_MARGIN = 2 * AppStyle.MarginSize ITEMS_PADDING = AppStyle.MarginSize - CONTENTS_MAX_WIDTH = 230 + CONTENTS_WIDTH = 230 ICON_SIZE = 20 def __init__(self, parent=None): @@ -57,6 +57,7 @@ def __init__(self, parent=None): SpyderFontType.Interface, font_size_delta=1 ) self._is_shown = False + self._separators = [] # Widgets self.pages_widget = QStackedWidget(self) @@ -84,7 +85,7 @@ def __init__(self, parent=None): self.contents_widget.setCurrentRow(0) self.contents_widget.setObjectName('configdialog-contents') self.contents_widget.setIconSize(QSize(self.ICON_SIZE, self.ICON_SIZE)) - self.contents_widget.setFixedWidth(self.CONTENTS_MAX_WIDTH) + self.contents_widget.setFixedWidth(self.CONTENTS_WIDTH) # Don't show horizontal scrollbar because it doesn't look good. Instead # we show tooltips if the text doesn't fit in contents_widget width. @@ -198,6 +199,9 @@ def add_separator(self): # pages_widget indexes. self.pages_widget.addWidget(QWidget(self)) + # Save separators to perform certain operations only on them + self._separators.append(hline) + def add_page(self, page): # Signals self.check_settings.connect(page.check_settings) @@ -353,6 +357,34 @@ def _adjust_items_margin(self): self.setStyleSheet(self._css.toString()) + def _adjust_separators_width(self): + """ + Adjust the width of separators present in contents_widget depending on + if its vertical scrollbar is visible. + + Notes + ----- + We need to do this only in Mac because Qt doesn't set the widths + correctly when there are elided items. + """ + if sys.platform == 'darwin': + scrollbar = self.contents_widget.verticalScrollBar() + for sep in self._separators: + if self.CONTENTS_WIDTH != 230: + raise ValueError( + "The values used here for the separators' width were " + "the ones reported by Qt for a contents_widget width " + "of 230px. Since this value changed, you need to " + "update them." + ) + + # These are the values reported by Qt when CONTENTS_WIDTH = 230 + # and the interface language is English. + if scrollbar.isVisible(): + sep.setFixedWidth(188) + else: + sep.setFixedWidth(204) + def _generate_stylesheet(self): """Generate stylesheet for this widget as qstylizer object.""" # Use special tabbar stylesheet for as the base one and then extend it. @@ -424,4 +456,5 @@ def _on_resize_event(self): """Method to run when Qt emits a resize event.""" self._add_tooltips() self._adjust_items_margin() + self._adjust_separators_width() self.sig_size_changed.emit(self.size()) From 7c7c3969dca794b5c3551b0a5a42d3fc9f7caf85 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 23 Oct 2023 11:07:32 -0500 Subject: [PATCH 30/34] Widgets: Debounce operations when resizing ElementsTable --- spyder/widgets/elementstable.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/spyder/widgets/elementstable.py b/spyder/widgets/elementstable.py index cf7a4408eab..fa80115d67d 100644 --- a/spyder/widgets/elementstable.py +++ b/spyder/widgets/elementstable.py @@ -18,6 +18,7 @@ from qtpy.QtCore import QAbstractTableModel, QModelIndex, QSize, Qt from qtpy.QtGui import QIcon from qtpy.QtWidgets import QAbstractItemView, QCheckBox, QHBoxLayout, QWidget +from superqt.utils import qdebounced # Local imports from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType @@ -333,6 +334,18 @@ def _set_layout(self): # changes row heights in unpredictable ways. self.resizeRowsToContents() + _set_layout_debounced = qdebounced(_set_layout, timeout=40) + """ + Debounced version of _set_layout. + + Notes + ----- + * We need a different version of _set_layout so that we can use the regular + one in showEvent. That way users won't experience a visual glitch when + the widget is rendered for the first time. + * We use this version in resizeEvent, where that is not a problem. + """ + def _with_feature(self, feature_name: str) -> bool: """Check if it's necessary to build the table with `feature_name`.""" return len([e for e in self.elements if e.get(feature_name)]) > 0 @@ -369,7 +382,7 @@ def enterEvent(self, event): def resizeEvent(self, event): # This is necessary to readjust the layout when the parent widget is # resized. - self._set_layout() + self._set_layout_debounced() super().resizeEvent(event) From 66700f0f84c08d1306d7aaf762d03465c708c488 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 23 Oct 2023 12:48:45 -0500 Subject: [PATCH 31/34] Preferences: Make several adjustments per operating system - Set default and min sizes for its dialog. - Increase items padding on Linux. - Decrease contents area width on Windows and increase it on Linux. - Adjust min height of editor config page to avoid clipped text. --- spyder/config/main.py | 4 ++- spyder/plugins/editor/confpage.py | 19 +++++++++++-- .../preferences/widgets/configdialog.py | 28 +++++++++++++------ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/spyder/config/main.py b/spyder/config/main.py index e89d750e51e..5cd419c7016 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -294,7 +294,9 @@ ('preferences', { 'enable': True, - 'dialog_size': (990, 715), + 'dialog_size': ( + (1010, 725) if MAC else ((900, 670) if WIN else (950, 690)) + ), }), ('project_explorer', { diff --git a/spyder/plugins/editor/confpage.py b/spyder/plugins/editor/confpage.py index 4deba3f01ea..387d932dece 100644 --- a/spyder/plugins/editor/confpage.py +++ b/spyder/plugins/editor/confpage.py @@ -6,6 +6,9 @@ """Editor config page.""" +import os +import sys + from qtpy.QtWidgets import (QGridLayout, QGroupBox, QHBoxLayout, QLabel, QVBoxLayout) @@ -37,6 +40,19 @@ def __init__(self, plugin, parent): self.add_newline_box = None self.remove_trail_newline_box = None + # *********************** IMPORTANT NOTES ***************************** + # * This value needs to be ajusted if we add new options to the + # "Advanced settings" tab. + # * We need to do this so that the text of some options is not clipped. + if os.name == "nt": + min_height = 620 + elif sys.platform == "darwin": + min_height = 760 + else: + min_height = 670 + + self.setMinimumHeight(min_height) + def get_name(self): return _("Editor") @@ -44,9 +60,6 @@ def get_icon(self): return ima.icon('edit') def setup_page(self): - # We need to set this so that the text of some options is not clipped - self.setMinimumHeight(670) - newcb = self.create_checkbox # --- Display tab --- diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index a18da136642..65fb5b6e0ac 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -4,9 +4,6 @@ # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) -# Standard library imports -import sys - # Third party imports from qtpy.QtCore import QSize, Qt, Signal, Slot from qtpy.QtGui import QFontMetricsF @@ -44,9 +41,13 @@ class ConfigDialog(QDialog, SpyderFontsMixin): # Constants ITEMS_MARGIN = 2 * AppStyle.MarginSize - ITEMS_PADDING = AppStyle.MarginSize - CONTENTS_WIDTH = 230 + ITEMS_PADDING = ( + AppStyle.MarginSize if (MAC or WIN) else 2 * AppStyle.MarginSize + ) + CONTENTS_WIDTH = 230 if MAC else (200 if WIN else 240) ICON_SIZE = 20 + MIN_WIDTH = 940 if MAC else (875 if WIN else 920) + MIN_HEIGHT = 700 if MAC else (660 if WIN else 670) def __init__(self, parent=None): QDialog.__init__(self, parent) @@ -59,6 +60,10 @@ def __init__(self, parent=None): self._is_shown = False self._separators = [] + # Size + self.setMinimumWidth(self.MIN_WIDTH) + self.setMinimumHeight(self.MIN_HEIGHT) + # Widgets self.pages_widget = QStackedWidget(self) self.contents_widget = QListWidget(self) @@ -189,7 +194,12 @@ def add_separator(self): # Solution taken from https://stackoverflow.com/a/24819554/438386 item = QListWidgetItem(self.contents_widget) item.setFlags(Qt.NoItemFlags) - item.setSizeHint(QSize(9, 9)) + + size = ( + AppStyle.MarginSize * 3 if (MAC or WIN) + else AppStyle.MarginSize * 5 + ) + item.setSizeHint(QSize(size, size)) hline = QFrame(self.contents_widget) hline.setFrameShape(QFrame.HLine) @@ -318,7 +328,7 @@ def _add_tooltips(self): scrollbar = self.contents_widget.verticalScrollBar() if scrollbar.isVisible(): - if sys.platform == 'darwin': + if MAC: # This is a crude heuristic to detect if we need to add # tooltips on Mac. However, it's the best we can do # (the approach for other OSes below ends up adding @@ -341,7 +351,7 @@ def _adjust_items_margin(self): We need to do this only in Mac because Qt doesn't account for the scrollbar width in most widgets. """ - if sys.platform == 'darwin': + if MAC: scrollbar = self.contents_widget.verticalScrollBar() extra_margin = ( AppStyle.MacScrollBarWidth if scrollbar.isVisible() else 0 @@ -367,7 +377,7 @@ def _adjust_separators_width(self): We need to do this only in Mac because Qt doesn't set the widths correctly when there are elided items. """ - if sys.platform == 'darwin': + if MAC: scrollbar = self.contents_widget.verticalScrollBar() for sep in self._separators: if self.CONTENTS_WIDTH != 230: From 69fa4bae24927c2e3ccaef804e0895f2bc035ecd Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 24 Oct 2023 20:28:29 -0500 Subject: [PATCH 32/34] Stylesheet: Create a new class for tab bars used in Preferences --- .../preferences/widgets/configdialog.py | 31 +----- spyder/utils/stylesheet.py | 95 +++++++++++++++---- 2 files changed, 79 insertions(+), 47 deletions(-) diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index 65fb5b6e0ac..255336b1229 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -20,7 +20,7 @@ from spyder.utils.icon_manager import ima from spyder.utils.palette import QStylePalette from spyder.utils.stylesheet import ( - AppStyle, MAC, SPECIAL_TABBAR_STYLESHEET, WIN) + AppStyle, MAC, PREFERENCES_TABBAR_STYLESHEET, WIN) class PageScrollArea(QScrollArea): @@ -396,34 +396,11 @@ def _adjust_separators_width(self): sep.setFixedWidth(204) def _generate_stylesheet(self): - """Generate stylesheet for this widget as qstylizer object.""" - # Use special tabbar stylesheet for as the base one and then extend it. - tabs_stylesheet = SPECIAL_TABBAR_STYLESHEET.get_copy() + """Generate stylesheet for this widget as a qstylizer object.""" + # Use the tabbar stylesheet as the base one and extend it. + tabs_stylesheet = PREFERENCES_TABBAR_STYLESHEET.get_copy() css = tabs_stylesheet.get_stylesheet() - # Set tabs font size to be the same as the one for items - css.QTabBar.setValues( - fontSize=f'{self.items_font.pointSize()}pt', - ) - - # Make tabbar scroll button a bit bigger on Windows and Mac (this has - # no effect on Linux). - if WIN or MAC: - css['QTabBar QToolButton'].setValues( - padding=f'{tabs_stylesheet.SCROLL_BUTTONS_PADDING - 1}px', - ) - - # Increase padding around text a bit because we're using an larger font - css['QTabBar::tab'].setValues( - padding='6px 10px', - ) - - # Remove border and add padding for content inside tabs - css['QTabWidget::pane'].setValues( - border='0px', - padding='15px', - ) - # Set style of contents area css['QListView#configdialog-contents'].setValues( padding=f'{self.ITEMS_MARGIN}px 0px', diff --git a/spyder/utils/stylesheet.py b/spyder/utils/stylesheet.py index 1bad5ab49a6..ee40bd997cd 100644 --- a/spyder/utils/stylesheet.py +++ b/spyder/utils/stylesheet.py @@ -57,15 +57,27 @@ class AppStyle: class SpyderStyleSheet: """Base class for Spyder stylesheets.""" - def __init__(self, set_stylesheet=True): + SET_STYLESHEET_AT_INIT = True + """ + Decide if the stylesheet must be set when the class is initialized. + + Notes + ----- + There are some stylesheets for which this is not possible (e.g. the ones + that need to access our fonts). + """ + + def __init__(self): self._stylesheet = qstylizer.style.StyleSheet() - if set_stylesheet: + if self.SET_STYLESHEET_AT_INIT: self.set_stylesheet() def get_stylesheet(self): return self._stylesheet def to_string(self): + if self._stylesheet.toString() == "": + self.set_stylesheet() return self._stylesheet.toString() def get_copy(self): @@ -74,6 +86,8 @@ def get_copy(self): This allows it to be modified for specific widgets. """ + if self._stylesheet.toString() == "": + self.set_stylesheet() return copy.deepcopy(self) def set_stylesheet(self): @@ -99,17 +113,19 @@ class AppStylesheet(SpyderStyleSheet, SpyderConfigurationAccessor): application. """ + # Don't create the stylesheet here so that Spyder gets the app font from + # the system when it starts for the first time. This also allows us to + # display the splash screen more quickly because the stylesheet is then + # computed only when it's going to be applied to the app, not when this + # object is imported. + SET_STYLESHEET_AT_INIT = False + def __init__(self): - # Don't create the stylesheet here so that Spyder gets the app font - # from the system when it starts for the first time. This also allows - # us to display the splash screen more quickly because the stylesheet - # is then computed only when it's going to be applied to the app, not - # when this object is imported. - super().__init__(set_stylesheet=False) + super().__init__() self._stylesheet_as_string = None def to_string(self): - "Save stylesheet as a string for quick access." + """Save stylesheet as a string for quick access.""" if self._stylesheet_as_string is None: self.set_stylesheet() self._stylesheet_as_string = self._stylesheet.toString() @@ -280,7 +296,7 @@ class ApplicationToolbarStylesheet(SpyderStyleSheet): BUTTON_MARGIN_RIGHT = '3px' def set_stylesheet(self): - css = self._stylesheet + css = self.get_stylesheet() # Main background color css.QToolBar.setValues( @@ -320,7 +336,7 @@ class PanesToolbarStyleSheet(SpyderStyleSheet): BUTTON_HEIGHT = '37px' def set_stylesheet(self): - css = self._stylesheet + css = self.get_stylesheet() css.QToolBar.setValues( spacing='4px' @@ -531,7 +547,14 @@ def set_stylesheet(self): class SpecialTabBarStyleSheet(BaseDockTabBarStyleSheet): - """Style for special horizontal tabbars.""" + """ + Style for special tab bars. + + Notes + ----- + This is the base class for horizontal tab bars that follow the design + discussed on issue spyder-ide/ux-improvements#4. + """ SCROLL_BUTTONS_BORDER_POS = 'right' @@ -599,11 +622,45 @@ def set_stylesheet(self): ) +class PreferencesTabBarStyleSheet(SpecialTabBarStyleSheet, SpyderFontsMixin): + """Style for tab bars in our Preferences dialog.""" + + # This is necessary because this class needs to access fonts + SET_STYLESHEET_AT_INIT = False + + def set_stylesheet(self): + super().set_stylesheet() + + # Main constants + css = self.get_stylesheet() + font = self.get_font(SpyderFontType.Interface, font_size_delta=1) + + # Set font size to be one point bigger than the regular text. + css.QTabBar.setValues( + fontSize=f'{font.pointSize()}pt', + ) + + # Make scroll buttons a bit bigger on Windows and Mac (this has no + # effect on Linux). + if WIN or MAC: + css['QTabBar QToolButton'].setValues( + padding=f'{self.SCROLL_BUTTONS_PADDING - 1}px', + ) + + # Increase padding around text because we're using a larger font. + css['QTabBar::tab'].setValues( + padding='6px 10px', + ) + + # Remove border and add padding for content inside tabs + css['QTabWidget::pane'].setValues( + border='0px', + padding='15px', + ) + + class HorizontalDockTabBarStyleSheet(SpecialTabBarStyleSheet): - """ - This implements the design for dockwidget tabs discussed on issue - spyder-ide/ux-improvements#4. - """ + """Style for horizontal dockwidget tab bars.""" def set_stylesheet(self): super().set_stylesheet() @@ -646,9 +703,7 @@ def set_stylesheet(self): class VerticalDockTabBarStyleSheet(BaseDockTabBarStyleSheet): - """ - Vertical implementation for the design on spyder-ide/ux-improvements#4. - """ + """Style for vertical dockwidget tab bars.""" SCROLL_BUTTONS_BORDER_POS = 'bottom' @@ -723,7 +778,7 @@ def set_stylesheet(self): PANES_TABBAR_STYLESHEET = PanesTabBarStyleSheet() HORIZONTAL_DOCK_TABBAR_STYLESHEET = HorizontalDockTabBarStyleSheet() VERTICAL_DOCK_TABBAR_STYLESHEET = VerticalDockTabBarStyleSheet() -SPECIAL_TABBAR_STYLESHEET = SpecialTabBarStyleSheet() +PREFERENCES_TABBAR_STYLESHEET = PreferencesTabBarStyleSheet() # ============================================================================= From f73966278543bea7f61433d0dfc6b384f7aab538 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 24 Oct 2023 20:30:40 -0500 Subject: [PATCH 33/34] Preferences: Show "Main interpreter" entry above "Keyboard shortcuts" Also, fix small style issue. --- spyder/plugins/preferences/api.py | 2 +- spyder/plugins/preferences/plugin.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spyder/plugins/preferences/api.py b/spyder/plugins/preferences/api.py index 7f1bf384670..6a48427ffb1 100644 --- a/spyder/plugins/preferences/api.py +++ b/spyder/plugins/preferences/api.py @@ -16,8 +16,8 @@ MOST_IMPORTANT_PAGES = [ Plugins.Appearance, Plugins.Application, - Plugins.Shortcuts, Plugins.MainInterpreter, + Plugins.Shortcuts, ] diff --git a/spyder/plugins/preferences/plugin.py b/spyder/plugins/preferences/plugin.py index 25a9d5a642e..68b630bdc0d 100644 --- a/spyder/plugins/preferences/plugin.py +++ b/spyder/plugins/preferences/plugin.py @@ -295,9 +295,10 @@ def reset(self): answer = QMessageBox.warning( self.main, _("Warning"), - _("Spyder will restart and reset to default settings:

" - "Do you want to continue?"), - QMessageBox.Yes | QMessageBox.No + _("Spyder will restart and reset to default settings:" + "

" + "Do you want to continue?"), + QMessageBox.Yes | QMessageBox.No ) if answer == QMessageBox.Yes: From c59a2e5889d74626795d85691b22479470d798c1 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 4 Nov 2023 09:28:56 -0500 Subject: [PATCH 34/34] Preferences: Move MOST_IMPORTANT_PAGES constant to its init module - That's because that constant doesn't need to be imported by other plugins but shared by several modules in this one. - So, we need a separate module to declare it in order to avoid circular imports. --- spyder/plugins/preferences/__init__.py | 12 ++++++++++++ spyder/plugins/preferences/api.py | 12 ------------ spyder/plugins/preferences/plugin.py | 2 +- spyder/plugins/preferences/widgets/container.py | 6 ++---- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/spyder/plugins/preferences/__init__.py b/spyder/plugins/preferences/__init__.py index 88a2b96a750..66c374cd4d8 100644 --- a/spyder/plugins/preferences/__init__.py +++ b/spyder/plugins/preferences/__init__.py @@ -10,3 +10,15 @@ Preferences plugin """ + +from spyder.api.plugins import Plugins + + +# We consider these to be the plugins with the most important pages. So, we'll +# show those pages as the first entries in the config dialog +MOST_IMPORTANT_PAGES = [ + Plugins.Appearance, + Plugins.Application, + Plugins.MainInterpreter, + Plugins.Shortcuts, +] diff --git a/spyder/plugins/preferences/api.py b/spyder/plugins/preferences/api.py index 6a48427ffb1..2d68fb98494 100644 --- a/spyder/plugins/preferences/api.py +++ b/spyder/plugins/preferences/api.py @@ -8,18 +8,6 @@ Preferences Plugin API. """ -from spyder.api.plugins import Plugins - - -# We consider these to be the plugins with the most important pages. So, we'll -# show those pages as the first entries in the config dialog -MOST_IMPORTANT_PAGES = [ - Plugins.Appearance, - Plugins.Application, - Plugins.MainInterpreter, - Plugins.Shortcuts, -] - class PreferencesActions: Show = 'show_action' diff --git a/spyder/plugins/preferences/plugin.py b/spyder/plugins/preferences/plugin.py index 68b630bdc0d..80d7f459c3d 100644 --- a/spyder/plugins/preferences/plugin.py +++ b/spyder/plugins/preferences/plugin.py @@ -32,7 +32,7 @@ from spyder.config.main import CONF_VERSION from spyder.config.user import NoDefault from spyder.plugins.mainmenu.api import ApplicationMenus, ToolsMenuSections -from spyder.plugins.preferences.api import MOST_IMPORTANT_PAGES +from spyder.plugins.preferences import MOST_IMPORTANT_PAGES from spyder.plugins.preferences.widgets.container import ( PreferencesActions, PreferencesContainer) from spyder.plugins.pythonpath.api import PythonpathActions diff --git a/spyder/plugins/preferences/widgets/container.py b/spyder/plugins/preferences/widgets/container.py index 261cc04bf35..1aa46fc168b 100644 --- a/spyder/plugins/preferences/widgets/container.py +++ b/spyder/plugins/preferences/widgets/container.py @@ -13,10 +13,8 @@ from spyder.api.plugin_registration.registry import PreferencesAdapter from spyder.api.translations import _ from spyder.api.widgets.main_container import PluginMainContainer -from spyder.plugins.preferences.api import ( - MOST_IMPORTANT_PAGES, - PreferencesActions -) +from spyder.plugins.preferences import MOST_IMPORTANT_PAGES +from spyder.plugins.preferences.api import PreferencesActions from spyder.plugins.preferences.widgets.configdialog import ConfigDialog