From e73518d04105409254af0e2a14bbe1bab367539a Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 29 Oct 2023 00:41:53 -0500 Subject: [PATCH 01/55] Run: Hide file combobox in RunDialog - Instead, show label with the current file name. - That helps to decrease the complexity of this dialog. --- spyder/plugins/run/widgets.py | 84 +++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index f6b84aee4c9..344eb41896e 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -15,12 +15,15 @@ # Third party imports from qtpy.compat import getexistingdirectory from qtpy.QtCore import QSize, Qt, Signal +from qtpy.QtGui import QFontMetrics from qtpy.QtWidgets import (QCheckBox, QDialog, QDialogButtonBox, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QLayout, QRadioButton, QStackedWidget, QVBoxLayout, QWidget) +import qstylizer.style # Local imports from spyder.api.translations import _ +from spyder.api.config.fonts import SpyderFontType, SpyderFontsMixin from spyder.api.widgets.comboboxes import SpyderComboBox from spyder.plugins.run.api import ( RunParameterFlags, WorkingDirSource, WorkingDirOpts, @@ -28,7 +31,9 @@ RunExecutorConfigurationGroup, SupportedExecutionRunConfiguration) from spyder.utils.icon_manager import ima from spyder.utils.misc import getcwd_or_home +from spyder.utils.palette import QStylePalette from spyder.utils.qthelpers import create_toolbutton +from spyder.utils.stylesheet import AppStyle @@ -413,7 +418,7 @@ def get_configuration( return self.saved_conf -class RunDialog(BaseRunConfigDialog): +class RunDialog(BaseRunConfigDialog, SpyderFontsMixin): """Run dialog used to configure run executors.""" def __init__( @@ -430,12 +435,19 @@ def __init__( self.parameter_model = parameter_model self.current_widget = None self.status = RunDialogStatus.Close + self._is_shown = False + # ---- Public methods def setup(self): - combo_label = QLabel(_("Select a run configuration:")) - executor_label = QLabel(_("Select a run executor:")) + # Header + self.header_label = QLabel(self) + self.header_label.setObjectName("header-label") + # Hide this combobox to decrease the dialog complexity self.configuration_combo = SpyderComboBox(self) + self.configuration_combo.hide() + + executor_label = QLabel(_("Select an executor:")) self.executor_combo = SpyderComboBox(self) parameters_label = QLabel(_("Select the run parameters:")) @@ -464,7 +476,7 @@ def setup(self): fixed_dir_layout = QHBoxLayout() self.fixed_dir_radio = QRadioButton(FIXED_DIR) fixed_dir_layout.addWidget(self.fixed_dir_radio) - self.wd_edit = QLineEdit() + self.wd_edit = QLineEdit(self) self.fixed_dir_radio.toggled.connect(self.wd_edit.setEnabled) self.wd_edit.setEnabled(False) fixed_dir_layout.addWidget(self.wd_edit) @@ -479,7 +491,7 @@ def setup(self): # --- Store new custom configuration self.store_params_cb = QCheckBox(STORE_PARAMS) - self.store_params_text = QLineEdit() + self.store_params_text = QLineEdit(self) store_params_layout = QHBoxLayout() store_params_layout.addWidget(self.store_params_cb) store_params_layout.addWidget(self.store_params_text) @@ -491,10 +503,18 @@ def setup(self): self.firstrun_cb = QCheckBox(ALWAYS_OPEN_FIRST_RUN % _("this dialog")) - layout = self.add_widgets(combo_label, self.configuration_combo, - executor_label, self.executor_combo, - 10, self.executor_group, self.firstrun_cb) + layout = self.add_widgets( + self.header_label, + 5, + self.configuration_combo, + executor_label, + self.executor_combo, + 10, + self.executor_group, + self.firstrun_cb + ) + # Settings self.executor_combo.currentIndexChanged.connect( self.display_executor_configuration) self.executor_combo.setModel(self.executors_model) @@ -504,10 +524,7 @@ def setup(self): self.configuration_combo.setModel(self.run_conf_model) self.configuration_combo.setCurrentIndex( self.run_conf_model.get_initial_index()) - - self.configuration_combo.setMaxVisibleItems(20) - self.configuration_combo.view().setVerticalScrollBarPolicy( - Qt.ScrollBarAsNeeded) + self.configuration_combo.setMaxVisibleItems(1) self.executor_combo.setMaxVisibleItems(20) self.executor_combo.view().setVerticalScrollBarPolicy( @@ -517,7 +534,7 @@ def setup(self): self.update_parameter_set) self.parameters_combo.setModel(self.parameter_model) - widget_dialog = QWidget() + widget_dialog = QWidget(self) widget_dialog.setMinimumWidth(600) widget_dialog.setLayout(layout) scroll_layout = QVBoxLayout(self) @@ -527,6 +544,8 @@ def setup(self): self.setWindowTitle(_("Run configuration per file")) self.layout().setSizeConstraint(QLayout.SetFixedSize) + self.setStyleSheet(self._stylesheet) + def select_directory(self): """Select directory""" basedir = str(self.wd_edit.text()) @@ -700,3 +719,42 @@ def get_configuration( ) -> Tuple[str, str, ExtendedRunExecutionParameters, bool]: return self.saved_conf + + # ---- Qt methods + def showEvent(self, event): + """Adjustments when the widget is shown.""" + if not self._is_shown: + # Set file name as the header + fname = self.configuration_combo.currentText() + header_font = ( + self.get_font(SpyderFontType.Interface, font_size_delta=1) + ) + + # Elide fname in case fname is too long + fm = QFontMetrics(header_font) + text = fm.elidedText( + fname, Qt.ElideLeft, self.header_label.width() + ) + + self.header_label.setFont(header_font) + self.header_label.setAlignment(Qt.AlignCenter) + self.header_label.setText(text) + if text != fname: + self.header_label.setToolTip(fname) + + self._is_shown = True + + super().showEvent(event) + + # ---- Private methods + @property + def _stylesheet(self): + css = qstylizer.style.StyleSheet() + + css["QLabel#header-label"].setValues( + backgroundColor=QStylePalette.COLOR_BACKGROUND_3, + padding=f"{2 * AppStyle.MarginSize} {4 * AppStyle.MarginSize}", + borderRadius=QStylePalette.SIZE_BORDER_RADIUS + ) + + return css.toString() From 5e0182f9948af338e48e58efb948d7b79e025741 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 29 Oct 2023 01:04:41 -0500 Subject: [PATCH 02/55] Run: Remove checkbox to always show RunDialog This also helps to decrease the complexity of that dialog. --- spyder/app/tests/conftest.py | 1 - spyder/plugins/run/api.py | 4 ---- spyder/plugins/run/container.py | 29 +++++++++++++--------------- spyder/plugins/run/tests/test_run.py | 2 -- spyder/plugins/run/widgets.py | 15 ++------------ 5 files changed, 15 insertions(+), 36 deletions(-) diff --git a/spyder/app/tests/conftest.py b/spyder/app/tests/conftest.py index b1d1232cd19..5678c7d1b88 100755 --- a/spyder/app/tests/conftest.py +++ b/spyder/app/tests/conftest.py @@ -249,7 +249,6 @@ def generate_run_parameters(mainwindow, filename, selected=None, file_run_params = StoredRunConfigurationExecutor( executor=executor, selected=selected, - display_dialog=False ) return {file_uuid: file_run_params} diff --git a/spyder/plugins/run/api.py b/spyder/plugins/run/api.py index 1881299b2d4..55c00447edb 100644 --- a/spyder/plugins/run/api.py +++ b/spyder/plugins/run/api.py @@ -267,10 +267,6 @@ class StoredRunConfigurationExecutor(TypedDict): # if using default or transient settings. selected: Optional[str] - # If True, then the run dialog will displayed every time the run - # configuration is executed. Otherwise not. - display_dialog: bool - class RunConfigurationProvider(QObject): """ diff --git a/spyder/plugins/run/container.py b/spyder/plugins/run/container.py index d1193ee00a3..b36c5052636 100644 --- a/spyder/plugins/run/container.py +++ b/spyder/plugins/run/container.py @@ -200,13 +200,8 @@ def run_file(self, selected_uuid=None, selected_executor=None): if not isinstance(selected_uuid, bool) and selected_uuid is not None: self.switch_focused_run_configuration(selected_uuid) - exec_params = self.get_last_used_executor_parameters( - self.currently_selected_configuration) - - display_dialog = exec_params['display_dialog'] - self.edit_run_configurations( - display_dialog=display_dialog, + display_dialog=False, selected_executor=selected_executor) def edit_run_configurations( @@ -216,8 +211,13 @@ def edit_run_configurations( selected_executor=None ): self.dialog = RunDialog( - self, self.metadata_model, self.executor_model, - self.parameter_model, disable_run_btn=disable_run_btn) + self, + self.metadata_model, + self.executor_model, + self.parameter_model, + disable_run_btn=disable_run_btn + ) + self.dialog.setup() self.dialog.finished.connect(self.process_run_dialog_result) @@ -235,8 +235,7 @@ def process_run_dialog_result(self, result): if status == RunDialogStatus.Close: return - (uuid, executor_name, - ext_params, open_dialog) = self.dialog.get_configuration() + uuid, executor_name, ext_params = self.dialog.get_configuration() if (status & RunDialogStatus.Save) == RunDialogStatus.Save: exec_uuid = ext_params['uuid'] @@ -254,8 +253,8 @@ def process_run_dialog_result(self, result): executor_name, ext, context_id, all_exec_params) last_used_conf = StoredRunConfigurationExecutor( - executor=executor_name, selected=ext_params['uuid'], - display_dialog=open_dialog) + executor=executor_name, selected=ext_params['uuid'] + ) self.set_last_used_execution_params(uuid, last_used_conf) @@ -949,8 +948,7 @@ def get_last_used_executor_parameters( uuid, StoredRunConfigurationExecutor( executor=None, - selected=None, - display_dialog=False, + selected=None ) ) @@ -987,8 +985,7 @@ def get_last_used_execution_params( default = StoredRunConfigurationExecutor( executor=executor_name, - selected=None, - display_dialog=False + selected=None ) params = mru_executors_uuids.get(uuid, default) diff --git a/spyder/plugins/run/tests/test_run.py b/spyder/plugins/run/tests/test_run.py index a4f35592bd7..b1d88dcfd78 100644 --- a/spyder/plugins/run/tests/test_run.py +++ b/spyder/plugins/run/tests/test_run.py @@ -616,7 +616,6 @@ def test_run_plugin(qtbot, run_mock): stored_run_params = run.get_last_used_executor_parameters(run_conf_uuid) assert stored_run_params['executor'] == test_executor_name assert stored_run_params['selected'] is None - assert not stored_run_params['display_dialog'] # The configuration gets run again with qtbot.waitSignal(executor_1.sig_run_invocation) as sig: @@ -778,7 +777,6 @@ def test_run_plugin(qtbot, run_mock): stored_run_params = run.get_last_used_executor_parameters(run_conf_uuid) assert stored_run_params['executor'] == executor_name assert stored_run_params['selected'] == exec_conf_uuid - assert not stored_run_params['display_dialog'] # Test teardown functions executor_1.on_run_teardown(run) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 344eb41896e..0b8ea68d0c8 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -53,9 +53,6 @@ WDIR_USE_FIXED_DIR_OPTION = 'default/wdir/use_fixed_directory' WDIR_FIXED_DIR_OPTION = 'default/wdir/fixed_directory' -ALWAYS_OPEN_FIRST_RUN = _("Always show %s for this run configuration") -ALWAYS_OPEN_FIRST_RUN_OPTION = 'open_on_firstrun' - CLEAR_ALL_VARIABLES = _("Remove all variables before execution") CONSOLE_NAMESPACE = _("Run in console's namespace instead of an empty one") POST_MORTEM = _("Directly enter debugging when errors appear") @@ -501,8 +498,6 @@ def setup(self): self.store_params_text.setPlaceholderText(_('My configuration name')) self.store_params_text.setEnabled(False) - self.firstrun_cb = QCheckBox(ALWAYS_OPEN_FIRST_RUN % _("this dialog")) - layout = self.add_widgets( self.header_label, 5, @@ -511,7 +506,6 @@ def setup(self): self.executor_combo, 10, self.executor_group, - self.firstrun_cb ) # Settings @@ -640,16 +634,12 @@ def display_executor_configuration(self, index: int): selected_params = self.run_conf_model.get_last_used_execution_params( uuid, executor_name) - all_selected_params = ( - self.run_conf_model.get_last_used_executor_parameters(uuid)) - re_open_dialog = all_selected_params['display_dialog'] index = self.parameter_model.get_parameters_index(selected_params) if self.parameters_combo.count() == 0: self.index_to_select = index self.parameters_combo.setCurrentIndex(index) - self.firstrun_cb.setChecked(re_open_dialog) self.adjustSize() def select_executor(self, executor_name: str): @@ -708,10 +698,9 @@ def accept(self) -> None: self.configuration_combo.currentIndex() ) - open_dialog = self.firstrun_cb.isChecked() - self.saved_conf = (metadata_info['uuid'], executor_name, - ext_exec_params, open_dialog) + ext_exec_params) + return super().accept() def get_configuration( From 2c33b79fc0d52d82b3afc13a0642cdf9ce7f4af2 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 29 Oct 2023 11:42:17 -0500 Subject: [PATCH 03/55] Run: Simplify layout of executor and parameters comboboxes in RunDialog --- spyder/plugins/run/widgets.py | 45 ++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 0b8ea68d0c8..1eb38ccd299 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -16,9 +16,10 @@ from qtpy.compat import getexistingdirectory from qtpy.QtCore import QSize, Qt, Signal from qtpy.QtGui import QFontMetrics -from qtpy.QtWidgets import (QCheckBox, QDialog, QDialogButtonBox, - QGroupBox, QHBoxLayout, QLabel, QLineEdit, QLayout, - QRadioButton, QStackedWidget, QVBoxLayout, QWidget) +from qtpy.QtWidgets import ( + QCheckBox, QDialog, QDialogButtonBox, QGridLayout, QGroupBox, QHBoxLayout, + QLabel, QLineEdit, QLayout, QRadioButton, QStackedWidget, QVBoxLayout, + QWidget) import qstylizer.style # Local imports @@ -444,23 +445,34 @@ def setup(self): self.configuration_combo = SpyderComboBox(self) self.configuration_combo.hide() - executor_label = QLabel(_("Select an executor:")) + executor_label = QLabel(_("Run this file in:")) self.executor_combo = SpyderComboBox(self) - - parameters_label = QLabel(_("Select the run parameters:")) + parameters_label = QLabel(_("Preset run parameters:")) self.parameters_combo = SpyderComboBox(self) + + self.executor_combo.setMinimumWidth(250) + self.parameters_combo.setMinimumWidth(250) + + executor_g_layout = QGridLayout() + executor_g_layout.addWidget(executor_label, 0, 0) + executor_g_layout.addWidget(self.executor_combo, 0, 1) + executor_g_layout.addWidget(parameters_label, 1, 0) + executor_g_layout.addWidget(self.parameters_combo, 1, 1) + + executor_layout = QHBoxLayout() + executor_layout.addLayout(executor_g_layout) + executor_layout.addStretch(1) + self.stack = QStackedWidget() - executor_layout = QVBoxLayout() - executor_layout.addWidget(parameters_label) - executor_layout.addWidget(self.parameters_combo) - executor_layout.addWidget(self.stack) + parameters_layout = QVBoxLayout() + parameters_layout.addWidget(self.stack) self.executor_group = QGroupBox(_("Executor parameters")) - self.executor_group.setLayout(executor_layout) + self.executor_group.setLayout(parameters_layout) # --- Working directory --- self.wdir_group = QGroupBox(_("Working directory settings")) - executor_layout.addWidget(self.wdir_group) + parameters_layout.addWidget(self.wdir_group) wdir_layout = QVBoxLayout(self.wdir_group) @@ -492,7 +504,7 @@ def setup(self): store_params_layout = QHBoxLayout() store_params_layout.addWidget(self.store_params_cb) store_params_layout.addWidget(self.store_params_text) - executor_layout.addLayout(store_params_layout) + parameters_layout.addLayout(store_params_layout) self.store_params_cb.toggled.connect(self.store_params_text.setEnabled) self.store_params_text.setPlaceholderText(_('My configuration name')) @@ -500,10 +512,9 @@ def setup(self): layout = self.add_widgets( self.header_label, - 5, - self.configuration_combo, - executor_label, - self.executor_combo, + 10, + self.configuration_combo, # Hidden for simplicity + executor_layout, 10, self.executor_group, ) From 91f2ddb542ddc42cdb752b0bc3bdc31f959a7082 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 30 Oct 2023 12:36:10 -0500 Subject: [PATCH 04/55] Run: Automatically save config if customized by users in RunDialog - The name of that config is the "Custom" string localized. - If the config is not named differently, additional customizations will also be saved in "Custom". - Also, only show global configs (i.e. those created in Preferences) or the ones that correspond to the file currently displayed in RunDialog. --- spyder/plugins/run/api.py | 4 + spyder/plugins/run/models.py | 32 +++++--- spyder/plugins/run/tests/test_run.py | 48 +++++++----- spyder/plugins/run/widgets.py | 106 ++++++++++++++++----------- 4 files changed, 119 insertions(+), 71 deletions(-) diff --git a/spyder/plugins/run/api.py b/spyder/plugins/run/api.py index 55c00447edb..32ea9d33cc1 100644 --- a/spyder/plugins/run/api.py +++ b/spyder/plugins/run/api.py @@ -248,6 +248,10 @@ class ExtendedRunExecutionParameters(TypedDict): # The run execution parameters. params: RunExecutionParameters + # The unique identifier for the file to which these parameters correspond + # to, if any. + file_uuid: Optional[str] + class StoredRunExecutorParameters(TypedDict): """Per run executor configuration parameters.""" diff --git a/spyder/plugins/run/models.py b/spyder/plugins/run/models.py index 1081c308961..2a40730b589 100644 --- a/spyder/plugins/run/models.py +++ b/spyder/plugins/run/models.py @@ -284,13 +284,10 @@ def __init__(self, parent): def data(self, index: QModelIndex, role: int = Qt.DisplayRole): pos = index.row() total_saved_params = len(self.executor_conf_params) + if pos == total_saved_params: if role == Qt.DisplayRole: - return _("Default/Transient") - elif role == Qt.ToolTipRole: - return _( - "This configuration will not be saved after execution" - ) + return _("Default") else: params_id = self.params_index[pos] params = self.executor_conf_params[params_id] @@ -341,13 +338,28 @@ def set_parameters( self.beginResetModel() self.executor_conf_params = parameters self.params_index = dict(enumerate(self.executor_conf_params)) - self.inverse_index = {self.params_index[k]: k - for k in self.params_index} + self.inverse_index = { + self.params_index[k]: k for k in self.params_index + } self.endResetModel() - def get_parameters_index(self, parameters_name: Optional[str]) -> int: - index = self.inverse_index.get(parameters_name, - len(self.executor_conf_params)) + def get_parameters_index_by_uuid( + self, + parameters_uuid: Optional[str] + ) -> int: + index = self.inverse_index.get( + parameters_uuid, len(self.executor_conf_params) + ) + + return index + + def get_parameters_index_by_name(self, parameters_name: str) -> int: + index = -1 + for id_, idx in self.inverse_index.items(): + if self.executor_conf_params[id_]['name'] == parameters_name: + index = idx + break + return index def __len__(self) -> int: diff --git a/spyder/plugins/run/tests/test_run.py b/spyder/plugins/run/tests/test_run.py index b1d88dcfd78..5c7e6552f3c 100644 --- a/spyder/plugins/run/tests/test_run.py +++ b/spyder/plugins/run/tests/test_run.py @@ -597,9 +597,9 @@ def test_run_plugin(qtbot, run_mock): assert test_executor_name == executor_name assert handler == 'both' - # Ensure that the executor run configuration is transient - assert exec_conf['uuid'] is None - assert exec_conf['name'] is None + # Ensure that the executor run configuration was saved + assert exec_conf['uuid'] is not None + assert exec_conf['name'] == "Custom" # Check that the configuration parameters are the ones defined by the # dialog @@ -612,23 +612,37 @@ def test_run_plugin(qtbot, run_mock): # Assert that the run_exec dispatcher works for the specific combination assert handler_name == f'{ext}_{context["identifier"]}' - # Assert that the run configuration gets registered without executor params + # Assert that the run configuration gets registered stored_run_params = run.get_last_used_executor_parameters(run_conf_uuid) + current_exec_uuid = exec_conf['uuid'] assert stored_run_params['executor'] == test_executor_name - assert stored_run_params['selected'] is None + assert stored_run_params['selected'] == current_exec_uuid - # The configuration gets run again - with qtbot.waitSignal(executor_1.sig_run_invocation) as sig: - run_act.trigger() + # Spawn the configuration dialog + run_act = run.get_action(RunActions.Run) + run_act.trigger() + + dialog = container.dialog + with qtbot.waitSignal(dialog.finished, timeout=200000): + # Select the first configuration again + conf_combo.setCurrentIndex(0) + + # Change some options + conf_widget = dialog.current_widget + conf_widget.widgets['opt1'].setChecked(False) - # Assert that the transient run executor parameters reverted to the - # default ones + # Execute the configuration + buttons = dialog.bbox.buttons() + run_btn = buttons[2] + with qtbot.waitSignal(executor_1.sig_run_invocation) as sig: + qtbot.mouseClick(run_btn, Qt.LeftButton) + + # Assert that changes took effect and that the run executor parameters were + # saved in the Custom config _, _, _, exec_conf = sig.args[0] params = exec_conf['params'] - working_dir = params['working_dir'] - assert working_dir['source'] == WorkingDirSource.ConfigurationDirectory - assert working_dir['path'] == '' - assert params['executor_params'] == default_conf + assert params['executor_params']['opt1'] == False + assert exec_conf['uuid'] == current_exec_uuid # Focus into another configuration exec_provider_2.switch_focus('ext3', 'AnotherSuperContext') @@ -737,8 +751,7 @@ def test_run_plugin(qtbot, run_mock): with qtbot.waitSignal(dialog.finished, timeout=200000): conf_combo = dialog.configuration_combo exec_combo = dialog.executor_combo - store_params_cb = dialog.store_params_cb - store_params_text = dialog.store_params_text + name_params_text = dialog.name_params_text # Modify some options conf_widget = dialog.current_widget @@ -746,8 +759,7 @@ def test_run_plugin(qtbot, run_mock): conf_widget.widgets['name_2'].setChecked(False) # Make sure that the custom configuration is stored - store_params_cb.setChecked(True) - store_params_text.setText('CustomParams') + name_params_text.setText('CustomParams') # Execute the configuration buttons = dialog.bbox.buttons() diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 1eb38ccd299..7fd9235b9a4 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -17,9 +17,8 @@ from qtpy.QtCore import QSize, Qt, Signal from qtpy.QtGui import QFontMetrics from qtpy.QtWidgets import ( - QCheckBox, QDialog, QDialogButtonBox, QGridLayout, QGroupBox, QHBoxLayout, - QLabel, QLineEdit, QLayout, QRadioButton, QStackedWidget, QVBoxLayout, - QWidget) + QDialog, QDialogButtonBox, QGridLayout, QGroupBox, QHBoxLayout, QLabel, + QLineEdit, QLayout, QRadioButton, QStackedWidget, QVBoxLayout, QWidget) import qstylizer.style # Local imports @@ -63,8 +62,6 @@ CW_DIR = _("The current working directory") FIXED_DIR = _("The following directory:") -STORE_PARAMS = _('Store current configuration as:') - class RunDialogStatus: Close = 0 @@ -447,7 +444,7 @@ def setup(self): executor_label = QLabel(_("Run this file in:")) self.executor_combo = SpyderComboBox(self) - parameters_label = QLabel(_("Preset run parameters:")) + parameters_label = QLabel(_("Preset configuration:")) self.parameters_combo = SpyderComboBox(self) self.executor_combo.setMinimumWidth(250) @@ -499,16 +496,12 @@ def setup(self): wdir_layout.addLayout(fixed_dir_layout) # --- Store new custom configuration - self.store_params_cb = QCheckBox(STORE_PARAMS) - self.store_params_text = QLineEdit(self) - store_params_layout = QHBoxLayout() - store_params_layout.addWidget(self.store_params_cb) - store_params_layout.addWidget(self.store_params_text) - parameters_layout.addLayout(store_params_layout) - - self.store_params_cb.toggled.connect(self.store_params_text.setEnabled) - self.store_params_text.setPlaceholderText(_('My configuration name')) - self.store_params_text.setEnabled(False) + name_params_label = QLabel(_("Save current configuration as:")) + self.name_params_text = QLineEdit(self) + name_params_layout = QHBoxLayout() + name_params_layout.addWidget(name_params_label) + name_params_layout.addWidget(self.name_params_text) + parameters_layout.addLayout(name_params_layout) layout = self.add_widgets( self.header_label, @@ -638,14 +631,22 @@ def display_executor_configuration(self, index: int): if uuid not in self.run_conf_model: return - stored_param = self.run_conf_model.get_run_configuration_parameters( - uuid, executor_name) + stored_params = self.run_conf_model.get_run_configuration_parameters( + uuid, executor_name)['params'] - self.parameter_model.set_parameters(stored_param['params']) + # Only show global parameters (i.e. those with file_uuid = None) or + # those that correspond to the current file. + stored_params = { + k:v for (k, v) in stored_params.items() + if v.get("file_uuid") in [None, uuid] + } + self.parameter_model.set_parameters(stored_params) selected_params = self.run_conf_model.get_last_used_execution_params( uuid, executor_name) - index = self.parameter_model.get_parameters_index(selected_params) + index = self.parameter_model.get_parameters_index_by_uuid( + selected_params + ) if self.parameters_combo.count() == 0: self.index_to_select = index @@ -661,18 +662,50 @@ def reset_btn_clicked(self): self.parameters_combo.setCurrentIndex(-1) index = self.executor_combo.currentIndex() self.display_executor_configuration(index) - self.store_params_text.setText('') - self.store_params_cb.setChecked(False) + self.name_params_text.setText('') def run_btn_clicked(self): self.status |= RunDialogStatus.Run self.accept() + def get_configuration( + self + ) -> Tuple[str, str, ExtendedRunExecutionParameters, bool]: + + return self.saved_conf + + # ---- Qt methods def accept(self) -> None: self.status |= RunDialogStatus.Save + default_conf = self.current_widget.get_default_configuration() widget_conf = self.current_widget.get_configuration() + # Check if config is named + given_name = self.name_params_text.text() + if not given_name and widget_conf != default_conf: + # If parameters are not named and are different from the default + # ones, we always save them in a config named "Custom". This avoids + # the hassle of asking users to provide a name when they want to + # customize the config. + given_name = _("Custom") + + # Get index associated with config + if given_name: + idx = self.parameter_model.get_parameters_index_by_name(given_name) + else: + idx = self.parameters_combo.currentIndex() + + # Get uuid and name from index + if idx == -1: + # This means that there are no saved parameters for given_name, so + # we need to generate a new uuid for them. + uuid = str(uuid4()) + name = given_name + else: + # Retrieve uuid and name from our config system + uuid, name = self.parameter_model.get_parameters_uuid_name(idx) + path = None source = None if self.file_dir_radio.isChecked(): @@ -688,39 +721,26 @@ def accept(self) -> None: exec_params = RunExecutionParameters( working_dir=cwd_opts, executor_params=widget_conf) - uuid, name = self.parameter_model.get_parameters_uuid_name( - self.parameters_combo.currentIndex() + metadata_info = self.run_conf_model.get_metadata( + self.configuration_combo.currentIndex() ) - if self.store_params_cb.isChecked(): - uuid = str(uuid4()) - name = self.store_params_text.text() - if name == '': - date_str = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") - name = f'Configuration-{date_str}' - ext_exec_params = ExtendedRunExecutionParameters( - uuid=uuid, name=name, params=exec_params + uuid=uuid, + name=name, + params=exec_params, + file_uuid=metadata_info['uuid'] ) - executor_name, _ = self.executors_model.get_selected_run_executor( + + executor_name, __ = self.executors_model.get_selected_run_executor( self.executor_combo.currentIndex() ) - metadata_info = self.run_conf_model.get_metadata( - self.configuration_combo.currentIndex() - ) self.saved_conf = (metadata_info['uuid'], executor_name, ext_exec_params) return super().accept() - def get_configuration( - self - ) -> Tuple[str, str, ExtendedRunExecutionParameters, bool]: - - return self.saved_conf - - # ---- Qt methods def showEvent(self, event): """Adjustments when the widget is shown.""" if not self._is_shown: From e52c13fd5175ea6693e192ccd919af39705c6c59 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 31 Oct 2023 10:51:53 -0500 Subject: [PATCH 05/55] Run: Initial UX improvements for ExecutionParametersDialog and conf page - For the dialog: * Move configuration name to the beginning. * Simplify layout of extension and context comboboxes. * Show localized executor name in dialog's title. * Prevent saving configs without a name. - For the conf page: * Don't show configs set for files. * Code style improvements. - Also, remove several unused constants. --- .../plugins/ipythonconsole/widgets/config.py | 8 -- spyder/plugins/run/confpage.py | 73 +++++++++--- spyder/plugins/run/widgets.py | 112 +++++++++++------- 3 files changed, 122 insertions(+), 71 deletions(-) diff --git a/spyder/plugins/ipythonconsole/widgets/config.py b/spyder/plugins/ipythonconsole/widgets/config.py index bcd2bfdfa60..1df675f2d17 100644 --- a/spyder/plugins/ipythonconsole/widgets/config.py +++ b/spyder/plugins/ipythonconsole/widgets/config.py @@ -22,19 +22,11 @@ # Main constants -RUN_DEFAULT_CONFIG = _("Run file with default configuration") -RUN_CUSTOM_CONFIG = _("Run file with custom configuration") CURRENT_INTERPRETER = _("Execute in current console") DEDICATED_INTERPRETER = _("Execute in a dedicated console") -SYSTERM_INTERPRETER = _("Execute in an external system terminal") CLEAR_ALL_VARIABLES = _("Remove all variables before execution") CONSOLE_NAMESPACE = _("Run in console's namespace instead of an empty one") POST_MORTEM = _("Directly enter debugging when errors appear") -INTERACT = _("Interact with the Python console after execution") -FILE_DIR = _("The directory of the file being executed") -CW_DIR = _("The current working directory") -FIXED_DIR = _("The following directory:") -ALWAYS_OPEN_FIRST_RUN = _("Always show %s on a first file run") class IPythonConfigOptions(RunExecutorConfigurationGroup): diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index 6b4a44f5201..6643e743f72 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -85,8 +85,9 @@ def reset_plain(self): def show_editor(self, new=False, clone=False): extension, context, params = None, None, None - extensions, contexts, executor_params = ( - self._parent.get_executor_configurations()) + extensions, contexts, plugin_name, executor_params = ( + self._parent.get_executor_configurations() + ) if not new: index = self.currentIndex().row() @@ -94,13 +95,24 @@ def show_editor(self, new=False, clone=False): (extension, context, params) = model[index] self.dialog = ExecutionParametersDialog( - self, executor_params, extensions, contexts, params, - extension, context) + self, + plugin_name, + executor_params, + extensions, + contexts, + params, + extension, + context, + new + ) self.dialog.setup() self.dialog.finished.connect( - functools.partial(self.process_run_dialog_result, - new=new, clone=clone, params=params)) + functools.partial( + self.process_run_dialog_result, + new=new, clone=clone, params=params + ) + ) if not clone: self.dialog.open() @@ -113,8 +125,11 @@ def process_run_dialog_result(self, result, new=False, if status == RunDialogStatus.Close: return - (extension, context, - new_executor_params) = self.dialog.get_configuration() + conf = self.dialog.get_configuration() + if conf is None: + return + else: + extension, context, new_executor_params = conf if not new and clone: new_executor_params["uuid"] = str(uuid4()) @@ -254,19 +269,29 @@ def executor_index_changed(self, index: int): executor, available_inputs = self.executor_model.selected_executor( index) container = self.plugin_container + executor_conf_params = self.all_executor_model.get(executor, {}) if executor_conf_params == {}: for (ext, context) in available_inputs: - params = ( - container.get_executor_configuration_parameters( - executor, ext, context)) + params = container.get_executor_configuration_parameters( + executor, ext, context + ) params = params["params"] for exec_params_id in params: exec_params = params[exec_params_id] - executor_conf_params[ - (ext, context, exec_params_id)] = exec_params + + # Don't display configs set for specific files. Here + # users are allowed to configure global configs, i.e. those + # that can be used by any file. + if exec_params["file_uuid"] is not None: + continue + + params_key = (ext, context, exec_params_id) + executor_conf_params[params_key] = exec_params + self.default_executor_conf_params[executor] = deepcopy( executor_conf_params) + self.all_executor_model[executor] = deepcopy(executor_conf_params) self.table_model.set_parameters(executor_conf_params) @@ -286,14 +311,16 @@ def get_executor_configurations(self) -> Dict[ str, SupportedExecutionRunConfiguration]: exec_index = self.executor_combo.currentIndex() executor_name, available_inputs = ( - self.executor_model.selected_executor(exec_index)) + self.executor_model.selected_executor(exec_index) + ) executor_params: Dict[str, SupportedExecutionRunConfiguration] = {} extensions: Set[str] = set({}) contexts: Dict[str, List[str]] = {} conf_indices = ( - self.plugin_container.executor_model.executor_configurations) + self.plugin_container.executor_model.executor_configurations + ) for _input in available_inputs: extension, context = _input @@ -306,9 +333,19 @@ def get_executor_configurations(self) -> Dict[ conf = executors[executor_name] executor_params[_input] = conf - contexts = {ext: move_file_to_front(ctx) - for ext, ctx in contexts.items()} - return list(sorted(extensions)), contexts, executor_params + contexts = { + ext: move_file_to_front(ctx) for ext, ctx in contexts.items() + } + + # Localized version of the executor + executor_loc_name = self.main.get_plugin(executor_name).get_name() + + return ( + list(sorted(extensions)), + contexts, + executor_loc_name, + executor_params + ) def create_new_configuration(self): self.params_table.show_editor(new=True) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 7fd9235b9a4..df7c8321d0d 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -7,7 +7,6 @@ """Run dialogs and widgets and data models.""" # Standard library imports -from datetime import datetime import os.path as osp from typing import Optional, Tuple, List, Dict from uuid import uuid4 @@ -36,33 +35,11 @@ from spyder.utils.stylesheet import AppStyle - # Main constants -RUN_DEFAULT_CONFIG = _("Run file with default configuration") -RUN_CUSTOM_CONFIG = _("Run file with custom configuration") -CURRENT_INTERPRETER = _("Execute in current console") -DEDICATED_INTERPRETER = _("Execute in a dedicated console") -SYSTERM_INTERPRETER = _("Execute in an external system terminal") - -CURRENT_INTERPRETER_OPTION = 'default/interpreter/current' -DEDICATED_INTERPRETER_OPTION = 'default/interpreter/dedicated' -SYSTERM_INTERPRETER_OPTION = 'default/interpreter/systerm' - -WDIR_USE_SCRIPT_DIR_OPTION = 'default/wdir/use_script_directory' -WDIR_USE_CWD_DIR_OPTION = 'default/wdir/use_cwd_directory' -WDIR_USE_FIXED_DIR_OPTION = 'default/wdir/use_fixed_directory' -WDIR_FIXED_DIR_OPTION = 'default/wdir/fixed_directory' - -CLEAR_ALL_VARIABLES = _("Remove all variables before execution") -CONSOLE_NAMESPACE = _("Run in console's namespace instead of an empty one") -POST_MORTEM = _("Directly enter debugging when errors appear") -INTERACT = _("Interact with the Python console after execution") - FILE_DIR = _("The directory of the configuration being executed") CW_DIR = _("The current working directory") FIXED_DIR = _("The following directory:") - class RunDialogStatus: Close = 0 Save = 1 @@ -152,21 +129,25 @@ class ExecutionParametersDialog(BaseRunConfigDialog): def __init__( self, parent, + executor_name, executor_params: Dict[Tuple[str, str], SupportedExecutionRunConfiguration], extensions: Optional[List[str]] = None, contexts: Optional[Dict[str, List[str]]] = None, default_params: Optional[ExtendedRunExecutionParameters] = None, extension: Optional[str] = None, - context: Optional[str] = None + context: Optional[str] = None, + new_config: bool = False ): super().__init__(parent, True) + self.executor_name = executor_name self.executor_params = executor_params self.default_params = default_params self.extensions = extensions or [] self.contexts = contexts or {} self.extension = extension self.context = context + self.new_config = new_config self.parameters_name = None if default_params is not None: @@ -174,8 +155,12 @@ def __init__( self.current_widget = None self.status = RunDialogStatus.Close + self.saved_conf = None + # ---- Public methods + # ------------------------------------------------------------------------- def setup(self): + # Widgets ext_combo_label = QLabel(_("Select a file extension:")) context_combo_label = QLabel(_("Select a run context:")) @@ -186,6 +171,19 @@ def setup(self): self.context_combo = SpyderComboBox(self) self.context_combo.currentIndexChanged.connect(self.context_changed) + self.extension_combo.setMinimumWidth(150) + self.context_combo.setMinimumWidth(150) + + ext_context_g_layout = QGridLayout() + ext_context_g_layout.addWidget(ext_combo_label, 0, 0) + ext_context_g_layout.addWidget(self.extension_combo, 0, 1) + ext_context_g_layout.addWidget(context_combo_label, 1, 0) + ext_context_g_layout.addWidget(self.context_combo, 1, 1) + + ext_context_layout = QHBoxLayout() + ext_context_layout.addLayout(ext_context_g_layout) + ext_context_layout.addStretch(1) + self.stack = QStackedWidget() self.executor_group = QGroupBox(_("Executor parameters")) executor_layout = QVBoxLayout(self.executor_group) @@ -205,7 +203,7 @@ def setup(self): self.fixed_dir_radio = QRadioButton(FIXED_DIR) fixed_dir_layout.addWidget(self.fixed_dir_radio) - self.wd_edit = QLineEdit() + self.wd_edit = QLineEdit(self) self.fixed_dir_radio.toggled.connect(self.wd_edit.setEnabled) self.wd_edit.setEnabled(False) fixed_dir_layout.addWidget(self.wd_edit) @@ -214,35 +212,47 @@ def setup(self): triggered=self.select_directory, icon=ima.icon('DirOpenIcon'), tip=_("Select directory") - ) + ) fixed_dir_layout.addWidget(browse_btn) wdir_layout.addLayout(fixed_dir_layout) - params_name_label = QLabel(_('Configuration name:')) - self.store_params_text = QLineEdit() + if self.new_config: + params_name_text = _("Save configuration as:") + else: + params_name_text = _("Configuration name:") + + params_name_label = QLabel(params_name_text) + self.store_params_text = QLineEdit(self) + self.store_params_text.setMinimumWidth(300) store_params_layout = QHBoxLayout() store_params_layout.addWidget(params_name_label) store_params_layout.addWidget(self.store_params_text) - - self.store_params_text.setPlaceholderText(_('My configuration name')) + store_params_layout.addStretch(1) all_group = QVBoxLayout() all_group.addWidget(self.executor_group) all_group.addWidget(self.wdir_group) - all_group.addLayout(store_params_layout) - layout = self.add_widgets(ext_combo_label, self.extension_combo, - context_combo_label, self.context_combo, - 10, all_group) + # Final layout + layout = self.add_widgets( + store_params_layout, + 15, + ext_context_layout, + 10, + all_group + ) - widget_dialog = QWidget() + widget_dialog = QWidget(self) widget_dialog.setMinimumWidth(600) widget_dialog.setLayout(layout) scroll_layout = QVBoxLayout(self) scroll_layout.addWidget(widget_dialog) self.add_button_box(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.setWindowTitle(_("Run parameters")) + # Set title + self.setWindowTitle( + _("New run configuration for: {}").format(self.executor_name) + ) self.extension_combo.addItems(self.extensions) @@ -370,6 +380,14 @@ def run_btn_clicked(self): def ok_btn_clicked(self): self.status |= RunDialogStatus.Save + def get_configuration( + self + ) -> Tuple[str, str, ExtendedRunExecutionParameters]: + + return self.saved_conf + + # ---- Qt methods + # ------------------------------------------------------------------------- def accept(self) -> None: self.status |= RunDialogStatus.Save widget_conf = self.current_widget.get_configuration() @@ -393,24 +411,25 @@ def accept(self) -> None: uuid = self.default_params['uuid'] else: uuid = str(uuid4()) + name = self.store_params_text.text() if name == '': - date_str = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") - name = f'Configuration-{date_str}' + self.store_params_text.setPlaceholderText( + _("Set a name here to proceed!") + ) + return ext_exec_params = ExtendedRunExecutionParameters( - uuid=uuid, name=name, params=exec_params + uuid=uuid, + name=name, + params=exec_params, + file_uuid=None ) self.saved_conf = (self.selected_extension, self.selected_context, ext_exec_params) - super().accept() - def get_configuration( - self - ) -> Tuple[str, str, ExtendedRunExecutionParameters]: - - return self.saved_conf + super().accept() class RunDialog(BaseRunConfigDialog, SpyderFontsMixin): @@ -433,6 +452,7 @@ def __init__( self._is_shown = False # ---- Public methods + # ------------------------------------------------------------------------- def setup(self): # Header self.header_label = QLabel(self) @@ -675,6 +695,7 @@ def get_configuration( return self.saved_conf # ---- Qt methods + # ------------------------------------------------------------------------- def accept(self) -> None: self.status |= RunDialogStatus.Save @@ -767,6 +788,7 @@ def showEvent(self, event): super().showEvent(event) # ---- Private methods + # ------------------------------------------------------------------------- @property def _stylesheet(self): css = qstylizer.style.StyleSheet() From bdbef374fb85f4cde6ee7f29db90134d4e0cd013 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 3 Nov 2023 09:54:08 -0500 Subject: [PATCH 06/55] Run: Fix deleting parameters from its config page Also, disable delete and clone buttons if there's no index selected in RunParametersTableView and add a test for this new functionality. --- spyder/plugins/run/confpage.py | 54 +++++++++++++++----- spyder/plugins/run/container.py | 74 +++++++++++++++++++++++----- spyder/plugins/run/tests/test_run.py | 17 +++++++ 3 files changed, 121 insertions(+), 24 deletions(-) diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index 6643e743f72..0ab7a483824 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -53,7 +53,8 @@ def __init__(self, parent, model): self.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.horizontalHeader().setSectionResizeMode( - 1, QHeaderView.Stretch) + 1, QHeaderView.Stretch + ) self.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) def focusInEvent(self, e): @@ -164,12 +165,15 @@ class RunConfigPage(PluginConfigPage): """Default Run Settings configuration page.""" def setup_page(self): + self._params_to_delete = {} + self.plugin_container: RunContainer = self.plugin.get_container() self.executor_model = RunExecutorNamesListModel( self, self.plugin_container.executor_model) self.table_model = ExecutorRunParametersTableModel(self) self.table_model.sig_data_changed.connect( - lambda: self.set_modified(True)) + lambda: self.set_modified(True) + ) self.all_executor_model: Dict[ str, Dict[Tuple[str, str, str], @@ -186,14 +190,15 @@ def setup_page(self): ) about_label.setWordWrap(True) + self.params_table = RunParametersTableView(self, self.table_model) + self.params_table.setMaximumHeight(180) + self.executor_combo = SpyderComboBox(self) self.executor_combo.currentIndexChanged.connect( - self.executor_index_changed) + self.executor_index_changed + ) self.executor_combo.setModel(self.executor_model) - self.params_table = RunParametersTableView(self, self.table_model) - self.params_table.setMaximumHeight(180) - params_group = QGroupBox(_('Available execution parameters')) params_layout = QVBoxLayout(params_group) params_layout.addWidget(self.params_table) @@ -299,12 +304,16 @@ def executor_index_changed(self, index: int): self.set_clone_delete_btn_status() def set_clone_delete_btn_status(self): - status = self.table_model.rowCount() != 0 + status = ( + self.table_model.rowCount() != 0 + and self.params_table.currentIndex().isValid() + ) + try: self.delete_configuration_btn.setEnabled(status) self.clone_configuration_btn.setEnabled(status) except AttributeError: - # Buttons might not exist yet + # Buttons might not be created yet pass def get_executor_configurations(self) -> Dict[ @@ -355,19 +364,28 @@ def clone_configuration(self): def delete_configuration(self): executor_name, _ = self.executor_model.selected_executor( - self.previous_executor_index) + self.previous_executor_index + ) index = self.params_table.currentIndex().row() conf_index = self.table_model.get_tuple_index(index) + executor_params = self.all_executor_model[executor_name] executor_params.pop(conf_index, None) + + if executor_name not in self._params_to_delete: + self._params_to_delete[executor_name] = [] + self._params_to_delete[executor_name].append(conf_index) + self.table_model.set_parameters(executor_params) self.table_model.reset_model() + self.set_modified(True) self.set_clone_delete_btn_status() def reset_to_default(self): self.all_executor_model = deepcopy(self.default_executor_conf_params) executor_name, _ = self.executor_model.selected_executor( - self.previous_executor_index) + self.previous_executor_index + ) executor_params = self.all_executor_model[executor_name] self.table_model.set_parameters(executor_params) self.table_model.reset_model() @@ -377,9 +395,11 @@ def reset_to_default(self): def apply_settings(self): prev_executor_info = self.table_model.get_current_view() previous_executor_name, _ = self.executor_model.selected_executor( - self.previous_executor_index) + self.previous_executor_index + ) self.all_executor_model[previous_executor_name] = prev_executor_info + # Save new parameters for executor in self.all_executor_model: executor_params = self.all_executor_model[executor] stored_execution_params: Dict[ @@ -400,4 +420,16 @@ def apply_settings(self): executor, extension, context, {'params': ext_ctx_list} ) + # Delete removed parameters + for executor in self._params_to_delete: + executor_params_to_delete = self._params_to_delete[executor] + + for key in executor_params_to_delete: + (extension, context, params_id) = key + self.plugin_container.delete_executor_configuration_parameters( + executor, extension, context, params_id + ) + + self._params_to_delete = {} + return {'parameters'} diff --git a/spyder/plugins/run/container.py b/spyder/plugins/run/container.py index b36c5052636..10038b14d66 100644 --- a/spyder/plugins/run/container.py +++ b/spyder/plugins/run/container.py @@ -246,11 +246,18 @@ def process_run_dialog_result(self, result): context_name = context['name'] context_id = getattr(RunContext, context_name) all_exec_params = self.get_executor_configuration_parameters( - executor_name, ext, context_id) + executor_name, + ext, + context_id + ) exec_params = all_exec_params['params'] exec_params[exec_uuid] = ext_params self.set_executor_configuration_parameters( - executor_name, ext, context_id, all_exec_params) + executor_name, + ext, + context_id, + all_exec_params, + ) last_used_conf = StoredRunConfigurationExecutor( executor=executor_name, selected=ext_params['uuid'] @@ -874,15 +881,16 @@ def get_executor_configuration_parameters( run configuration. """ - all_executor_params: Dict[ + all_execution_params: Dict[ str, - Dict[Tuple[str, str], - StoredRunExecutorParameters] + Dict[Tuple[str, str], StoredRunExecutorParameters] ] = self.get_conf('parameters', default={}) - executor_params = all_executor_params.get(executor_name, {}) + executor_params = all_execution_params.get(executor_name, {}) params = executor_params.get( - (extension, context_id), StoredRunExecutorParameters(params={})) + (extension, context_id), + StoredRunExecutorParameters(params={}) + ) return params @@ -909,16 +917,56 @@ def set_executor_configuration_parameters( A dictionary containing the run configuration parameters for the given executor. """ - all_executor_params: Dict[ + all_execution_params: Dict[ str, - Dict[Tuple[str, str], - StoredRunExecutorParameters] + Dict[Tuple[str, str], StoredRunExecutorParameters] ] = self.get_conf('parameters', default={}) - executor_params = all_executor_params.get(executor_name, {}) + executor_params = all_execution_params.get(executor_name, {}) executor_params[(extension, context_id)] = params - all_executor_params[executor_name] = executor_params - self.set_conf('parameters', all_executor_params) + all_execution_params[executor_name] = executor_params + + self.set_conf('parameters', all_execution_params) + + def delete_executor_configuration_parameters( + self, + executor_name: str, + extension: str, + context_id: str, + uuid: str + ): + """ + Delete an executor parameter set from our config system. + + Parameters + ---------- + executor_name: str + The identifier of the run executor. + extension: str + The file extension to register the configuration parameters for. + context_id: str + The context to register the configuration parameters for. + uuid: str + The run configuration identifier. + """ + all_execution_params: Dict[ + str, + Dict[Tuple[str, str], StoredRunExecutorParameters] + ] = self.get_conf('parameters', default={}) + + executor_params = all_execution_params[executor_name] + ext_ctx_params = executor_params[(extension, context_id)]['params'] + + for params_id in ext_ctx_params: + if params_id == uuid: + ext_ctx_params.pop(params_id, None) + break + + executor_params[(extension, context_id)]['params'] = ext_ctx_params + all_execution_params[executor_name] = executor_params + + self.set_conf('parameters', all_execution_params) + def get_last_used_executor_parameters( self, diff --git a/spyder/plugins/run/tests/test_run.py b/spyder/plugins/run/tests/test_run.py index 5c7e6552f3c..a7a1def76c3 100644 --- a/spyder/plugins/run/tests/test_run.py +++ b/spyder/plugins/run/tests/test_run.py @@ -790,6 +790,23 @@ def test_run_plugin(qtbot, run_mock): assert stored_run_params['executor'] == executor_name assert stored_run_params['selected'] == exec_conf_uuid + # Check deleting a parameters config + current_exec_params = container.get_conf('parameters')[executor_name] + current_ext_ctx_params = ( + current_exec_params[('ext3', RunContext.AnotherSuperContext)]['params'] + ) + assert current_ext_ctx_params != {} # Check params to delete are present + + container.delete_executor_configuration_parameters( + executor_name, 'ext3', RunContext.AnotherSuperContext, exec_conf_uuid + ) + + new_exec_params = container.get_conf('parameters')[executor_name] + new_ext_ctx_params = ( + new_exec_params[('ext3', RunContext.AnotherSuperContext)]['params'] + ) + assert new_ext_ctx_params == {} + # Test teardown functions executor_1.on_run_teardown(run) executor_2.on_run_teardown(run) From 1dcf7d57a8b8f2ddbe3352476a8c6554f38cffd7 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 3 Nov 2023 17:55:26 -0500 Subject: [PATCH 07/55] Widgets: Make HoverRowsTableView highlight a hovered row on its own --- spyder/plugins/shortcuts/widgets/table.py | 3 +- spyder/widgets/elementstable.py | 2 +- spyder/widgets/helperwidgets.py | 48 +++++++++++++++++------ 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/spyder/plugins/shortcuts/widgets/table.py b/spyder/plugins/shortcuts/widgets/table.py index 71cdb54f231..0b886016faf 100644 --- a/spyder/plugins/shortcuts/widgets/table.py +++ b/spyder/plugins/shortcuts/widgets/table.py @@ -646,7 +646,7 @@ def reset(self): class ShortcutsTable(HoverRowsTableView): def __init__(self, parent=None): - HoverRowsTableView.__init__(self, parent) + HoverRowsTableView.__init__(self, parent, custom_delegate=True) self._parent = parent self.finder = None self.shortcut_data = None @@ -671,6 +671,7 @@ def __init__(self, parent=None): self.verticalHeader().hide() + # To highlight the entire row on hover self.sig_hover_index_changed.connect( self.itemDelegate().on_hover_index_changed ) diff --git a/spyder/widgets/elementstable.py b/spyder/widgets/elementstable.py index fa80115d67d..75178d2b427 100644 --- a/spyder/widgets/elementstable.py +++ b/spyder/widgets/elementstable.py @@ -154,7 +154,7 @@ def get_info_repr(self, element: Element) -> str: class ElementsTable(HoverRowsTableView): def __init__(self, parent: Optional[QWidget], elements: List[Element]): - HoverRowsTableView.__init__(self, parent) + HoverRowsTableView.__init__(self, parent, custom_delegate=True) self.elements = elements # Check for additional features diff --git a/spyder/widgets/helperwidgets.py b/spyder/widgets/helperwidgets.py index ad0033d406c..82c83f21bd6 100644 --- a/spyder/widgets/helperwidgets.py +++ b/spyder/widgets/helperwidgets.py @@ -26,7 +26,7 @@ QAction, QApplication, QCheckBox, QLineEdit, QMessageBox, QSpacerItem, QStyle, QStyledItemDelegate, QStyleOptionFrame, QStyleOptionViewItem, QTableView, QToolButton, QToolTip, QVBoxLayout, QWidget, QHBoxLayout, - QLabel, QFrame) + QLabel, QFrame, QItemDelegate) # Local imports from spyder.api.config.fonts import SpyderFontType, SpyderFontsMixin @@ -737,14 +737,7 @@ def _apply_stylesheet(self, focus): class HoverRowsTableView(QTableView): - """ - QTableView subclass that can highlight an entire row when hovered. - - Notes - ----- - * Classes that inherit from this one need to connect a slot to - sig_hover_index_changed that handles how the row is painted. - """ + """QTableView subclass that can highlight an entire row when hovered.""" sig_hover_index_changed = Signal(object) """ @@ -756,7 +749,7 @@ class HoverRowsTableView(QTableView): QModelIndex that has changed on hover. """ - def __init__(self, parent): + def __init__(self, parent, custom_delegate=False): QTableView.__init__(self, parent) # For mouseMoveEvent @@ -766,10 +759,13 @@ def __init__(self, parent): # over the widget. css = qstylizer.style.StyleSheet() css["QTableView::item"].setValues( - backgroundColor=f"{QStylePalette.COLOR_BACKGROUND_1}" + backgroundColor=QStylePalette.COLOR_BACKGROUND_1 ) self._stylesheet = css.toString() + if not custom_delegate: + self._set_delegate() + # ---- Qt methods def mouseMoveEvent(self, event): self._inform_hover_index_changed(event) @@ -793,6 +789,36 @@ def _inform_hover_index_changed(self, event): self.sig_hover_index_changed.emit(index) self.viewport().update() + def _set_delegate(self): + """ + Set a custom item delegate that can highlight the current row when + hovered. + """ + + class HoverRowDelegate(QItemDelegate): + + def __init__(self, parent): + super().__init__(parent) + self._hovered_row = -1 + + def on_hover_index_changed(self, index): + self._hovered_row = index.row() + + def paint(self, painter, option, index): + # This paints the entire row associated to the delegate when + # it's hovered. + if index.row() == self._hovered_row: + painter.fillRect( + option.rect, QColor(QStylePalette.COLOR_BACKGROUND_3) + ) + + super().paint(painter, option, index) + + self.setItemDelegate(HoverRowDelegate(self)) + self.sig_hover_index_changed.connect( + self.itemDelegate().on_hover_index_changed + ) + def test_msgcheckbox(): from spyder.utils.qthelpers import qapplication From 448cc852429248db2c9598851b1e638fcb2ae0d2 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 3 Nov 2023 18:11:31 -0500 Subject: [PATCH 08/55] Run: Make RunParametersTableView highlight its rows on hover - Also, improve the main label of its config page and fix the name of one of its actions. --- spyder/plugins/run/confpage.py | 13 +++++++------ spyder/plugins/run/container.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index 0ab7a483824..d00ca6d3aa0 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -15,8 +15,8 @@ # Third party imports from qtpy.QtCore import Qt from qtpy.QtWidgets import (QGroupBox, QLabel, QVBoxLayout, - QTableView, QAbstractItemView, QPushButton, - QGridLayout, QHeaderView, QWidget) + QAbstractItemView, QPushButton, QGridLayout, + QHeaderView, QWidget) # Local imports from spyder.api.preferences import PluginConfigPage @@ -29,7 +29,7 @@ RunExecutorNamesListModel, ExecutorRunParametersTableModel) from spyder.plugins.run.widgets import ( ExecutionParametersDialog, RunDialogStatus) - +from spyder.widgets.helperwidgets import HoverRowsTableView def move_file_to_front(contexts: List[str]) -> List[str]: @@ -38,7 +38,7 @@ def move_file_to_front(contexts: List[str]) -> List[str]: return contexts -class RunParametersTableView(QTableView): +class RunParametersTableView(HoverRowsTableView): def __init__(self, parent, model): super().__init__(parent) self._parent = parent @@ -184,8 +184,9 @@ def setup_page(self): ExtendedRunExecutionParameters]] = {} about_label = QLabel( - _("The following are the per-executor configuration settings used " - "for running. These options may be overriden using the " + _("The following are the global configuration settings of the " + "different plugins that can run files, cells or selections in " + "Spyder. These options can be overridden in the " "Configuration per file entry of the Run menu.") ) about_label.setWordWrap(True) diff --git a/spyder/plugins/run/container.py b/spyder/plugins/run/container.py index 10038b14d66..6c9b1dbaac5 100644 --- a/spyder/plugins/run/container.py +++ b/spyder/plugins/run/container.py @@ -74,7 +74,7 @@ def setup(self): self.configure_action = self.create_action( RunActions.Configure, - _('&Open run settings'), + _('&Configuration per file'), self.create_icon('run_settings'), tip=_('Run settings'), triggered=functools.partial( From db398f744ca03d9d9470be71ec84737dc8469333 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 4 Nov 2023 19:38:42 -0500 Subject: [PATCH 09/55] Run: Fix saving config parameters when the user has saved some before Also add test to cover that case. --- spyder/plugins/run/container.py | 11 +++++++- spyder/plugins/run/tests/test_run.py | 38 ++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/spyder/plugins/run/container.py b/spyder/plugins/run/container.py index 6c9b1dbaac5..07e5f4aec13 100644 --- a/spyder/plugins/run/container.py +++ b/spyder/plugins/run/container.py @@ -923,7 +923,16 @@ def set_executor_configuration_parameters( ] = self.get_conf('parameters', default={}) executor_params = all_execution_params.get(executor_name, {}) - executor_params[(extension, context_id)] = params + ext_ctx_params = executor_params.get((extension, context_id), {}) + + if ext_ctx_params: + # Update current parameters in case the user has already saved some + # before. + ext_ctx_params['params'].update(params['params']) + else: + # Create a new entry of executor parameters in case there isn't any + executor_params[(extension, context_id)] = params + all_execution_params[executor_name] = executor_params self.set_conf('parameters', all_execution_params) diff --git a/spyder/plugins/run/tests/test_run.py b/spyder/plugins/run/tests/test_run.py index a7a1def76c3..61fa5ecced3 100644 --- a/spyder/plugins/run/tests/test_run.py +++ b/spyder/plugins/run/tests/test_run.py @@ -25,10 +25,10 @@ # Local imports from spyder.plugins.run.api import ( RunExecutor, RunConfigurationProvider, RunConfigurationMetadata, Context, - RunConfiguration, SupportedExtensionContexts, + RunConfiguration, SupportedExtensionContexts, RunExecutionParameters, RunExecutorConfigurationGroup, ExtendedRunExecutionParameters, PossibleRunResult, RunContext, ExtendedContext, RunActions, run_execute, - WorkingDirSource) + WorkingDirOpts, WorkingDirSource, StoredRunExecutorParameters) from spyder.plugins.run.plugin import Run @@ -807,6 +807,40 @@ def test_run_plugin(qtbot, run_mock): ) assert new_ext_ctx_params == {} + # Check that adding new parameters preserves the previous ones + current_exec_params = container.get_conf('parameters')[executor_name] + assert ( + len(current_exec_params[('ext1', RunContext.RegisteredContext)] + ['params'] + ) == 1 + ) # Check that we have one config in this context + + new_exec_conf_uuid = str(uuid4()) + new_params = StoredRunExecutorParameters( + params={ + new_exec_conf_uuid: ExtendedRunExecutionParameters( + uuid=new_exec_conf_uuid, + name='Foo', + params=RunExecutionParameters( + WorkingDirOpts(source=WorkingDirSource.CurrentDirectory) + ), + file_uuid=None + ) + } + ) + + container.set_executor_configuration_parameters( + executor_name, 'ext1', RunContext.RegisteredContext, new_params + ) + + new_exec_params = container.get_conf('parameters')[executor_name] + + assert ( + len( + new_exec_params[('ext1', RunContext.RegisteredContext)]['params'] + ) == 2 + ) # Now we should have two configs in the same context + # Test teardown functions executor_1.on_run_teardown(run) executor_2.on_run_teardown(run) From f57ba7ba27f19cca9e90ff571ca9f87f9bf7b211 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 8 Nov 2023 08:17:12 -0500 Subject: [PATCH 10/55] Widgets: Add a new CollapsibleWidget to hide and show child widgets - This widget is based on superqt's QCollapsible widget. - It has several style customizations on top to match our style. --- spyder/utils/icon_manager.py | 6 +- spyder/utils/stylesheet.py | 7 ++- spyder/widgets/collapsible.py | 105 ++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 spyder/widgets/collapsible.py diff --git a/spyder/utils/icon_manager.py b/spyder/utils/icon_manager.py index 899cc5d5c3b..4cb82bd47ab 100644 --- a/spyder/utils/icon_manager.py +++ b/spyder/utils/icon_manager.py @@ -49,8 +49,7 @@ def __init__(self): self.ICONS_BY_EXTENSION = {} - # Magnification factors for attribute icons - # per platform + # Magnification factors for attribute icons per platform if sys.platform.startswith('linux'): self.BIG_ATTR_FACTOR = 1.0 self.SMALL_ATTR_FACTOR = 0.9 @@ -373,6 +372,9 @@ def __init__(self): 'print.single_page': [('mdi.file-document-outline',), {'color': self.MAIN_FG_COLOR}], 'print.all_pages': [('mdi.file-document-multiple-outline',), {'color': self.MAIN_FG_COLOR}], 'print.page_setup': [('mdi.ruler-square',), {'color': self.MAIN_FG_COLOR}], + # --- For our collapsed widget + 'collapsed': [('mdi.chevron-right',), {'color': self.MAIN_FG_COLOR, 'scale_factor': 1.3}], + 'expanded': [('mdi.chevron-down',), {'color': self.MAIN_FG_COLOR, 'scale_factor': 1.3}], } def get_std_icon(self, name, size=None): diff --git a/spyder/utils/stylesheet.py b/spyder/utils/stylesheet.py index 969444b42f5..54b641ab990 100644 --- a/spyder/utils/stylesheet.py +++ b/spyder/utils/stylesheet.py @@ -64,6 +64,9 @@ def ComboBoxMinHeight(cls): return min_height + # Padding for content inside an element of higher hierarchy + InnerContentPadding = 5 * MarginSize + # ============================================================================= # ---- Base stylesheet class @@ -506,7 +509,7 @@ def set_stylesheet(self): # Main constants css = self.get_stylesheet() - self.color_tabs_separator = f'{Gray.B70}' + self.color_tabs_separator = Gray.B70 if is_dark_interface(): self.color_selected_tab = f'{QStylePalette.COLOR_ACCENT_2}' @@ -648,7 +651,7 @@ def set_stylesheet(self): # Remove border and add padding for content inside tabs css['QTabWidget::pane'].setValues( border='0px', - padding='15px', + padding=f'{AppStyle.InnerContentPadding}px', ) diff --git a/spyder/widgets/collapsible.py b/spyder/widgets/collapsible.py new file mode 100644 index 00000000000..a016f0b103f --- /dev/null +++ b/spyder/widgets/collapsible.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Collapsible widget to hide and show child widgets.""" + +import qstylizer.style +from superqt import QCollapsible + +from spyder.utils.icon_manager import ima +from spyder.utils.palette import QStylePalette +from spyder.utils.stylesheet import AppStyle + + +class CollapsibleWidget(QCollapsible): + """Collapsible widget to hide and show child widgets.""" + + def __init__(self, parent=None, title=""): + super().__init__(title=title, parent=parent) + + # Align widget to the left to text before or after it (don't know why + # this is necessary). + self.layout().setContentsMargins(5, 0, 0, 0) + + # Remove spacing between toggle button and contents area + self.layout().setSpacing(0) + + # Set icons + self.setCollapsedIcon(ima.icon("collapsed")) + self.setExpandedIcon(ima.icon("expanded")) + + # To change the style only of these widgets + self._toggle_btn.setObjectName("collapsible-toggle") + self.content().setObjectName("collapsible-content") + + # Add padding to the inside content + self.content().layout().setContentsMargins( + *((AppStyle.InnerContentPadding,) * 4) + ) + + # Set stylesheet + self._css = self._generate_stylesheet() + self.setStyleSheet(self._css.toString()) + + # Signals + self.toggled.connect(self._on_toggled) + + def set_content_bottom_margin(self, bottom_margin): + """Set bottom margin of the content area to `bottom_margin`.""" + margins = self.content().layout().contentsMargins() + margins.setBottom(bottom_margin) + self.content().layout().setContentsMargins(margins) + + def _generate_stylesheet(self): + """Generate base stylesheet for this widget.""" + css = qstylizer.style.StyleSheet() + + # --- Style for the header button + css["QPushButton#collapsible-toggle"].setValues( + # Increase padding (the default one is too small). + padding=f"{2 * AppStyle.MarginSize}px", + # Make it a bit different from a default QPushButton to not drag + # the same amount of attention to it. + backgroundColor=QStylePalette.COLOR_BACKGROUND_3 + ) + + # Make hover color match the change of background color above + css["QPushButton#collapsible-toggle:hover"].setValues( + backgroundColor=QStylePalette.COLOR_BACKGROUND_4, + ) + + # --- Style for the contents area + css["QWidget#collapsible-content"].setValues( + # Remove top border to make it appear attached to the header button + borderTop="0px", + # Add border to the other edges + border=f'1px solid {QStylePalette.COLOR_BACKGROUND_4}', + # Add border radius to the bottom to make it match the style of our + # other widgets. + borderBottomLeftRadius=f'{QStylePalette.SIZE_BORDER_RADIUS}', + borderBottomRightRadius=f'{QStylePalette.SIZE_BORDER_RADIUS}', + ) + + return css + + def _on_toggled(self, state): + """Adjustments when the button is toggled.""" + if state: + # Remove bottom rounded borders from the header when the widget is + # expanded. + self._css["QPushButton#collapsible-toggle"].setValues( + borderBottomLeftRadius='0px', + borderBottomRightRadius='0px', + ) + else: + # Restore bottom rounded borders to the header when the widget is + # collapsed. + self._css["QPushButton#collapsible-toggle"].setValues( + borderBottomLeftRadius=f'{QStylePalette.SIZE_BORDER_RADIUS}', + borderBottomRightRadius=f'{QStylePalette.SIZE_BORDER_RADIUS}', + ) + + self.setStyleSheet(self._css.toString()) From 51063c84c144f80cca270444cc09062653b5c29e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 8 Nov 2023 09:03:10 -0500 Subject: [PATCH 11/55] Run: Group widgets to customize config into a collapsible one in RunDialog - Also, move QLineEdit to name the config to the top and show it inside a QGroupBox. - And move extra spacing between QGroupBoxes to the style of the Preferences dialog. --- .../preferences/widgets/configdialog.py | 5 + spyder/plugins/run/widgets.py | 121 +++++++++++++----- spyder/utils/stylesheet.py | 8 +- 3 files changed, 100 insertions(+), 34 deletions(-) diff --git a/spyder/plugins/preferences/widgets/configdialog.py b/spyder/plugins/preferences/widgets/configdialog.py index 5fd4a0b5497..0434d92f580 100644 --- a/spyder/plugins/preferences/widgets/configdialog.py +++ b/spyder/plugins/preferences/widgets/configdialog.py @@ -436,6 +436,11 @@ def _generate_stylesheet(self): border='0px', ) + # Add more spacing between QGroupBoxes than normal. + css.QGroupBox.setValues( + marginBottom='15px', + ) + return css @qdebounced(timeout=40) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index df7c8321d0d..c607f735c75 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -33,6 +33,7 @@ from spyder.utils.palette import QStylePalette from spyder.utils.qthelpers import create_toolbutton from spyder.utils.stylesheet import AppStyle +from spyder.widgets.collapsible import CollapsibleWidget # Main constants @@ -444,9 +445,11 @@ def __init__( disable_run_btn=False ): super().__init__(parent, disable_run_btn=disable_run_btn) + self.run_conf_model = run_conf_model self.executors_model = executors_model self.parameter_model = parameter_model + self.current_widget = None self.status = RunDialogStatus.Close self._is_shown = False @@ -454,14 +457,16 @@ def __init__( # ---- Public methods # ------------------------------------------------------------------------- def setup(self): - # Header + # --- Header self.header_label = QLabel(self) - self.header_label.setObjectName("header-label") + self.header_label.setObjectName("run-header-label") - # Hide this combobox to decrease the dialog complexity + # --- File combobox + # It's hidden by default to decrease the complexity of this dialog self.configuration_combo = SpyderComboBox(self) self.configuration_combo.hide() + # --- Executor and parameters widgets executor_label = QLabel(_("Run this file in:")) self.executor_combo = SpyderComboBox(self) parameters_label = QLabel(_("Preset configuration:")) @@ -478,19 +483,44 @@ def setup(self): executor_layout = QHBoxLayout() executor_layout.addLayout(executor_g_layout) - executor_layout.addStretch(1) + executor_layout.addStretch() + + # --- Configuration properties + config_props_group = QGroupBox(_("Configuration properties")) + config_props_layout = QVBoxLayout(config_props_group) + + # Increase margin between title and line edit below so this looks good + config_props_margins = config_props_layout.contentsMargins() + config_props_margins.setTop(12) + config_props_layout.setContentsMargins(config_props_margins) + + # Name to save custom configuration + name_params_label = QLabel(_("Name:")) + self.name_params_text = QLineEdit(self) + self.name_params_text.setMinimumWidth(250) + name_params_layout = QHBoxLayout() + name_params_layout.addWidget(name_params_label) + name_params_layout.addWidget(self.name_params_text) + name_params_layout.addStretch() + config_props_layout.addLayout(name_params_layout) + + # --- Runner settings self.stack = QStackedWidget() - parameters_layout = QVBoxLayout() + self.executor_group = QGroupBox(_("Runner settings")) + self.executor_group.setObjectName("run-executor-group") + + parameters_layout = QVBoxLayout(self.executor_group) parameters_layout.addWidget(self.stack) - self.executor_group = QGroupBox(_("Executor parameters")) - self.executor_group.setLayout(parameters_layout) + # Remove bottom margin because it adds unnecessary space + parameters_layout_margins = parameters_layout.contentsMargins() + parameters_layout_margins.setBottom(0) + parameters_layout.setContentsMargins(parameters_layout_margins) - # --- Working directory --- + # --- Working directory settings self.wdir_group = QGroupBox(_("Working directory settings")) - parameters_layout.addWidget(self.wdir_group) - + self.wdir_group.setObjectName("run-wdir-group") wdir_layout = QVBoxLayout(self.wdir_group) self.file_dir_radio = QRadioButton(FILE_DIR) @@ -515,24 +545,31 @@ def setup(self): fixed_dir_layout.addWidget(browse_btn) wdir_layout.addLayout(fixed_dir_layout) - # --- Store new custom configuration - name_params_label = QLabel(_("Save current configuration as:")) - self.name_params_text = QLineEdit(self) - name_params_layout = QHBoxLayout() - name_params_layout.addWidget(name_params_label) - name_params_layout.addWidget(self.name_params_text) - parameters_layout.addLayout(name_params_layout) + # --- Group all customization widgets into a collapsible one + custom_config = CollapsibleWidget(self, _("Custom configuration")) + custom_config.addWidget(config_props_group) + custom_config.addWidget(self.executor_group) + custom_config.addWidget(self.wdir_group) + + # Remove unnecessary margin at the bottom. + custom_config.set_content_bottom_margin(0) + # --- Final layout layout = self.add_widgets( self.header_label, - 10, self.configuration_combo, # Hidden for simplicity executor_layout, - 10, - self.executor_group, + custom_config ) - # Settings + widget_dialog = QWidget(self) + widget_dialog.setMinimumWidth(600) + widget_dialog.setLayout(layout) + scroll_layout = QVBoxLayout(self) + scroll_layout.addWidget(widget_dialog) + self.add_button_box(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + # --- Settings self.executor_combo.currentIndexChanged.connect( self.display_executor_configuration) self.executor_combo.setModel(self.executors_model) @@ -552,13 +589,6 @@ def setup(self): self.update_parameter_set) self.parameters_combo.setModel(self.parameter_model) - widget_dialog = QWidget(self) - widget_dialog.setMinimumWidth(600) - widget_dialog.setLayout(layout) - scroll_layout = QVBoxLayout(self) - scroll_layout.addWidget(widget_dialog) - self.add_button_box(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.setWindowTitle(_("Run configuration per file")) self.layout().setSizeConstraint(QLayout.SetFixedSize) @@ -793,10 +823,39 @@ def showEvent(self, event): def _stylesheet(self): css = qstylizer.style.StyleSheet() - css["QLabel#header-label"].setValues( - backgroundColor=QStylePalette.COLOR_BACKGROUND_3, + # --- Style for the header + css["QLabel#run-header-label"].setValues( + # Give it a background color to make it highlight over the other + # widgets. + backgroundColor=QStylePalette.COLOR_BACKGROUND_4, + # The left and right margins are a bit bigger to prevent the file + # name from being too close to the borders in case it's too long. padding=f"{2 * AppStyle.MarginSize} {4 * AppStyle.MarginSize}", - borderRadius=QStylePalette.SIZE_BORDER_RADIUS + borderRadius=QStylePalette.SIZE_BORDER_RADIUS, + # Add good enough margin with the widgets below it. + marginBottom=f"{AppStyle.InnerContentPadding}px" + ) + + + # --- Style for the collapsible + css["CollapsibleWidget"].setValues( + # Separate it from the widgets above it with the same margin as the + # one between the header and those widgets. + marginTop=f"{AppStyle.InnerContentPadding}px" + ) + + # --- Style for QGroupBoxes + # This makes the spacing between this group and the one above it (i.e. + # "Configuration properties") to be almost the same as the one between + # it and the group below (i.e. "Working directory settings"). + css["QGroupBox#run-executor-group::title"].setValues( + marginTop="7px" + ) + + # Reduce extra top margin for this group to make the spacing between + # groups uniform. + css["QGroupBox#run-wdir-group::title"].setValues( + marginTop="-5px" ) return css.toString() diff --git a/spyder/utils/stylesheet.py b/spyder/utils/stylesheet.py index 54b641ab990..ff406291c4d 100644 --- a/spyder/utils/stylesheet.py +++ b/spyder/utils/stylesheet.py @@ -259,14 +259,16 @@ def _customize_stylesheet(self): minHeight=f'{AppStyle.ComboBoxMinHeight - 0.2}em' ) - # Change QGroupBox style to avoid the "boxes within boxes" antipattern - # in Preferences + # Remove border in QGroupBox to avoid the "boxes within boxes" + # antipattern. Also, increase its title font in one point to make it + # more relevant. css.QGroupBox.setValues( border='0px', - marginBottom='15px', fontSize=f'{font_size + 1}pt', ) + # Increase separation between title and content of QGroupBoxes and fix + # its alignment. css['QGroupBox::title'].setValues( paddingTop='-0.3em', left='0px', From 530bc9af88c5a870a184ab42204e881f5717e838 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 8 Nov 2023 09:39:02 -0500 Subject: [PATCH 12/55] Minor fixes to the run config group of several plugins - IPython console group: Remove grid layout to give more space to introduce command line options. - External terminal: Change text of of several widgets to make options easier to understand. - Profiler: Minor inheritance problems. --- .../externalterminal/widgets/run_conf.py | 28 +++++++++---------- .../plugins/ipythonconsole/widgets/config.py | 20 +++++++------ spyder/plugins/profiler/widgets/run_conf.py | 5 ++-- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/spyder/plugins/externalterminal/widgets/run_conf.py b/spyder/plugins/externalterminal/widgets/run_conf.py index 3c4fdfc44f9..dd091d8110b 100644 --- a/spyder/plugins/externalterminal/widgets/run_conf.py +++ b/spyder/plugins/externalterminal/widgets/run_conf.py @@ -26,10 +26,6 @@ from spyder.utils.qthelpers import create_toolbutton -# Main constants -INTERACT = _("Interact with the Python terminal after execution") - - class ExternalTerminalPyConfiguration(RunExecutorConfigurationGroup): """External terminal Python run configuration options.""" @@ -40,36 +36,38 @@ def __init__(self, parent, context: Context, input_extension: str, self.dir = None # --- Interpreter --- - interpreter_group = QGroupBox(_("Terminal")) + interpreter_group = QGroupBox(_("Python interpreter")) interpreter_layout = QVBoxLayout(interpreter_group) # --- System terminal --- - external_group = QWidget() - + external_group = QWidget(self) external_layout = QGridLayout() external_group.setLayout(external_layout) - self.interact_cb = QCheckBox(INTERACT) + + self.interact_cb = QCheckBox( + _("Interact with the interpreter after execution") + ) external_layout.addWidget(self.interact_cb, 1, 0, 1, -1) - self.pclo_cb = QCheckBox(_("Command line options:")) + self.pclo_cb = QCheckBox(_("Interpreter options:")) external_layout.addWidget(self.pclo_cb, 3, 0) - self.pclo_edit = QLineEdit() + self.pclo_edit = QLineEdit(self) self.pclo_cb.toggled.connect(self.pclo_edit.setEnabled) self.pclo_edit.setEnabled(False) - self.pclo_edit.setToolTip(_("-u<_b> is added to the " - "other options you set here")) + self.pclo_edit.setToolTip( + _("-u is added to the other options you set here") + ) external_layout.addWidget(self.pclo_edit, 3, 1) interpreter_layout.addWidget(external_group) # --- General settings ---- - common_group = QGroupBox(_("Script settings")) - + common_group = QGroupBox(_("Bash/Batch script settings")) common_layout = QGridLayout(common_group) self.clo_cb = QCheckBox(_("Command line options:")) common_layout.addWidget(self.clo_cb, 0, 0) - self.clo_edit = QLineEdit() + self.clo_edit = QLineEdit(self) self.clo_cb.toggled.connect(self.clo_edit.setEnabled) self.clo_edit.setEnabled(False) common_layout.addWidget(self.clo_edit, 0, 1) diff --git a/spyder/plugins/ipythonconsole/widgets/config.py b/spyder/plugins/ipythonconsole/widgets/config.py index 1df675f2d17..d1c24c04c3d 100644 --- a/spyder/plugins/ipythonconsole/widgets/config.py +++ b/spyder/plugins/ipythonconsole/widgets/config.py @@ -12,7 +12,7 @@ # Third-party imports from qtpy.compat import getexistingdirectory from qtpy.QtWidgets import ( - QRadioButton, QGroupBox, QVBoxLayout, QGridLayout, QCheckBox, QLineEdit) + QCheckBox, QGroupBox, QHBoxLayout, QLineEdit, QRadioButton, QVBoxLayout) # Local imports from spyder.api.translations import _ @@ -51,24 +51,26 @@ def __init__(self, parent, context: Context, input_extension: str, # --- General settings ---- common_group = QGroupBox(_("General settings")) - - common_layout = QGridLayout(common_group) + common_layout = QVBoxLayout(common_group) self.clear_var_cb = QCheckBox(CLEAR_ALL_VARIABLES) - common_layout.addWidget(self.clear_var_cb, 0, 0) + common_layout.addWidget(self.clear_var_cb) self.console_ns_cb = QCheckBox(CONSOLE_NAMESPACE) - common_layout.addWidget(self.console_ns_cb, 1, 0) + common_layout.addWidget(self.console_ns_cb) self.post_mortem_cb = QCheckBox(POST_MORTEM) - common_layout.addWidget(self.post_mortem_cb, 2, 0) + common_layout.addWidget(self.post_mortem_cb) self.clo_cb = QCheckBox(_("Command line options:")) - common_layout.addWidget(self.clo_cb, 3, 0) - self.clo_edit = QLineEdit() + self.clo_edit = QLineEdit(self) self.clo_cb.toggled.connect(self.clo_edit.setEnabled) self.clo_edit.setEnabled(False) - common_layout.addWidget(self.clo_edit, 3, 1) + + cli_layout = QHBoxLayout() + cli_layout.addWidget(self.clo_cb) + cli_layout.addWidget(self.clo_edit) + common_layout.addLayout(cli_layout) layout = QVBoxLayout(self) layout.addWidget(interpreter_group) diff --git a/spyder/plugins/profiler/widgets/run_conf.py b/spyder/plugins/profiler/widgets/run_conf.py index ffd95e3f061..a1105c7c230 100644 --- a/spyder/plugins/profiler/widgets/run_conf.py +++ b/spyder/plugins/profiler/widgets/run_conf.py @@ -32,17 +32,16 @@ def __init__(self, parent, context: Context, input_extension: str, # --- General settings ---- common_group = QGroupBox(_("Script settings")) - common_layout = QGridLayout(common_group) self.clo_cb = QCheckBox(_("Command line options:")) common_layout.addWidget(self.clo_cb, 0, 0) - self.clo_edit = QLineEdit() + self.clo_edit = QLineEdit(self) self.clo_cb.toggled.connect(self.clo_edit.setEnabled) self.clo_edit.setEnabled(False) common_layout.addWidget(self.clo_edit, 0, 1) - layout = QVBoxLayout(self) + layout = QVBoxLayout() layout.addWidget(common_group) layout.addStretch(100) From 3a48a879f151a9fcc9055cb1adf49988b3b51bd2 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 8 Nov 2023 10:01:57 -0500 Subject: [PATCH 13/55] Run: Center dialog after custom_config widget is expanded/collapsed --- spyder/app/utils.py | 10 ++++++++++ spyder/plugins/run/widgets.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/spyder/app/utils.py b/spyder/app/utils.py index edcb325d700..d589ed80385 100644 --- a/spyder/app/utils.py +++ b/spyder/app/utils.py @@ -367,6 +367,16 @@ def create_window(WindowClass, app, splash, options, args): main.show() main.post_visible_setup() + # Add a reference to the main window so it can be accessed from anywhere. + # + # Notes + # ----- + # * This should be used to get main window properties (such as width, + # height or position) that other widgets can need to position relative to + # it. + # * **DO NOT** use it to access other plugins functionality through it. + app.main_window = main + if main.console: main.console.start_interpreter(namespace={}) main.console.set_namespace_item('spy', Spy(app=app, window=main)) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index c607f735c75..fd3739557a1 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -16,8 +16,9 @@ from qtpy.QtCore import QSize, Qt, Signal from qtpy.QtGui import QFontMetrics from qtpy.QtWidgets import ( - QDialog, QDialogButtonBox, QGridLayout, QGroupBox, QHBoxLayout, QLabel, - QLineEdit, QLayout, QRadioButton, QStackedWidget, QVBoxLayout, QWidget) + QApplication, QDialog, QDialogButtonBox, QGridLayout, QGroupBox, + QHBoxLayout, QLabel, QLineEdit, QLayout, QRadioButton, QStackedWidget, + QVBoxLayout, QWidget) import qstylizer.style # Local imports @@ -554,6 +555,9 @@ def setup(self): # Remove unnecessary margin at the bottom. custom_config.set_content_bottom_margin(0) + # Center dialog after custom_config is expanded/collapsed + custom_config._animation.finished.connect(self._center_dialog) + # --- Final layout layout = self.add_widgets( self.header_label, @@ -859,3 +863,25 @@ def _stylesheet(self): ) return css.toString() + + def _center_dialog(self): + """ + Center dialog relative to the main window after collapsing/expanding + the custom configuration widget. + """ + # main_window is usually not available in our tests, so we need to + # check for this. + main_window = getattr(QApplication.instance(), 'main_window', None) + + if main_window: + x = ( + main_window.pos().x() + + ((main_window.width() - self.width()) / 2) + ) + + y = ( + main_window.pos().y() + + ((main_window.height() - self.height()) / 2) + ) + + self.move(x, y) From f5cc47cf8b142bf01080deb94ea145a0fb47bc68 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 16 May 2024 17:54:28 -0500 Subject: [PATCH 14/55] Widgets: Change cursor shape when hovering toggle button of CollapsibleWidget - We use PointingHandCursor for that, which will better indicate to users that the button is clickable. - That requires a more recent version of superqt, which exposes that button publicly. --- binder/environment.yml | 2 +- requirements/main.yml | 2 +- setup.py | 2 +- spyder/dependencies.py | 2 +- spyder/widgets/collapsible.py | 19 +++++++++++++++++++ 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/binder/environment.yml b/binder/environment.yml index 61903049553..8d925e55da5 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -49,7 +49,7 @@ dependencies: - setuptools >=49.6.0 - sphinx >=0.6.6 - spyder-kernels >=3.0.0b6,<3.0.0b7 -- superqt >=0.6.1,<1.0.0 +- superqt >=0.6.2,<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 6496d1a3348..7d0a2b3b5cb 100644 --- a/requirements/main.yml +++ b/requirements/main.yml @@ -45,7 +45,7 @@ dependencies: - setuptools >=49.6.0 - sphinx >=0.6.6 - spyder-kernels >=3.0.0b6,<3.0.0b7 - - superqt >=0.6.1,<1.0.0 + - superqt >=0.6.2,<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 009a2bcbecd..e3230f34bb2 100644 --- a/setup.py +++ b/setup.py @@ -246,7 +246,7 @@ def run(self): 'setuptools>=49.6.0', 'sphinx>=0.6.6', 'spyder-kernels>=3.0.0b6,<3.0.0b7', - 'superqt>=0.6.1,<1.0.0', + 'superqt>=0.6.2,<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 4dcb258c6a4..839f3e4a151 100644 --- a/spyder/dependencies.py +++ b/spyder/dependencies.py @@ -73,7 +73,7 @@ SETUPTOOLS_REQVER = '>=49.6.0' SPHINX_REQVER = '>=0.6.6' SPYDER_KERNELS_REQVER = '>=3.0.0b6,<3.0.0b7' -SUPERQT_REQVER = '>=0.6.1,<1.0.0' +SUPERQT_REQVER = '>=0.6.2,<1.0.0' TEXTDISTANCE_REQVER = '>=4.2.0' THREE_MERGE_REQVER = '>=0.1.1' WATCHDOG_REQVER = '>=0.10.3' diff --git a/spyder/widgets/collapsible.py b/spyder/widgets/collapsible.py index e5bd990e2e2..54edf64828e 100644 --- a/spyder/widgets/collapsible.py +++ b/spyder/widgets/collapsible.py @@ -7,6 +7,8 @@ """Collapsible widget to hide and show child widgets.""" import qstylizer.style +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QPushButton from superqt import QCollapsible from spyder.utils.icon_manager import ima @@ -47,6 +49,9 @@ def __init__(self, parent=None, title=""): # Signals self.toggled.connect(self._on_toggled) + # Set our properties for the toggle button + self._set_toggle_btn_properties() + def set_content_bottom_margin(self, bottom_margin): """Set bottom margin of the content area to `bottom_margin`.""" margins = self.content().layout().contentsMargins() @@ -103,3 +108,17 @@ def _on_toggled(self, state): ) self.setStyleSheet(self._css.toString()) + + def _set_toggle_btn_properties(self): + """Set properties for the toogle button.""" + + def enter_event(event): + self.setCursor(Qt.PointingHandCursor) + super(QPushButton, self._toggle_btn).enterEvent(event) + + def leave_event(event): + self.setCursor(Qt.ArrowCursor) + super(QPushButton, self._toggle_btn).leaveEvent(event) + + self.toggleButton().enterEvent = enter_event + self.toggleButton().leaveEvent = leave_event From ebf949f9d7ccc2fc9c1bc5b38643a4f5b6e4a9a4 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 17 May 2024 10:36:13 -0500 Subject: [PATCH 15/55] Run: Several fixes to its widgets - Correctly display the entries of its comboboxes now that they are instances of SpyderComboBox. - Fix error in Python 3.10+ when centering RunDialog. - Fix error in its config page when an executor doesn't have parameters for specific files. --- spyder/plugins/run/confpage.py | 2 +- spyder/plugins/run/models.py | 16 ++++++++-------- spyder/plugins/run/widgets.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index d00ca6d3aa0..80091ff08c6 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -289,7 +289,7 @@ def executor_index_changed(self, index: int): # Don't display configs set for specific files. Here # users are allowed to configure global configs, i.e. those # that can be used by any file. - if exec_params["file_uuid"] is not None: + if exec_params.get("file_uuid") is not None: continue params_key = (ext, context, exec_params_id) diff --git a/spyder/plugins/run/models.py b/spyder/plugins/run/models.py index 2a40730b589..4bd6f053d18 100644 --- a/spyder/plugins/run/models.py +++ b/spyder/plugins/run/models.py @@ -138,7 +138,7 @@ def executor_supports_configuration( return executor in input_executors def data(self, index: QModelIndex, role: int = Qt.DisplayRole): - if role == Qt.DisplayRole: + if role == Qt.DisplayRole or role == Qt.EditRole: executor_indices = self.inverted_pos[self.current_input] executor_id = executor_indices[index.row()] return self.executor_names[executor_id] @@ -206,7 +206,7 @@ def update_index(self, index: int): self.executor_model.switch_input(uuid, (ext, context_id)) def data(self, index: QModelIndex, role: int = Qt.DisplayRole): - if role == Qt.DisplayRole: + if role == Qt.DisplayRole or role == Qt.EditRole: uuid = self.metadata_index[index.row()] metadata = self.run_configurations[uuid] return metadata['name'] @@ -285,14 +285,14 @@ def data(self, index: QModelIndex, role: int = Qt.DisplayRole): pos = index.row() total_saved_params = len(self.executor_conf_params) - if pos == total_saved_params: - if role == Qt.DisplayRole: + if pos == total_saved_params or pos == -1: + if role == Qt.DisplayRole or role == Qt.EditRole: return _("Default") else: params_id = self.params_index[pos] params = self.executor_conf_params[params_id] params_name = params['name'] - if role == Qt.DisplayRole: + if role == Qt.DisplayRole or role == Qt.EditRole: return params_name def rowCount(self, parent: QModelIndex = ...) -> int: @@ -394,7 +394,7 @@ def __init__(self, parent, executor_model: RunExecutorListModel): def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> str: row = index.row() - if role == Qt.DisplayRole: + if role == Qt.DisplayRole or role == Qt.EditRole: executor_id = self.executor_indexed_list[row] return self.executor_model.executor_names[executor_id] @@ -431,7 +431,7 @@ def data(self, index: QModelIndex, (extension, context, __) = params_idx params = self.executor_conf_params[params_idx] - if role == Qt.DisplayRole: + if role == Qt.DisplayRole or role == Qt.EditRole: if column == self.EXTENSION: return extension elif column == self.CONTEXT: @@ -455,7 +455,7 @@ def headerData( return int(Qt.AlignHCenter | Qt.AlignVCenter) return int(Qt.AlignRight | Qt.AlignVCenter) - if role == Qt.DisplayRole: + if role == Qt.DisplayRole or role == Qt.EditRole: if orientation == Qt.Horizontal: if section == self.EXTENSION: return _('File extension') diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index aeca188edf4..28d1522feb7 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -877,12 +877,12 @@ def _center_dialog(self): if main_window: x = ( main_window.pos().x() - + ((main_window.width() - self.width()) / 2) + + ((main_window.width() - self.width()) // 2) ) y = ( main_window.pos().y() - + ((main_window.height() - self.height()) / 2) + + ((main_window.height() - self.height()) // 2) ) self.move(x, y) From 543ad31413a89ffd57fe2fbd8ae6286157d6ea36 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 17 May 2024 11:56:06 -0500 Subject: [PATCH 16/55] IPython console: Rename file that contains run conf options to run_conf This is done to follow the same convention present in the Profiler and External Terminal plugins. --- spyder/plugins/debugger/plugin.py | 2 +- spyder/plugins/ipythonconsole/plugin.py | 2 +- .../plugins/ipythonconsole/widgets/{config.py => run_conf.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename spyder/plugins/ipythonconsole/widgets/{config.py => run_conf.py} (100%) diff --git a/spyder/plugins/debugger/plugin.py b/spyder/plugins/debugger/plugin.py index 5926d047c41..2ae0fa8910a 100644 --- a/spyder/plugins/debugger/plugin.py +++ b/spyder/plugins/debugger/plugin.py @@ -33,7 +33,7 @@ RunConfiguration, ExtendedRunExecutionParameters, RunExecutor, run_execute, RunContext, RunResult) from spyder.plugins.toolbar.api import ApplicationToolbars -from spyder.plugins.ipythonconsole.widgets.config import IPythonConfigOptions +from spyder.plugins.ipythonconsole.widgets.run_conf import IPythonConfigOptions from spyder.plugins.editor.api.run import CellRun, SelectionRun diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py index ba46e4b4a14..83c97b90446 100644 --- a/spyder/plugins/ipythonconsole/plugin.py +++ b/spyder/plugins/ipythonconsole/plugin.py @@ -26,7 +26,7 @@ IPythonConsoleWidgetMenus ) from spyder.plugins.ipythonconsole.confpage import IPythonConsoleConfigPage -from spyder.plugins.ipythonconsole.widgets.config import IPythonConfigOptions +from spyder.plugins.ipythonconsole.widgets.run_conf import IPythonConfigOptions from spyder.plugins.ipythonconsole.widgets.main_widget import ( IPythonConsoleWidget ) diff --git a/spyder/plugins/ipythonconsole/widgets/config.py b/spyder/plugins/ipythonconsole/widgets/run_conf.py similarity index 100% rename from spyder/plugins/ipythonconsole/widgets/config.py rename to spyder/plugins/ipythonconsole/widgets/run_conf.py From 5c00548b7d2a52bdb3d6f38f280135e5373c0c86 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 17 May 2024 12:32:31 -0500 Subject: [PATCH 17/55] Set min width for command line options of run conf widgets This will give more space to users to enter them. --- spyder/plugins/externalterminal/widgets/run_conf.py | 1 + spyder/plugins/ipythonconsole/widgets/run_conf.py | 1 + spyder/plugins/profiler/widgets/run_conf.py | 1 + 3 files changed, 3 insertions(+) diff --git a/spyder/plugins/externalterminal/widgets/run_conf.py b/spyder/plugins/externalterminal/widgets/run_conf.py index dd091d8110b..ab9c3c89929 100644 --- a/spyder/plugins/externalterminal/widgets/run_conf.py +++ b/spyder/plugins/externalterminal/widgets/run_conf.py @@ -68,6 +68,7 @@ def __init__(self, parent, context: Context, input_extension: str, self.clo_cb = QCheckBox(_("Command line options:")) common_layout.addWidget(self.clo_cb, 0, 0) self.clo_edit = QLineEdit(self) + self.clo_edit.setMinimumWidth(300) self.clo_cb.toggled.connect(self.clo_edit.setEnabled) self.clo_edit.setEnabled(False) common_layout.addWidget(self.clo_edit, 0, 1) diff --git a/spyder/plugins/ipythonconsole/widgets/run_conf.py b/spyder/plugins/ipythonconsole/widgets/run_conf.py index d1c24c04c3d..99bfd1e1cd0 100644 --- a/spyder/plugins/ipythonconsole/widgets/run_conf.py +++ b/spyder/plugins/ipythonconsole/widgets/run_conf.py @@ -64,6 +64,7 @@ def __init__(self, parent, context: Context, input_extension: str, self.clo_cb = QCheckBox(_("Command line options:")) self.clo_edit = QLineEdit(self) + self.clo_edit.setMinimumWidth(300) self.clo_cb.toggled.connect(self.clo_edit.setEnabled) self.clo_edit.setEnabled(False) diff --git a/spyder/plugins/profiler/widgets/run_conf.py b/spyder/plugins/profiler/widgets/run_conf.py index a1105c7c230..6bd99c8a72b 100644 --- a/spyder/plugins/profiler/widgets/run_conf.py +++ b/spyder/plugins/profiler/widgets/run_conf.py @@ -37,6 +37,7 @@ def __init__(self, parent, context: Context, input_extension: str, self.clo_cb = QCheckBox(_("Command line options:")) common_layout.addWidget(self.clo_cb, 0, 0) self.clo_edit = QLineEdit(self) + self.clo_edit.setMinimumWidth(300) self.clo_cb.toggled.connect(self.clo_edit.setEnabled) self.clo_edit.setEnabled(False) common_layout.addWidget(self.clo_edit, 0, 1) From 34366ebed086d5840cf96b2f472023e03ed75fde Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 17 May 2024 21:36:44 -0500 Subject: [PATCH 18/55] Run: Additional UI improvements for ExecutionParametersDialog - Remove extra margins added by the config dialog style that this dialog belongs to. - Make the dialog fit exactly to the size of the widgets inside it. - Increase inner padding. - Remove unnecessary widget that acted as container for the others. - Reorganize code for clarity. --- spyder/plugins/run/widgets.py | 94 +++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 28d1522feb7..20c43350a32 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -69,9 +69,13 @@ def __init__(self, parent=None, disable_run_btn=False): self.setLayout(layout) self.disable_run_btn = disable_run_btn + # Style that will be set by children + self._css = qstylizer.style.StyleSheet() + def add_widgets(self, *widgets_or_spacings): """Add widgets/spacing to dialog vertical layout""" layout = self.layout() + for widget_or_spacing in widgets_or_spacings: if isinstance(widget_or_spacing, int): layout.addSpacing(widget_or_spacing) @@ -79,6 +83,7 @@ def add_widgets(self, *widgets_or_spacings): layout.addLayout(widget_or_spacing) else: layout.addWidget(widget_or_spacing) + return layout def add_button_box(self, stdbtns): @@ -163,7 +168,21 @@ def __init__( # ---- Public methods # ------------------------------------------------------------------------- def setup(self): - # Widgets + # --- Configuration name + if self.new_config: + params_name_text = _("Save configuration as:") + else: + params_name_text = _("Configuration name:") + + params_name_label = QLabel(params_name_text) + self.store_params_text = QLineEdit(self) + self.store_params_text.setMinimumWidth(250) + store_params_layout = QHBoxLayout() + store_params_layout.addWidget(params_name_label) + store_params_layout.addWidget(self.store_params_text) + store_params_layout.addStretch(1) + + # --- Extension and context widgets ext_combo_label = QLabel(_("Select a file extension:")) context_combo_label = QLabel(_("Select a run context:")) @@ -187,13 +206,14 @@ def setup(self): ext_context_layout.addLayout(ext_context_g_layout) ext_context_layout.addStretch(1) - self.stack = QStackedWidget() - self.executor_group = QGroupBox(_("Executor parameters")) + # --- Runner settings + self.stack = QStackedWidget(self) + self.executor_group = QGroupBox(_("Runner settings")) executor_layout = QVBoxLayout(self.executor_group) executor_layout.addWidget(self.stack) + # --- Working directory settings self.wdir_group = QGroupBox(_("Working directory settings")) - wdir_layout = QVBoxLayout(self.wdir_group) self.file_dir_radio = QRadioButton(FILE_DIR) @@ -219,43 +239,29 @@ def setup(self): fixed_dir_layout.addWidget(browse_btn) wdir_layout.addLayout(fixed_dir_layout) - if self.new_config: - params_name_text = _("Save configuration as:") - else: - params_name_text = _("Configuration name:") + # --- All settings layout + all_settings_layout = QVBoxLayout() + all_settings_layout.addWidget(self.executor_group) + all_settings_layout.addWidget(self.wdir_group) - params_name_label = QLabel(params_name_text) - self.store_params_text = QLineEdit(self) - self.store_params_text.setMinimumWidth(300) - store_params_layout = QHBoxLayout() - store_params_layout.addWidget(params_name_label) - store_params_layout.addWidget(self.store_params_text) - store_params_layout.addStretch(1) - - all_group = QVBoxLayout() - all_group.addWidget(self.executor_group) - all_group.addWidget(self.wdir_group) - - # Final layout + # --- Final layout layout = self.add_widgets( store_params_layout, - 15, + 5 * AppStyle.MarginSize, ext_context_layout, - 10, - all_group + 5 * AppStyle.MarginSize, + all_settings_layout ) + layout.addStretch() + layout.setContentsMargins(*((AppStyle.InnerContentPadding,) * 4)) - widget_dialog = QWidget(self) - widget_dialog.setMinimumWidth(600) - widget_dialog.setLayout(layout) - scroll_layout = QVBoxLayout(self) - scroll_layout.addWidget(widget_dialog) self.add_button_box(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - # Set title + # --- Settings self.setWindowTitle( _("New run configuration for: {}").format(self.executor_name) ) + self.layout().setSizeConstraint(QLayout.SetFixedSize) self.extension_combo.addItems(self.extensions) @@ -272,6 +278,9 @@ def setup(self): if self.parameters_name: self.store_params_text.setText(self.parameters_name) + # --- Stylesheet + self.setStyleSheet(self._stylesheet) + def extension_changed(self, index: int): if index < 0: return @@ -434,6 +443,18 @@ def accept(self) -> None: super().accept() + # ---- Private methods + # ------------------------------------------------------------------------- + @property + def _stylesheet(self): + # This avoids the extra bottom margin added by the config dialog since + # this widget is one of its children + self._css.QGroupBox.setValues( + marginBottom='0px', + ) + + return self._css.toString() + class RunDialog(BaseRunConfigDialog, SpyderFontsMixin): """Run dialog used to configure run executors.""" @@ -826,10 +847,8 @@ def showEvent(self, event): # ------------------------------------------------------------------------- @property def _stylesheet(self): - css = qstylizer.style.StyleSheet() - # --- Style for the header - css["QLabel#run-header-label"].setValues( + self._css["QLabel#run-header-label"].setValues( # Give it a background color to make it highlight over the other # widgets. backgroundColor=SpyderPalette.COLOR_BACKGROUND_4, @@ -841,9 +860,8 @@ def _stylesheet(self): marginBottom=f"{AppStyle.InnerContentPadding}px" ) - # --- Style for the collapsible - css["CollapsibleWidget"].setValues( + self._css["CollapsibleWidget"].setValues( # Separate it from the widgets above it with the same margin as the # one between the header and those widgets. marginTop=f"{AppStyle.InnerContentPadding}px" @@ -853,17 +871,17 @@ def _stylesheet(self): # This makes the spacing between this group and the one above it (i.e. # "Configuration properties") to be almost the same as the one between # it and the group below (i.e. "Working directory settings"). - css["QGroupBox#run-executor-group::title"].setValues( + self._css["QGroupBox#run-executor-group::title"].setValues( marginTop="7px" ) # Reduce extra top margin for this group to make the spacing between # groups uniform. - css["QGroupBox#run-wdir-group::title"].setValues( + self._css["QGroupBox#run-wdir-group::title"].setValues( marginTop="-5px" ) - return css.toString() + return self._css.toString() def _center_dialog(self): """ From 03adbafec7c51e88156836379d0748e6b2975fe0 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 17 May 2024 21:46:46 -0500 Subject: [PATCH 19/55] Run: Correctly set bottom margins for QGroupBoxes shown in its dialogs The previous solution was not correct because it cropped the title of those QGroupBoxes for some font types. --- .../plugins/externalterminal/widgets/run_conf.py | 10 ++++++++++ .../plugins/ipythonconsole/widgets/run_conf.py | 11 ++++++++++- spyder/plugins/profiler/widgets/run_conf.py | 11 +++++++++-- spyder/plugins/run/widgets.py | 16 ---------------- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/spyder/plugins/externalterminal/widgets/run_conf.py b/spyder/plugins/externalterminal/widgets/run_conf.py index ab9c3c89929..cba5fd6f517 100644 --- a/spyder/plugins/externalterminal/widgets/run_conf.py +++ b/spyder/plugins/externalterminal/widgets/run_conf.py @@ -24,6 +24,7 @@ from spyder.utils.icon_manager import ima from spyder.utils.misc import getcwd_or_home from spyder.utils.qthelpers import create_toolbutton +from spyder.utils.stylesheet import AppStyle class ExternalTerminalPyConfiguration(RunExecutorConfigurationGroup): @@ -38,6 +39,9 @@ def __init__(self, parent, context: Context, input_extension: str, # --- Interpreter --- interpreter_group = QGroupBox(_("Python interpreter")) interpreter_layout = QVBoxLayout(interpreter_group) + interpreter_layout.setContentsMargins( + *((3 * AppStyle.MarginSize,) * 4) + ) # --- System terminal --- external_group = QWidget(self) @@ -64,6 +68,12 @@ def __init__(self, parent, context: Context, input_extension: str, # --- General settings ---- common_group = QGroupBox(_("Bash/Batch script settings")) common_layout = QGridLayout(common_group) + common_layout.setContentsMargins( + 3 * AppStyle.MarginSize, + 3 * AppStyle.MarginSize, + 3 * AppStyle.MarginSize, + 0, + ) self.clo_cb = QCheckBox(_("Command line options:")) common_layout.addWidget(self.clo_cb, 0, 0) diff --git a/spyder/plugins/ipythonconsole/widgets/run_conf.py b/spyder/plugins/ipythonconsole/widgets/run_conf.py index 99bfd1e1cd0..0d3060c0e37 100644 --- a/spyder/plugins/ipythonconsole/widgets/run_conf.py +++ b/spyder/plugins/ipythonconsole/widgets/run_conf.py @@ -19,6 +19,7 @@ from spyder.plugins.run.api import ( RunExecutorConfigurationGroup, Context, RunConfigurationMetadata) from spyder.utils.misc import getcwd_or_home +from spyder.utils.stylesheet import AppStyle # Main constants @@ -40,8 +41,10 @@ def __init__(self, parent, context: Context, input_extension: str, # --- Interpreter --- interpreter_group = QGroupBox(_("Console")) - interpreter_layout = QVBoxLayout(interpreter_group) + interpreter_layout.setContentsMargins( + *((3 * AppStyle.MarginSize,) * 4) + ) self.current_radio = QRadioButton(CURRENT_INTERPRETER) interpreter_layout.addWidget(self.current_radio) @@ -52,6 +55,12 @@ def __init__(self, parent, context: Context, input_extension: str, # --- General settings ---- common_group = QGroupBox(_("General settings")) common_layout = QVBoxLayout(common_group) + common_layout.setContentsMargins( + 3 * AppStyle.MarginSize, + 3 * AppStyle.MarginSize, + 3 * AppStyle.MarginSize, + 0, + ) self.clear_var_cb = QCheckBox(CLEAR_ALL_VARIABLES) common_layout.addWidget(self.clear_var_cb) diff --git a/spyder/plugins/profiler/widgets/run_conf.py b/spyder/plugins/profiler/widgets/run_conf.py index 6bd99c8a72b..de5feb68fa9 100644 --- a/spyder/plugins/profiler/widgets/run_conf.py +++ b/spyder/plugins/profiler/widgets/run_conf.py @@ -19,6 +19,7 @@ from spyder.plugins.run.api import ( RunExecutorConfigurationGroup, Context, RunConfigurationMetadata) from spyder.utils.misc import getcwd_or_home +from spyder.utils.stylesheet import AppStyle class ProfilerPyConfigurationGroup(RunExecutorConfigurationGroup): @@ -31,8 +32,14 @@ def __init__(self, parent, context: Context, input_extension: str, self.dir = None # --- General settings ---- - common_group = QGroupBox(_("Script settings")) + common_group = QGroupBox(_("File settings")) common_layout = QGridLayout(common_group) + common_layout.setContentsMargins( + 3 * AppStyle.MarginSize, + 3 * AppStyle.MarginSize, + 3 * AppStyle.MarginSize, + 0, + ) self.clo_cb = QCheckBox(_("Command line options:")) common_layout.addWidget(self.clo_cb, 0, 0) @@ -42,7 +49,7 @@ def __init__(self, parent, context: Context, input_extension: str, self.clo_edit.setEnabled(False) common_layout.addWidget(self.clo_edit, 0, 1) - layout = QVBoxLayout() + layout = QVBoxLayout(self) layout.addWidget(common_group) layout.addStretch(100) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 20c43350a32..006cc52147f 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -531,7 +531,6 @@ def setup(self): # --- Runner settings self.stack = QStackedWidget() self.executor_group = QGroupBox(_("Runner settings")) - self.executor_group.setObjectName("run-executor-group") parameters_layout = QVBoxLayout(self.executor_group) parameters_layout.addWidget(self.stack) @@ -543,7 +542,6 @@ def setup(self): # --- Working directory settings self.wdir_group = QGroupBox(_("Working directory settings")) - self.wdir_group.setObjectName("run-wdir-group") wdir_layout = QVBoxLayout(self.wdir_group) self.file_dir_radio = QRadioButton(FILE_DIR) @@ -867,20 +865,6 @@ def _stylesheet(self): marginTop=f"{AppStyle.InnerContentPadding}px" ) - # --- Style for QGroupBoxes - # This makes the spacing between this group and the one above it (i.e. - # "Configuration properties") to be almost the same as the one between - # it and the group below (i.e. "Working directory settings"). - self._css["QGroupBox#run-executor-group::title"].setValues( - marginTop="7px" - ) - - # Reduce extra top margin for this group to make the spacing between - # groups uniform. - self._css["QGroupBox#run-wdir-group::title"].setValues( - marginTop="-5px" - ) - return self._css.toString() def _center_dialog(self): From f0a0aba990c3d5d0c69291cebe211b417911ce70 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 18 May 2024 11:50:53 -0500 Subject: [PATCH 20/55] Run: Allow to delete and save globally config parameters in RunDialog --- spyder/plugins/run/container.py | 8 ++- spyder/plugins/run/widgets.py | 98 +++++++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 15 deletions(-) diff --git a/spyder/plugins/run/container.py b/spyder/plugins/run/container.py index 2d17fb65708..7839db0e47e 100644 --- a/spyder/plugins/run/container.py +++ b/spyder/plugins/run/container.py @@ -223,6 +223,9 @@ def edit_run_configurations( self.dialog.setup() self.dialog.finished.connect(self.process_run_dialog_result) + self.dialog.sig_delete_config_requested.connect( + self.delete_executor_configuration_parameters + ) if selected_executor is not None: self.dialog.select_executor(selected_executor) @@ -961,9 +964,9 @@ def delete_executor_configuration_parameters( executor_name: str The identifier of the run executor. extension: str - The file extension to register the configuration parameters for. + The file extension of the configuration parameters to delete. context_id: str - The context to register the configuration parameters for. + The context of the configuration parameters to delete. uuid: str The run configuration identifier. """ @@ -985,7 +988,6 @@ def delete_executor_configuration_parameters( self.set_conf('parameters', all_execution_params) - def get_last_used_executor_parameters( self, uuid: str diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 006cc52147f..790b8b566ce 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -16,9 +16,22 @@ from qtpy.QtCore import QSize, Qt, Signal from qtpy.QtGui import QFontMetrics from qtpy.QtWidgets import ( - QApplication, QDialog, QDialogButtonBox, QGridLayout, QGroupBox, - QHBoxLayout, QLabel, QLineEdit, QLayout, QRadioButton, QStackedWidget, - QVBoxLayout, QWidget) + QApplication, + QDialog, + QDialogButtonBox, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QLineEdit, + QLayout, + QMessageBox, + QRadioButton, + QPushButton, + QStackedWidget, + QVBoxLayout, + QWidget, +) import qstylizer.style # Local imports @@ -459,6 +472,8 @@ def _stylesheet(self): class RunDialog(BaseRunConfigDialog, SpyderFontsMixin): """Run dialog used to configure run executors.""" + sig_delete_config_requested = Signal(str, str, str, str) + def __init__( self, parent=None, @@ -476,6 +491,7 @@ def __init__( self.current_widget = None self.status = RunDialogStatus.Close self._is_shown = False + self._save_as_global = False # ---- Public methods # ------------------------------------------------------------------------- @@ -510,7 +526,7 @@ def setup(self): # --- Configuration properties config_props_group = QGroupBox(_("Configuration properties")) - config_props_layout = QVBoxLayout(config_props_group) + config_props_layout = QGridLayout(config_props_group) # Increase margin between title and line edit below so this looks good config_props_margins = config_props_layout.contentsMargins() @@ -520,13 +536,21 @@ def setup(self): # Name to save custom configuration name_params_label = QLabel(_("Name:")) self.name_params_text = QLineEdit(self) - self.name_params_text.setMinimumWidth(250) - name_params_layout = QHBoxLayout() - name_params_layout.addWidget(name_params_label) - name_params_layout.addWidget(self.name_params_text) - name_params_layout.addStretch() - config_props_layout.addLayout(name_params_layout) + # Buttons + delete_button = QPushButton(_("Delete")) + save_global_button = QPushButton(_("Save globally")) + delete_button.clicked.connect(self.delete_btn_clicked) + save_global_button.clicked.connect(self.save_global_btn_clicked) + + # Buttons layout + buttons_layout = QHBoxLayout() + buttons_layout.addWidget(delete_button) + buttons_layout.addWidget(save_global_button) + + config_props_layout.addWidget(name_params_label, 0, 0) + config_props_layout.addWidget(self.name_params_text, 0, 1) + config_props_layout.addLayout(buttons_layout, 1, 1) # --- Runner settings self.stack = QStackedWidget() @@ -632,7 +656,8 @@ def update_configuration_run_index(self, index: int): self.executor_combo.setCurrentIndex(-1) self.run_conf_model.update_index(index) self.executor_combo.setCurrentIndex( - self.executors_model.get_initial_index()) + self.executors_model.get_initial_index() + ) def update_parameter_set(self, index: int): if index < 0: @@ -742,6 +767,52 @@ def run_btn_clicked(self): self.status |= RunDialogStatus.Run self.accept() + def delete_btn_clicked(self): + answer = QMessageBox.question( + self, + _("Delete"), + _("Do you want to delete the current configuration?"), + ) + + if answer == QMessageBox.Yes: + # Get executor name + executor_name, __ = self.executors_model.get_selected_run_executor( + self.executor_combo.currentIndex() + ) + + # Get extension and context_id + metadata = self.run_conf_model.get_selected_metadata() + extension = metadata["input_extension"] + context_id = metadata["context"]["identifier"] + + # Get index associated with config + idx = self.parameters_combo.currentIndex() + + # Get config uuid + uuid, __ = self.parameter_model.get_parameters_uuid_name(idx) + + self.sig_delete_config_requested.emit( + executor_name, extension, context_id, uuid + ) + + # Close dialog to not have to deal with the difficult case of + # updating its contents after this config is deleted + self.reject() + + def save_global_btn_clicked(self): + answer = QMessageBox.question( + self, + _("Save globally"), + _( + "Do you want to save the current configuration for other " + "files?" + ), + ) + + if answer == QMessageBox.Yes: + self._save_as_global = True + self.accept() + def get_configuration( self ) -> Tuple[str, str, ExtendedRunExecutionParameters, bool]: @@ -804,7 +875,7 @@ def accept(self) -> None: uuid=uuid, name=name, params=exec_params, - file_uuid=metadata_info['uuid'] + file_uuid=None if self._save_as_global else metadata_info['uuid'] ) executor_name, __ = self.executors_model.get_selected_run_executor( @@ -814,6 +885,9 @@ def accept(self) -> None: self.saved_conf = (metadata_info['uuid'], executor_name, ext_exec_params) + # Reset attribute for next time + self._save_as_global = False + return super().accept() def showEvent(self, event): From 2706ea38df3b632298b0219df9515839f25348ac Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 20 May 2024 09:40:27 -0500 Subject: [PATCH 21/55] Editor: Remove unnecessary run extensions Those extensions are correctly added by other plugins. --- spyder/plugins/editor/plugin.py | 28 ++++---------------- spyder/plugins/editor/widgets/main_widget.py | 5 ++++ 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/spyder/plugins/editor/plugin.py b/spyder/plugins/editor/plugin.py index 8a3200156eb..28f8ca2bfa6 100644 --- a/spyder/plugins/editor/plugin.py +++ b/spyder/plugins/editor/plugin.py @@ -201,26 +201,6 @@ def on_initialize(self): lambda: self.switch_to_plugin(force_focus=True) ) - # ---- Run plugin config definitions - widget.supported_run_extensions = [ - { - 'input_extension': 'py', - 'contexts': [ - {'context': {'name': 'File'}, 'is_super': True}, - {'context': {'name': 'Selection'}, 'is_super': False}, - {'context': {'name': 'Cell'}, 'is_super': False} - ] - }, - { - 'input_extension': 'ipy', - 'contexts': [ - {'context': {'name': 'File'}, 'is_super': True}, - {'context': {'name': 'Selection'}, 'is_super': False}, - {'context': {'name': 'Cell'}, 'is_super': False} - ] - }, - ] - @on_plugin_available(plugin=Plugins.Preferences) def on_preferences_available(self): preferences = self.get_plugin(Plugins.Preferences) @@ -273,9 +253,11 @@ def on_run_available(self): self.NAME, unsupported_extensions ) ) - run.register_run_configuration_provider( - self.NAME, widget.supported_run_extensions - ) + + # This is necessary to register run configs that were added before Run + # is available + for extension in widget.supported_run_extensions: + run.register_run_configuration_provider(self.NAME, [extension]) # Buttons creation run.create_run_button( diff --git a/spyder/plugins/editor/widgets/main_widget.py b/spyder/plugins/editor/widgets/main_widget.py index 1939bc06c70..21f0c91155d 100644 --- a/spyder/plugins/editor/widgets/main_widget.py +++ b/spyder/plugins/editor/widgets/main_widget.py @@ -357,6 +357,9 @@ def __init__(self, name, plugin, parent, ignore_last_opened_files=False): self._print_editor = self._create_print_editor() self._print_editor.hide() + # To save run extensions + self.supported_run_extensions = [] + # ---- PluginMainWidget API # ------------------------------------------------------------------------ def get_title(self): @@ -3030,6 +3033,8 @@ def add_supported_run_configuration(self, config: EditorRunConfiguration): input_extension=extension, contexts=ext_contexts) self.supported_run_extensions.append(supported_extension) + # This is necessary for plugins that register run configs after Run + # is available self.sig_register_run_configuration_provider_requested.emit( [supported_extension] ) From 4e07ae54def9e8b35dd3899697cabf5d57266950 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 20 May 2024 09:48:59 -0500 Subject: [PATCH 22/55] Simplify code related to Run options in plugins to make it easier to read --- spyder/plugins/debugger/plugin.py | 48 ++++---------- spyder/plugins/externalterminal/plugin.py | 79 +++++++---------------- spyder/plugins/ipythonconsole/plugin.py | 56 ++++------------ spyder/plugins/profiler/plugin.py | 8 +-- spyder/plugins/pylint/plugin.py | 4 +- 5 files changed, 52 insertions(+), 143 deletions(-) diff --git a/spyder/plugins/debugger/plugin.py b/spyder/plugins/debugger/plugin.py index 2ae0fa8910a..1f9e92c97c7 100644 --- a/spyder/plugins/debugger/plugin.py +++ b/spyder/plugins/debugger/plugin.py @@ -80,15 +80,9 @@ def on_initialize(self): 'origin': self.NAME, 'extension': 'py', 'contexts': [ - { - 'name': 'File' - }, - { - 'name': 'Cell' - }, - { - 'name': 'Selection' - }, + {'name': 'File'}, + {'name': 'Cell'}, + {'name': 'Selection'}, ] } @@ -96,24 +90,16 @@ def on_initialize(self): 'origin': self.NAME, 'extension': 'ipy', 'contexts': [ - { - 'name': 'File' - }, - { - 'name': 'Cell' - }, - { - 'name': 'Selection' - }, + {'name': 'File'}, + {'name': 'Cell'}, + {'name': 'Selection'}, ] } self.executor_configuration = [ { 'input_extension': 'py', - 'context': { - 'name': 'File' - }, + 'context': {'name': 'File'}, 'output_formats': [], 'configuration_widget': IPythonConfigOptions, 'requires_cwd': True, @@ -121,9 +107,7 @@ def on_initialize(self): }, { 'input_extension': 'ipy', - 'context': { - 'name': 'File' - }, + 'context': {'name': 'File'}, 'output_formats': [], 'configuration_widget': IPythonConfigOptions, 'requires_cwd': True, @@ -131,9 +115,7 @@ def on_initialize(self): }, { 'input_extension': 'py', - 'context': { - 'name': 'Cell' - }, + 'context': {'name': 'Cell'}, 'output_formats': [], 'configuration_widget': None, 'requires_cwd': True, @@ -141,9 +123,7 @@ def on_initialize(self): }, { 'input_extension': 'ipy', - 'context': { - 'name': 'Cell' - }, + 'context': {'name': 'Cell'}, 'output_formats': [], 'configuration_widget': None, 'requires_cwd': True, @@ -151,9 +131,7 @@ def on_initialize(self): }, { 'input_extension': 'py', - 'context': { - 'name': 'Selection' - }, + 'context': {'name': 'Selection'}, 'output_formats': [], 'configuration_widget': None, 'requires_cwd': True, @@ -161,9 +139,7 @@ def on_initialize(self): }, { 'input_extension': 'ipy', - 'context': { - 'name': 'Selection' - }, + 'context': {'name': 'Selection'}, 'output_formats': [], 'configuration_widget': None, 'requires_cwd': True, diff --git a/spyder/plugins/externalterminal/plugin.py b/spyder/plugins/externalterminal/plugin.py index f843ec17874..8d37df13ef7 100644 --- a/spyder/plugins/externalterminal/plugin.py +++ b/spyder/plugins/externalterminal/plugin.py @@ -62,20 +62,14 @@ def on_initialize(self): { 'origin': self.NAME, 'extension': 'py', - 'contexts': [ - { - 'name': 'File' - } - ] + 'contexts': [{'name': 'File'}] } ] self.executor_configuration = [ { 'input_extension': 'py', - 'context': { - 'name': 'File' - }, + 'context': {'name': 'File'}, 'output_formats': [], 'configuration_widget': ExternalTerminalPyConfiguration, 'requires_cwd': True, @@ -87,36 +81,27 @@ def on_initialize(self): self.editor_configurations.append({ 'origin': self.NAME, 'extension': 'bat', - 'contexts': [ - { - 'name': 'File' - }, - { - 'name': 'Selection' - } - ] + 'contexts': [{'name': 'File'}, {'name': 'Selection'}] }) self.executor_configuration.append({ 'input_extension': 'bat', - 'context': { - 'name': 'File' - }, + 'context': {'name': 'File'}, 'output_formats': [], 'configuration_widget': ExternalTerminalShConfiguration( - 'cmd.exe', '/K'), + 'cmd.exe', '/K' + ), 'requires_cwd': True, 'priority': 1 }) self.executor_configuration.append({ 'input_extension': 'bat', - 'context': { - 'name': 'Selection' - }, + 'context': {'name': 'Selection'}, 'output_formats': [], 'configuration_widget': ExternalTerminalShConfiguration( - 'cmd.exe', '/K'), + 'cmd.exe', '/K' + ), 'requires_cwd': True, 'priority': 1 }) @@ -124,36 +109,27 @@ def on_initialize(self): self.editor_configurations.append({ 'origin': self.NAME, 'extension': 'ps1', - 'contexts': [ - { - 'name': 'File' - }, - { - 'name': 'Selection' - } - ] + 'contexts': [{'name': 'File'}, {'name': 'Selection'}] }) self.executor_configuration.append({ 'input_extension': 'ps1', - 'context': { - 'name': 'File' - }, + 'context': {'name': 'File'}, 'output_formats': [], 'configuration_widget': ExternalTerminalShConfiguration( - 'powershell.exe'), + 'powershell.exe' + ), 'requires_cwd': True, 'priority': 1 }) self.executor_configuration.append({ 'input_extension': 'ps1', - 'context': { - 'name': 'Selection' - }, + 'context': {'name': 'Selection'}, 'output_formats': [], 'configuration_widget': ExternalTerminalShConfiguration( - 'powershell.exe'), + 'powershell.exe' + ), 'requires_cwd': True, 'priority': 1 }) @@ -169,36 +145,27 @@ def on_initialize(self): self.editor_configurations.append({ 'origin': self.NAME, 'extension': 'sh', - 'contexts': [ - { - 'name': 'File' - }, - { - 'name': 'Selection' - } - ] + 'contexts': [{'name': 'File'}, {'name': 'Selection'}] }) self.executor_configuration.append({ 'input_extension': 'sh', - 'context': { - 'name': 'File' - }, + 'context': {'name': 'File'}, 'output_formats': [], 'configuration_widget': ExternalTerminalShConfiguration( - programs.is_program_installed(default_shell)), + programs.is_program_installed(default_shell) + ), 'requires_cwd': True, 'priority': 1 }) self.executor_configuration.append({ 'input_extension': 'sh', - 'context': { - 'name': 'Selection' - }, + 'context': {'name': 'Selection'}, 'output_formats': [], 'configuration_widget': ExternalTerminalShConfiguration( - programs.is_program_installed(default_shell)), + programs.is_program_installed(default_shell) + ), 'requires_cwd': True, 'priority': 1 }) diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py index 83c97b90446..55411653c8d 100644 --- a/spyder/plugins/ipythonconsole/plugin.py +++ b/spyder/plugins/ipythonconsole/plugin.py @@ -249,9 +249,7 @@ def on_initialize(self): 'origin': self.NAME, 'extension': 'pyx', 'contexts': [ - { - 'name': 'File' - } + {'name': 'File'} ] } @@ -259,15 +257,9 @@ def on_initialize(self): 'origin': self.NAME, 'extension': 'py', 'contexts': [ - { - 'name': 'File' - }, - { - 'name': 'Cell' - }, - { - 'name': 'Selection' - }, + {'name': 'File'}, + {'name': 'Cell'}, + {'name': 'Selection'}, ] } @@ -275,24 +267,16 @@ def on_initialize(self): 'origin': self.NAME, 'extension': 'ipy', 'contexts': [ - { - 'name': 'File' - }, - { - 'name': 'Cell' - }, - { - 'name': 'Selection' - }, + {'name': 'File'}, + {'name': 'Cell'}, + {'name': 'Selection'}, ] } self.executor_configuration = [ { 'input_extension': 'py', - 'context': { - 'name': 'File' - }, + 'context': {'name': 'File'}, 'output_formats': [], 'configuration_widget': IPythonConfigOptions, 'requires_cwd': True, @@ -300,9 +284,7 @@ def on_initialize(self): }, { 'input_extension': 'ipy', - 'context': { - 'name': 'File' - }, + 'context': {'name': 'File'}, 'output_formats': [], 'configuration_widget': IPythonConfigOptions, 'requires_cwd': True, @@ -310,9 +292,7 @@ def on_initialize(self): }, { 'input_extension': 'py', - 'context': { - 'name': 'Cell' - }, + 'context': {'name': 'Cell'}, 'output_formats': [], 'configuration_widget': None, 'requires_cwd': True, @@ -320,9 +300,7 @@ def on_initialize(self): }, { 'input_extension': 'ipy', - 'context': { - 'name': 'Cell' - }, + 'context': {'name': 'Cell'}, 'output_formats': [], 'configuration_widget': None, 'requires_cwd': True, @@ -330,9 +308,7 @@ def on_initialize(self): }, { 'input_extension': 'py', - 'context': { - 'name': 'Selection' - }, + 'context': {'name': 'Selection'}, 'output_formats': [], 'configuration_widget': None, 'requires_cwd': True, @@ -340,9 +316,7 @@ def on_initialize(self): }, { 'input_extension': 'ipy', - 'context': { - 'name': 'Selection' - }, + 'context': {'name': 'Selection'}, 'output_formats': [], 'configuration_widget': None, 'requires_cwd': True, @@ -350,9 +324,7 @@ def on_initialize(self): }, { 'input_extension': 'pyx', - 'context': { - 'name': 'File' - }, + 'context': {'name': 'File'}, 'output_formats': [], 'configuration_widget': IPythonConfigOptions, 'requires_cwd': True, diff --git a/spyder/plugins/profiler/plugin.py b/spyder/plugins/profiler/plugin.py index 78e44756341..e315e09f5f7 100644 --- a/spyder/plugins/profiler/plugin.py +++ b/spyder/plugins/profiler/plugin.py @@ -76,9 +76,7 @@ def on_initialize(self): self.executor_configuration = [ { 'input_extension': 'py', - 'context': { - 'name': 'File' - }, + 'context': {'name': 'File'}, 'output_formats': [], 'configuration_widget': ProfilerPyConfigurationGroup, 'requires_cwd': True, @@ -86,9 +84,7 @@ def on_initialize(self): }, { 'input_extension': 'ipy', - 'context': { - 'name': 'File' - }, + 'context': {'name': 'File'}, 'output_formats': [], 'configuration_widget': ProfilerPyConfigurationGroup, 'requires_cwd': True, diff --git a/spyder/plugins/pylint/plugin.py b/spyder/plugins/pylint/plugin.py index 90ed0841be1..aa0c65f078d 100644 --- a/spyder/plugins/pylint/plugin.py +++ b/spyder/plugins/pylint/plugin.py @@ -90,9 +90,7 @@ def on_initialize(self): self.executor_configuration = [ { 'input_extension': 'py', - 'context': { - 'name': 'File' - }, + 'context': {'name': 'File'}, 'output_formats': [], 'configuration_widget': None, 'requires_cwd': False, From a070cf625e6a210b6dc260a723027f9fc5c9b61d Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 20 May 2024 17:14:37 -0500 Subject: [PATCH 23/55] Run: Save default executor configurations to our config system - That way those configurations will be displayed in the Run confpage, which will allow users to easily check what are the default parameters for each one. - Also, fix showing the last selected parameters for the file after opening the dialog. --- spyder/config/main.py | 1 - spyder/plugins/run/container.py | 84 +++++++++++++++++++++++++++++++++ spyder/plugins/run/models.py | 68 ++++++++++---------------- spyder/plugins/run/widgets.py | 38 +++++++-------- 4 files changed, 126 insertions(+), 65 deletions(-) diff --git a/spyder/config/main.py b/spyder/config/main.py index 0a5b0ebdb5d..1ba9ad4f2db 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -641,7 +641,6 @@ ('run', [ 'breakpoints', 'configurations', - 'defaultconfiguration', 'default/wdir/fixed_directory', 'last_used_parameters', 'parameters' diff --git a/spyder/plugins/run/container.py b/spyder/plugins/run/container.py index 7839db0e47e..c385ee0a79b 100644 --- a/spyder/plugins/run/container.py +++ b/spyder/plugins/run/container.py @@ -10,6 +10,7 @@ import functools import os.path as osp from typing import Callable, List, Dict, Tuple, Set, Optional +from uuid import uuid4 from weakref import WeakSet, WeakValueDictionary # Third-party imports @@ -772,6 +773,21 @@ def register_executor_configuration( ext, context_id, executor_id, config) executor_count += 1 + # Save default configs to our config system so that they are + # displayed in the Run confpage + config_widget = config["configuration_widget"] + default_conf = ( + config_widget.get_default_configuration() + if config_widget + else {} + ) + self._save_default_graphical_executor_configuration( + executor_id, + ext, + context_id, + default_conf + ) + self.executor_use_count[executor_id] = executor_count self.executor_model.set_executor_name(executor_id, executor_name) self.set_actions_status() @@ -1087,3 +1103,71 @@ def set_last_used_execution_params( mru_executors_uuids[uuid] = params self.set_conf('last_used_parameters', mru_executors_uuids) + + # ---- Private API + # ------------------------------------------------------------------------- + def _save_default_graphical_executor_configuration( + self, + executor_name: str, + extension: str, + context_id: str, + default_conf: dict, + ): + """ + Save a default executor configuration to our config system. + + Parameters + ---------- + executor_name: str + The identifier of the run executor. + extension: str + The file extension to register the configuration parameters for. + context_id: str + The context to register the configuration parameters for. + default_conf: dict + A dictionary containing the run configuration parameters for the + given executor. + """ + # Check if there's already a default parameter config to not do this + # because it's not necessary. + current_params = self.get_executor_configuration_parameters( + executor_name, + extension, + context_id + ) + + for param in current_params["params"].values(): + if param["name"] == _("Default"): + return + + # Id for this config + uuid = str(uuid4()) + + # Build config + cwd_opts = WorkingDirOpts( + source=WorkingDirSource.ConfigurationDirectory, + path=None + ) + + exec_params = RunExecutionParameters( + working_dir=cwd_opts, executor_params=default_conf + ) + + ext_exec_params = ExtendedRunExecutionParameters( + uuid=uuid, + name=_("Default"), + params=exec_params, + file_uuid=None, + ) + + store_params = StoredRunExecutorParameters( + params={uuid: ext_exec_params} + ) + + # Save config + self.set_executor_configuration_parameters( + executor_name, + extension, + context_id, + store_params, + ) diff --git a/spyder/plugins/run/models.py b/spyder/plugins/run/models.py index 4bd6f053d18..04e30300a06 100644 --- a/spyder/plugins/run/models.py +++ b/spyder/plugins/run/models.py @@ -17,10 +17,15 @@ # Local imports from spyder.api.translations import _ from spyder.plugins.run.api import ( - StoredRunExecutorParameters, RunContext, RunConfigurationMetadata, - SupportedExecutionRunConfiguration, RunParameterFlags, - StoredRunConfigurationExecutor, ExtendedRunExecutionParameters, - RunExecutionParameters, WorkingDirOpts, WorkingDirSource) + ExtendedRunExecutionParameters, + RunConfigurationMetadata, + RunContext, + RunExecutionParameters, + RunParameterFlags, + StoredRunConfigurationExecutor, + StoredRunExecutorParameters, + SupportedExecutionRunConfiguration, +) class RunExecutorListModel(QAbstractListModel): @@ -283,50 +288,30 @@ def __init__(self, parent): def data(self, index: QModelIndex, role: int = Qt.DisplayRole): pos = index.row() - total_saved_params = len(self.executor_conf_params) - - if pos == total_saved_params or pos == -1: - if role == Qt.DisplayRole or role == Qt.EditRole: - return _("Default") - else: - params_id = self.params_index[pos] - params = self.executor_conf_params[params_id] - params_name = params['name'] - if role == Qt.DisplayRole or role == Qt.EditRole: - return params_name + pos = 0 if pos == -1 else pos + params_id = self.params_index[pos] + params = self.executor_conf_params[params_id] + params_name = params['name'] + if role == Qt.DisplayRole or role == Qt.EditRole: + return params_name def rowCount(self, parent: QModelIndex = ...) -> int: - return len(self.executor_conf_params) + 1 + return len(self.executor_conf_params) def get_executor_parameters( self, index: int ) -> Tuple[RunParameterFlags, RunExecutionParameters]: - if index == len(self) - 1: - default_working_dir = WorkingDirOpts( - source=WorkingDirSource.ConfigurationDirectory, - path=None) - default_params = RunExecutionParameters( - working_dir=default_working_dir, - executor_params={}) - - return RunParameterFlags.SetDefaults, default_params - else: - params_id = self.params_index[index] - params = self.executor_conf_params[params_id] - actual_params = params['params'] - - return RunParameterFlags.SwitchValues, actual_params + params_id = self.params_index[index] + params = self.executor_conf_params[params_id] + actual_params = params['params'] + return RunParameterFlags.SwitchValues, actual_params def get_parameters_uuid_name( - self, - index: int + self, index: int ) -> Tuple[Optional[str], Optional[str]]: - if index == len(self) - 1: - return None, None - params_id = self.params_index[index] params = self.executor_conf_params[params_id] return params['uuid'], params['name'] @@ -344,14 +329,9 @@ def set_parameters( self.endResetModel() def get_parameters_index_by_uuid( - self, - parameters_uuid: Optional[str] + self, parameters_uuid: Optional[str] ) -> int: - index = self.inverse_index.get( - parameters_uuid, len(self.executor_conf_params) - ) - - return index + return self.inverse_index.get(parameters_uuid, 0) def get_parameters_index_by_name(self, parameters_name: str) -> int: index = -1 @@ -363,7 +343,7 @@ def get_parameters_index_by_name(self, parameters_name: str) -> int: return index def __len__(self) -> int: - return len(self.executor_conf_params) + 1 + return len(self.executor_conf_params) class RunExecutorNamesListModel(QAbstractListModel): diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 790b8b566ce..1b02f15139b 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -40,9 +40,13 @@ from spyder.api.widgets.comboboxes import SpyderComboBox from spyder.api.widgets.dialogs import SpyderDialogButtonBox from spyder.plugins.run.api import ( - RunParameterFlags, WorkingDirSource, WorkingDirOpts, - RunExecutionParameters, ExtendedRunExecutionParameters, - RunExecutorConfigurationGroup, SupportedExecutionRunConfiguration) + ExtendedRunExecutionParameters, + RunExecutorConfigurationGroup, + RunExecutionParameters, + SupportedExecutionRunConfiguration, + WorkingDirOpts, + WorkingDirSource, +) from spyder.utils.icon_manager import ima from spyder.utils.misc import getcwd_or_home from spyder.utils.palette import SpyderPalette @@ -622,6 +626,15 @@ def setup(self): self.display_executor_configuration) self.executor_combo.setModel(self.executors_model) + # This signal needs to be connected after + # executor_combo.currentIndexChanged and before + # configuration_combo.currentIndexChanged for parameters_combo to be + # updated as expected when opening the dialog. + self.parameters_combo.currentIndexChanged.connect( + self.update_parameter_set + ) + self.parameters_combo.setModel(self.parameter_model) + self.configuration_combo.currentIndexChanged.connect( self.update_configuration_run_index) self.configuration_combo.setModel(self.run_conf_model) @@ -633,10 +646,6 @@ def setup(self): self.executor_combo.view().setVerticalScrollBarPolicy( Qt.ScrollBarAsNeeded) - self.parameters_combo.currentIndexChanged.connect( - self.update_parameter_set) - self.parameters_combo.setModel(self.parameter_model) - self.setWindowTitle(_("Run configuration per file")) self.layout().setSizeConstraint(QLayout.SetFixedSize) @@ -663,17 +672,9 @@ def update_parameter_set(self, index: int): if index < 0: return - if self.index_to_select is not None: - index = self.index_to_select - self.index_to_select = None - self.parameters_combo.setCurrentIndex(index) - action, params = self.parameter_model.get_executor_parameters(index) working_dir_params = params['working_dir'] stored_parameters = params['executor_params'] - - if action == RunParameterFlags.SetDefaults: - stored_parameters = self.current_widget.get_default_configuration() self.current_widget.set_configuration(stored_parameters) source = working_dir_params['source'] @@ -743,14 +744,11 @@ def display_executor_configuration(self, index: int): self.parameter_model.set_parameters(stored_params) selected_params = self.run_conf_model.get_last_used_execution_params( uuid, executor_name) - index = self.parameter_model.get_parameters_index_by_uuid( + params_index = self.parameter_model.get_parameters_index_by_uuid( selected_params ) - if self.parameters_combo.count() == 0: - self.index_to_select = index - - self.parameters_combo.setCurrentIndex(index) + self.parameters_combo.setCurrentIndex(params_index) self.adjustSize() def select_executor(self, executor_name: str): From e22e6c9100f15e0e6ec46ab5ce06e2a18a5bf276 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 21 May 2024 09:30:36 -0500 Subject: [PATCH 24/55] Run: Don't allow to remove default executor parameters in its confpage --- spyder/plugins/run/api.py | 3 +++ spyder/plugins/run/confpage.py | 36 ++++++++++++++++++++++++--------- spyder/plugins/run/container.py | 3 ++- spyder/plugins/run/widgets.py | 6 ++++-- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/spyder/plugins/run/api.py b/spyder/plugins/run/api.py index 32ea9d33cc1..c4e3a50acc4 100644 --- a/spyder/plugins/run/api.py +++ b/spyder/plugins/run/api.py @@ -252,6 +252,9 @@ class ExtendedRunExecutionParameters(TypedDict): # to, if any. file_uuid: Optional[str] + # To identify these parameters as default + default: bool + class StoredRunExecutorParameters(TypedDict): """Per run executor configuration parameters.""" diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index 80091ff08c6..4ac56d064cd 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -66,7 +66,17 @@ def focusInEvent(self, e): def selection(self, index): self.update() self.isActiveWindow() - self._parent.set_clone_delete_btn_status() + + # Detect if a row corresponds to a set of default parameters to prevent + # users from deleting it. + index = self.currentIndex().row() + is_default = False + if index >= 0: + params_id = self._parent.table_model.params_index[index] + params = self._parent.table_model.executor_conf_params[params_id] + is_default = True if params.get("default") else False + + self._parent.set_clone_delete_btn_status(is_default=is_default) def adjust_cells(self): """Adjust column size based on contents.""" @@ -304,17 +314,23 @@ def executor_index_changed(self, index: int): self.previous_executor_index = index self.set_clone_delete_btn_status() - def set_clone_delete_btn_status(self): - status = ( - self.table_model.rowCount() != 0 - and self.params_table.currentIndex().isValid() - ) - + def set_clone_delete_btn_status(self, is_default=False): + # We need to enclose the code below in a try/except because these + # buttons might not be created yet, which gives an AttributeError. try: - self.delete_configuration_btn.setEnabled(status) - self.clone_configuration_btn.setEnabled(status) + if is_default: + # Don't allow to delete default configurations, only to clone + # them + self.delete_configuration_btn.setEnabled(False) + self.clone_configuration_btn.setEnabled(True) + else: + status = ( + self.table_model.rowCount() != 0 + and self.params_table.currentIndex().isValid() + ) + self.delete_configuration_btn.setEnabled(status) + self.clone_configuration_btn.setEnabled(status) except AttributeError: - # Buttons might not be created yet pass def get_executor_configurations(self) -> Dict[ diff --git a/spyder/plugins/run/container.py b/spyder/plugins/run/container.py index c385ee0a79b..f60772622ff 100644 --- a/spyder/plugins/run/container.py +++ b/spyder/plugins/run/container.py @@ -1137,7 +1137,7 @@ def _save_default_graphical_executor_configuration( ) for param in current_params["params"].values(): - if param["name"] == _("Default"): + if param["default"]: return # Id for this config @@ -1158,6 +1158,7 @@ def _save_default_graphical_executor_configuration( name=_("Default"), params=exec_params, file_uuid=None, + default=True, ) store_params = StoredRunExecutorParameters( diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 1b02f15139b..f3c3305f015 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -452,7 +452,8 @@ def accept(self) -> None: uuid=uuid, name=name, params=exec_params, - file_uuid=None + file_uuid=None, + default=False, ) self.saved_conf = (self.selected_extension, self.selected_context, @@ -873,7 +874,8 @@ def accept(self) -> None: uuid=uuid, name=name, params=exec_params, - file_uuid=None if self._save_as_global else metadata_info['uuid'] + file_uuid=None if self._save_as_global else metadata_info['uuid'], + default=False ) executor_name, __ = self.executors_model.get_selected_run_executor( From 736721b95bc454b621bfa06dfd4a3eef71693a4e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 21 May 2024 10:16:25 -0500 Subject: [PATCH 25/55] Run: More UI improvements to its confpage - Simplify text for its opening label and buttons. - Add label to the left of the executor combobox to make clearer what it's about. - Don't show parameters table inside a QGroupBox to decrease its importance (they depend on the executor). - Add left and right horizontal margins to the parameters table to improve the table's left alignment. --- spyder/plugins/run/confpage.py | 65 +++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index 4ac56d064cd..2b0a1e7c2d7 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -14,9 +14,16 @@ # Third party imports from qtpy.QtCore import Qt -from qtpy.QtWidgets import (QGroupBox, QLabel, QVBoxLayout, - QAbstractItemView, QPushButton, QGridLayout, - QHeaderView, QWidget) +from qtpy.QtWidgets import ( + QAbstractItemView, + QGridLayout, + QHBoxLayout, + QHeaderView, + QLabel, + QPushButton, + QVBoxLayout, + QWidget, +) # Local imports from spyder.api.preferences import PluginConfigPage @@ -29,6 +36,7 @@ RunExecutorNamesListModel, ExecutorRunParametersTableModel) from spyder.plugins.run.widgets import ( ExecutionParametersDialog, RunDialogStatus) +from spyder.utils.stylesheet import AppStyle from spyder.widgets.helperwidgets import HoverRowsTableView @@ -194,34 +202,41 @@ def setup_page(self): ExtendedRunExecutionParameters]] = {} about_label = QLabel( - _("The following are the global configuration settings of the " - "different plugins that can run files, cells or selections in " - "Spyder. These options can be overridden in the " - "Configuration per file entry of the Run menu.") + _( + "The following are the global configuration settings of the " + "different plugins that can execute files in Spyder." + ) ) about_label.setWordWrap(True) + # The paremeters table needs to be created before the executor_combo + # below, although is displayed after it. + params_label = QLabel(_('Available parameters:')) self.params_table = RunParametersTableView(self, self.table_model) self.params_table.setMaximumHeight(180) + params_table_layout = QHBoxLayout() + params_table_layout.addSpacing(2 * AppStyle.MarginSize) + params_table_layout.addWidget(self.params_table) + params_table_layout.addSpacing(2 * AppStyle.MarginSize) + + executor_label = QLabel(_("Executor:")) self.executor_combo = SpyderComboBox(self) + self.executor_combo.setMinimumWidth(250) self.executor_combo.currentIndexChanged.connect( self.executor_index_changed ) self.executor_combo.setModel(self.executor_model) - params_group = QGroupBox(_('Available execution parameters')) - params_layout = QVBoxLayout(params_group) - params_layout.addWidget(self.params_table) - - self.new_configuration_btn = QPushButton( - _("Create new parameters")) - self.clone_configuration_btn = QPushButton( - _("Clone currently selected parameters")) - self.delete_configuration_btn = QPushButton( - _("Delete currently selected parameters")) - self.reset_configuration_btn = QPushButton( - _("Reset parameters")) + executor_layout = QHBoxLayout() + executor_layout.addWidget(executor_label) + executor_layout.addWidget(self.executor_combo) + executor_layout.addStretch() + + self.new_configuration_btn = QPushButton(_("New parameters")) + self.clone_configuration_btn = QPushButton(_("Clone selected")) + self.delete_configuration_btn = QPushButton(_("Delete selected")) + self.reset_configuration_btn = QPushButton(_("Reset")) self.delete_configuration_btn.setEnabled(False) self.clone_configuration_btn.setEnabled(False) @@ -262,12 +277,14 @@ def setup_page(self): vlayout = QVBoxLayout() vlayout.addWidget(about_label) - vlayout.addSpacing(9) - vlayout.addWidget(self.executor_combo) - vlayout.addSpacing(9) - vlayout.addWidget(params_group) + vlayout.addSpacing(3 * AppStyle.MarginSize) + vlayout.addLayout(executor_layout) + vlayout.addSpacing(3 * AppStyle.MarginSize) + vlayout.addWidget(params_label) + vlayout.addLayout(params_table_layout) + vlayout.addSpacing(2 * AppStyle.MarginSize) vlayout.addLayout(sn_buttons_layout) - vlayout.addStretch(1) + vlayout.addStretch() executor_widget = QWidget() executor_widget.setLayout(vlayout) From fac3f3bc0646aef535c52116cc0ed97c187c4b3b Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 21 May 2024 11:34:32 -0500 Subject: [PATCH 26/55] Run: Improve UI of ExecutorRunParametersTableModel - Show parameters name in the first column, then extension and finally context. - Reorganize parameters so that Python and IPython extensions are shown first and second by default. - Simplify column names. - If parameters are default, show a localized version of "Default" instead of its actual name. That's useful in case users switch between select different languages in the interface. --- spyder/plugins/run/models.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/spyder/plugins/run/models.py b/spyder/plugins/run/models.py index 04e30300a06..ed8f9f17c6c 100644 --- a/spyder/plugins/run/models.py +++ b/spyder/plugins/run/models.py @@ -388,9 +388,9 @@ def selected_executor( class ExecutorRunParametersTableModel(QAbstractTableModel): - EXTENSION = 0 - CONTEXT = 1 - NAME = 2 + NAME = 0 + EXTENSION = 1 + CONTEXT = 2 sig_data_changed = Signal() @@ -417,7 +417,7 @@ def data(self, index: QModelIndex, elif column == self.CONTEXT: return context elif column == self.NAME: - return params['name'] + return _("Default") if params['default'] else params['name'] elif role == Qt.TextAlignmentRole: return int(Qt.AlignHCenter | Qt.AlignVCenter) elif role == Qt.ToolTipRole: @@ -440,9 +440,9 @@ def headerData( if section == self.EXTENSION: return _('File extension') elif section == self.CONTEXT: - return _('Context name') + return _('Context') elif section == self.NAME: - return _('Run parameters name') + return _('Parameters name') def rowCount(self, parent: QModelIndex = None) -> int: return len(self.params_index) @@ -455,9 +455,24 @@ def set_parameters( params: Dict[Tuple[str, str, str], ExtendedRunExecutionParameters] ): self.beginResetModel() + + # Reorder params so that Python and IPython extensions are shown first + # and second by default, respectively. + ordered_params = [] + for k,v in params.items(): + if k[0] == "py": + ordered_params.insert(0, (k, v)) + elif k[0] == "ipy": + ordered_params.insert(1, (k, v)) + else: + ordered_params.append((k, v)) + params = dict(ordered_params) + + # Update attributes self.executor_conf_params = params self.params_index = dict(enumerate(params)) self.inverse_index = {v: k for k, v in self.params_index.items()} + self.endResetModel() def get_current_view( From 3b5b77323b0789ff2c579358d79c0e517aa94285 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 21 May 2024 21:39:30 -0500 Subject: [PATCH 27/55] Run: Add "Edit selected" button to its confpage That way it'll be easier for users to understand that they can edit even the default configurations. --- spyder/plugins/run/confpage.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index 2b0a1e7c2d7..638c8a6a421 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -65,12 +65,6 @@ def __init__(self, parent, model): ) self.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) - def focusInEvent(self, e): - """Qt Override.""" - super().focusInEvent(e) - self.selectRow(self.currentIndex().row()) - self.selection(self.currentIndex().row()) - def selection(self, index): self.update() self.isActiveWindow() @@ -86,6 +80,9 @@ def selection(self, index): self._parent.set_clone_delete_btn_status(is_default=is_default) + # Always enable edit button + self._parent.edit_configuration_btn.setEnabled(True) + def adjust_cells(self): """Adjust column size based on contents.""" self.resizeColumnsToContents() @@ -164,6 +161,12 @@ def process_run_dialog_result(self, result, new=False, def clone_configuration(self): self.show_editor(clone=True) + def focusInEvent(self, e): + """Qt Override.""" + super().focusInEvent(e) + self.selectRow(self.currentIndex().row()) + self.selection(self.currentIndex().row()) + def keyPressEvent(self, event): """Qt Override.""" key = event.key() @@ -234,14 +237,19 @@ def setup_page(self): executor_layout.addStretch() self.new_configuration_btn = QPushButton(_("New parameters")) + self.edit_configuration_btn = QPushButton(_("Edit selected")) self.clone_configuration_btn = QPushButton(_("Clone selected")) self.delete_configuration_btn = QPushButton(_("Delete selected")) self.reset_configuration_btn = QPushButton(_("Reset")) + self.edit_configuration_btn.setEnabled(False) self.delete_configuration_btn.setEnabled(False) self.clone_configuration_btn.setEnabled(False) self.new_configuration_btn.clicked.connect( self.create_new_configuration) + self.edit_configuration_btn.clicked.connect( + lambda checked: self.params_table.show_editor() + ) self.clone_configuration_btn.clicked.connect( self.clone_configuration) self.delete_configuration_btn.clicked.connect( @@ -251,8 +259,9 @@ def setup_page(self): # Buttons layout btns = [ self.new_configuration_btn, - self.clone_configuration_btn, + self.edit_configuration_btn, self.delete_configuration_btn, + self.clone_configuration_btn, self.reset_configuration_btn ] sn_buttons_layout = QGridLayout() @@ -331,6 +340,11 @@ def executor_index_changed(self, index: int): self.previous_executor_index = index self.set_clone_delete_btn_status() + # Repopulating the params table removes any selection, so we need to + # disable the edit button. + if hasattr(self, "edit_configuration_btn"): + self.edit_configuration_btn.setEnabled(False) + def set_clone_delete_btn_status(self, is_default=False): # We need to enclose the code below in a try/except because these # buttons might not be created yet, which gives an AttributeError. @@ -395,6 +409,7 @@ def create_new_configuration(self): def clone_configuration(self): self.params_table.clone_configuration() + self.edit_configuration_btn.setEnabled(False) def delete_configuration(self): executor_name, _ = self.executor_model.selected_executor( @@ -414,6 +429,7 @@ def delete_configuration(self): self.table_model.reset_model() self.set_modified(True) self.set_clone_delete_btn_status() + self.edit_configuration_btn.setEnabled(False) def reset_to_default(self): self.all_executor_model = deepcopy(self.default_executor_conf_params) From ddb85418623158471f5ee4004770ffdfc6f78631 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 21 May 2024 23:18:18 -0500 Subject: [PATCH 28/55] Run: Improve ExecutionParametersDialog UI when displaying default params - Use localized version of "Default" string instead of the params name. - Don't allow to change the params name. --- spyder/plugins/run/widgets.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index f3c3305f015..eb55b35ecea 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -176,7 +176,11 @@ def __init__( self.parameters_name = None if default_params is not None: - self.parameters_name = default_params['name'] + self.parameters_name = ( + _("Default") + if default_params["default"] + else default_params["name"] + ) self.current_widget = None self.status = RunDialogStatus.Close @@ -295,6 +299,10 @@ def setup(self): if self.parameters_name: self.store_params_text.setText(self.parameters_name) + # Don't allow to change name if params are default ones. + if self.default_params["default"]: + self.store_params_text.setEnabled(False) + # --- Stylesheet self.setStyleSheet(self._stylesheet) From 74d45fb2ee1371e7d43e77661b3b685d98b31705 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 21 May 2024 23:33:46 -0500 Subject: [PATCH 29/55] Run: Improve code and UX related to reset changes in its confpage --- spyder/plugins/run/confpage.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index 638c8a6a421..9f271c7b1be 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -240,7 +240,7 @@ def setup_page(self): self.edit_configuration_btn = QPushButton(_("Edit selected")) self.clone_configuration_btn = QPushButton(_("Clone selected")) self.delete_configuration_btn = QPushButton(_("Delete selected")) - self.reset_configuration_btn = QPushButton(_("Reset")) + self.reset_changes_btn = QPushButton(_("Reset changes")) self.edit_configuration_btn.setEnabled(False) self.delete_configuration_btn.setEnabled(False) self.clone_configuration_btn.setEnabled(False) @@ -254,7 +254,7 @@ def setup_page(self): self.clone_configuration) self.delete_configuration_btn.clicked.connect( self.delete_configuration) - self.reset_configuration_btn.clicked.connect(self.reset_to_default) + self.reset_changes_btn.clicked.connect(self.reset_changes) # Buttons layout btns = [ @@ -262,7 +262,7 @@ def setup_page(self): self.edit_configuration_btn, self.delete_configuration_btn, self.clone_configuration_btn, - self.reset_configuration_btn + self.reset_changes_btn, ] sn_buttons_layout = QGridLayout() for i, btn in enumerate(btns): @@ -431,7 +431,8 @@ def delete_configuration(self): self.set_clone_delete_btn_status() self.edit_configuration_btn.setEnabled(False) - def reset_to_default(self): + def reset_changes(self): + """Reset changes to the parameters loaded when the page was created.""" self.all_executor_model = deepcopy(self.default_executor_conf_params) executor_name, _ = self.executor_model.selected_executor( self.previous_executor_index @@ -439,7 +440,7 @@ def reset_to_default(self): executor_params = self.all_executor_model[executor_name] self.table_model.set_parameters(executor_params) self.table_model.reset_model() - self.set_modified(False) + self.set_modified(True) self.set_clone_delete_btn_status() def apply_settings(self): From 02c753b4a068e564cb64e2bf863f4c0438d8f10a Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 23 May 2024 10:45:28 -0500 Subject: [PATCH 30/55] Stylesheet: Move style declaration to SidebarDialog That's because it seems to be necessary only there. --- spyder/utils/stylesheet.py | 5 ----- spyder/widgets/sidebardialog.py | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spyder/utils/stylesheet.py b/spyder/utils/stylesheet.py index 7ca3ae27cb8..10d54039fdd 100644 --- a/spyder/utils/stylesheet.py +++ b/spyder/utils/stylesheet.py @@ -295,11 +295,6 @@ def _customize_stylesheet(self): padding="1px 2px", ) - # Substract extra padding that comes from QLineEdit - css["QLineEdit QToolTip"].setValues( - padding="-2px -3px", - ) - # Add padding to tree widget items to make them look better css["QTreeWidget::item"].setValues( padding=f"{AppStyle.MarginSize - 1}px 0px", diff --git a/spyder/widgets/sidebardialog.py b/spyder/widgets/sidebardialog.py index 7379ebaf369..86593cf747b 100644 --- a/spyder/widgets/sidebardialog.py +++ b/spyder/widgets/sidebardialog.py @@ -482,6 +482,11 @@ def _main_stylesheet(self): marginBottom='15px', ) + # Substract extra padding that comes from QLineEdit + css["QLineEdit QToolTip"].setValues( + padding="-2px -3px", + ) + return css.toString() def _generate_contents_stylesheet(self): From daed31685ace4818ce1f4def58ab6264b19b24ce Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 24 May 2024 10:38:27 -0500 Subject: [PATCH 31/55] Run: Fix saving and executing custom configs in RunDialog - Set "Custom for this file" as the config name if default parameters are loaded. That way users won't have to think what name to set for a custom config. - If that's not the case, set the selected custom config name in the name lineedit. - Prevent saving custom configs without a name and when their parameters are exactly the same as the default ones. - Prevent removing default configs. - Fix test_run_plugin. --- spyder/plugins/run/api.py | 6 --- spyder/plugins/run/container.py | 10 +++- spyder/plugins/run/models.py | 12 +---- spyder/plugins/run/tests/test_run.py | 11 +++-- spyder/plugins/run/widgets.py | 68 +++++++++++++++++++++------- 5 files changed, 68 insertions(+), 39 deletions(-) diff --git a/spyder/plugins/run/api.py b/spyder/plugins/run/api.py index c4e3a50acc4..8b0d0c01b4a 100644 --- a/spyder/plugins/run/api.py +++ b/spyder/plugins/run/api.py @@ -10,7 +10,6 @@ from __future__ import annotations import functools -from enum import IntEnum from datetime import datetime import logging from typing import Any, Callable, Set, List, Union, Optional, Dict, TypedDict @@ -23,11 +22,6 @@ logger = logging.getLogger(__name__) -class RunParameterFlags(IntEnum): - SetDefaults = 0 - SwitchValues = 1 - - class RunActions: Run = 'run' Configure = 'configure' diff --git a/spyder/plugins/run/container.py b/spyder/plugins/run/container.py index f60772622ff..9b65e0cf50b 100644 --- a/spyder/plugins/run/container.py +++ b/spyder/plugins/run/container.py @@ -246,7 +246,10 @@ def process_run_dialog_result(self, result): if (status & RunDialogStatus.Save) == RunDialogStatus.Save: exec_uuid = ext_params['uuid'] - if exec_uuid is not None: + + # Default parameters should already be saved in our config system. + # So, there is no need to save them again here. + if exec_uuid is not None and not ext_params["default"]: info = self.metadata_model.get_metadata_context_extension(uuid) context, ext = info @@ -259,6 +262,7 @@ def process_run_dialog_result(self, result): ) exec_params = all_exec_params['params'] exec_params[exec_uuid] = ext_params + self.set_executor_configuration_parameters( executor_name, ext, @@ -996,6 +1000,10 @@ def delete_executor_configuration_parameters( for params_id in ext_ctx_params: if params_id == uuid: + # Prevent to remove default parameters + if ext_ctx_params[params_id]["default"]: + return + ext_ctx_params.pop(params_id, None) break diff --git a/spyder/plugins/run/models.py b/spyder/plugins/run/models.py index ed8f9f17c6c..5fe84cdb55b 100644 --- a/spyder/plugins/run/models.py +++ b/spyder/plugins/run/models.py @@ -20,8 +20,6 @@ ExtendedRunExecutionParameters, RunConfigurationMetadata, RunContext, - RunExecutionParameters, - RunParameterFlags, StoredRunConfigurationExecutor, StoredRunExecutorParameters, SupportedExecutionRunConfiguration, @@ -298,15 +296,9 @@ def data(self, index: QModelIndex, role: int = Qt.DisplayRole): def rowCount(self, parent: QModelIndex = ...) -> int: return len(self.executor_conf_params) - def get_executor_parameters( - self, - index: int - ) -> Tuple[RunParameterFlags, RunExecutionParameters]: - + def get_parameters(self, index: int) -> ExtendedRunExecutionParameters: params_id = self.params_index[index] - params = self.executor_conf_params[params_id] - actual_params = params['params'] - return RunParameterFlags.SwitchValues, actual_params + return self.executor_conf_params[params_id] def get_parameters_uuid_name( self, index: int diff --git a/spyder/plugins/run/tests/test_run.py b/spyder/plugins/run/tests/test_run.py index 61fa5ecced3..2a2674fd708 100644 --- a/spyder/plugins/run/tests/test_run.py +++ b/spyder/plugins/run/tests/test_run.py @@ -599,7 +599,7 @@ def test_run_plugin(qtbot, run_mock): # Ensure that the executor run configuration was saved assert exec_conf['uuid'] is not None - assert exec_conf['name'] == "Custom" + assert exec_conf['name'] == "Custom for this file" # Check that the configuration parameters are the ones defined by the # dialog @@ -742,6 +742,7 @@ def test_run_plugin(qtbot, run_mock): # Switch to other run configuration and register a set of custom run # executor parameters exec_provider_2.switch_focus('ext3', 'AnotherSuperContext') + dialog.exec_() # Spawn the configuration dialog run_act = run.get_action(RunActions.Run) @@ -773,7 +774,7 @@ def test_run_plugin(qtbot, run_mock): saved_params = run.get_executor_configuration_parameters( executor_name, 'ext3', RunContext.AnotherSuperContext) executor_params = saved_params['params'] - assert len(executor_params) == 1 + assert len(executor_params) == 2 # Check that the configuration passed to the executor is the same that was # saved. @@ -805,14 +806,14 @@ def test_run_plugin(qtbot, run_mock): new_ext_ctx_params = ( new_exec_params[('ext3', RunContext.AnotherSuperContext)]['params'] ) - assert new_ext_ctx_params == {} + assert len(new_ext_ctx_params) == 1 # Check that adding new parameters preserves the previous ones current_exec_params = container.get_conf('parameters')[executor_name] assert ( len(current_exec_params[('ext1', RunContext.RegisteredContext)] ['params'] - ) == 1 + ) == 2 ) # Check that we have one config in this context new_exec_conf_uuid = str(uuid4()) @@ -838,7 +839,7 @@ def test_run_plugin(qtbot, run_mock): assert ( len( new_exec_params[('ext1', RunContext.RegisteredContext)]['params'] - ) == 2 + ) == 3 ) # Now we should have two configs in the same context # Test teardown functions diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index eb55b35ecea..e33990a9db3 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -16,6 +16,7 @@ from qtpy.QtCore import QSize, Qt, Signal from qtpy.QtGui import QFontMetrics from qtpy.QtWidgets import ( + QAction, QApplication, QDialog, QDialogButtonBox, @@ -550,6 +551,21 @@ def setup(self): name_params_label = QLabel(_("Name:")) self.name_params_text = QLineEdit(self) + # This action needs to be added before setting an icon for it so that + # it doesn't show up in the line edit (despite being set as not visible + # below). That's probably a Qt bug. + status_action = QAction(self) + self.name_params_text.addAction( + status_action, QLineEdit.TrailingPosition + ) + self.name_params_text.status_action = status_action + + status_action.setIcon(ima.icon("error")) + status_action.setToolTip( + _("You need to provide a name to save this configuration") + ) + status_action.setVisible(False) + # Buttons delete_button = QPushButton(_("Delete")) save_global_button = QPushButton(_("Save globally")) @@ -681,10 +697,22 @@ def update_parameter_set(self, index: int): if index < 0: return - action, params = self.parameter_model.get_executor_parameters(index) + # Get parameters + stored_params = self.parameter_model.get_parameters(index) + + # Set parameters name + if stored_params["default"]: + # We set this name for default paramaters so users don't have to + # think about selecting one when customizing them + self.name_params_text.setText(_("Custom for this file")) + else: + self.name_params_text.setText(stored_params["name"]) + + # Set parameters in their corresponding graphical elements + params = stored_params["params"] working_dir_params = params['working_dir'] - stored_parameters = params['executor_params'] - self.current_widget.set_configuration(stored_parameters) + exec_params = params['executor_params'] + self.current_widget.set_configuration(exec_params) source = working_dir_params['source'] path = working_dir_params['path'] @@ -833,28 +861,34 @@ def accept(self) -> None: default_conf = self.current_widget.get_default_configuration() widget_conf = self.current_widget.get_configuration() + self.name_params_text.status_action.setVisible(False) + + # Check if config is named but only when the dialog is visible + params_name = self.name_params_text.text() + if self.isVisible() and not params_name: + # Don't allow to save configs without a name + self.name_params_text.status_action.setVisible(True) - # Check if config is named - given_name = self.name_params_text.text() - if not given_name and widget_conf != default_conf: - # If parameters are not named and are different from the default - # ones, we always save them in a config named "Custom". This avoids - # the hassle of asking users to provide a name when they want to - # customize the config. - given_name = _("Custom") + # This allows to close the dialog when clicking the Cancel button + self.status = RunDialogStatus.Close + return # Get index associated with config - if given_name: - idx = self.parameter_model.get_parameters_index_by_name(given_name) + if widget_conf == default_conf: + # This avoids saving an unnecessary "Custom for this file" config + # when the parameters haven't been modified + idx = 0 else: - idx = self.parameters_combo.currentIndex() + idx = self.parameter_model.get_parameters_index_by_name( + params_name + ) # Get uuid and name from index if idx == -1: - # This means that there are no saved parameters for given_name, so + # This means that there are no saved parameters for params_name, so # we need to generate a new uuid for them. uuid = str(uuid4()) - name = given_name + name = params_name else: # Retrieve uuid and name from our config system uuid, name = self.parameter_model.get_parameters_uuid_name(idx) @@ -883,7 +917,7 @@ def accept(self) -> None: name=name, params=exec_params, file_uuid=None if self._save_as_global else metadata_info['uuid'], - default=False + default=True if (widget_conf == default_conf) else False ) executor_name, __ = self.executors_model.get_selected_run_executor( From 3375eb42da6d22a5ca27f3458628c7ccffe25f73 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 24 May 2024 10:59:27 -0500 Subject: [PATCH 32/55] Run: Improve RunDialog layout and style - Remove unnecessary widgets. - Fix inner padding because it was not uniform. - Decrease background color brightness of the header label. - Decrease vertical margin between some of its widgets. --- spyder/plugins/run/widgets.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index e33990a9db3..30e625c97d9 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -31,7 +31,6 @@ QPushButton, QStackedWidget, QVBoxLayout, - QWidget, ) import qstylizer.style @@ -636,14 +635,18 @@ def setup(self): self.header_label, self.configuration_combo, # Hidden for simplicity executor_layout, - custom_config + custom_config, + 2 * AppStyle.MarginSize, + ) + layout.setContentsMargins( + AppStyle.InnerContentPadding, + # This needs to be bigger to make the layout look better + AppStyle.InnerContentPadding + AppStyle.MarginSize, + # This makes the left and right padding be the same + AppStyle.InnerContentPadding + 4, + AppStyle.InnerContentPadding, ) - widget_dialog = QWidget(self) - widget_dialog.setMinimumWidth(600) - widget_dialog.setLayout(layout) - scroll_layout = QVBoxLayout(self) - scroll_layout.addWidget(widget_dialog) self.add_button_box(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) # --- Settings @@ -965,20 +968,21 @@ def _stylesheet(self): self._css["QLabel#run-header-label"].setValues( # Give it a background color to make it highlight over the other # widgets. - backgroundColor=SpyderPalette.COLOR_BACKGROUND_4, + backgroundColor=SpyderPalette.COLOR_BACKGROUND_2, # The left and right margins are a bit bigger to prevent the file # name from being too close to the borders in case it's too long. padding=f"{2 * AppStyle.MarginSize} {4 * AppStyle.MarginSize}", borderRadius=SpyderPalette.SIZE_BORDER_RADIUS, # Add good enough margin with the widgets below it. - marginBottom=f"{AppStyle.InnerContentPadding}px" + marginBottom=f"{3 * AppStyle.MarginSize}px", + # This is necessary to align the label to the widgets below it. + marginLeft="4px", ) # --- Style for the collapsible self._css["CollapsibleWidget"].setValues( - # Separate it from the widgets above it with the same margin as the - # one between the header and those widgets. - marginTop=f"{AppStyle.InnerContentPadding}px" + # Separate it from the widgets above it + marginTop=f"{3 * AppStyle.MarginSize}px" ) return self._css.toString() From 5e1862da066b5c9c912488ea780ac2e0d635762b Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 28 May 2024 21:06:03 -0500 Subject: [PATCH 33/55] Run: Prevent saving global configs for different conditions in RunDialog - We don't allow global configs named "Custom for this file" or with the name of an already custom config to avoid having two repeated configs. - Also, add place holder to the name_params_text to clarify its meaning. --- spyder/plugins/run/widgets.py | 45 ++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 30e625c97d9..d924784d083 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -549,6 +549,9 @@ def setup(self): # Name to save custom configuration name_params_label = QLabel(_("Name:")) self.name_params_text = QLineEdit(self) + self.name_params_text.setPlaceholderText( + _("Set a name for this configuration") + ) # This action needs to be added before setting an icon for it so that # it doesn't show up in the line edit (despite being set as not visible @@ -560,9 +563,6 @@ def setup(self): self.name_params_text.status_action = status_action status_action.setIcon(ima.icon("error")) - status_action.setToolTip( - _("You need to provide a name to save this configuration") - ) status_action.setVisible(False) # Buttons @@ -866,15 +866,38 @@ def accept(self) -> None: widget_conf = self.current_widget.get_configuration() self.name_params_text.status_action.setVisible(False) - # Check if config is named but only when the dialog is visible + # Different checks for the config name params_name = self.name_params_text.text() - if self.isVisible() and not params_name: - # Don't allow to save configs without a name - self.name_params_text.status_action.setVisible(True) - - # This allows to close the dialog when clicking the Cancel button - self.status = RunDialogStatus.Close - return + if self.isVisible(): + allow_to_close = True + + if not params_name: + # Don't allow to save configs without a name + self.name_params_text.status_action.setVisible(True) + self.name_params_text.status_action.setToolTip( + _("You need to provide a name to save this configuration") + ) + allow_to_close = False + self._save_as_global = False + elif self._save_as_global and ( + params_name == _("Custom for this file") + or params_name == self.parameters_combo.lineEdit().text() + ): + # Don't allow to save a global config named "Custom for this + # file" or with the current config name because it'll end up + # being confusing. + allow_to_close = False + self.name_params_text.status_action.setVisible(True) + self.name_params_text.status_action.setToolTip( + _("Select a different name for this configuration") + ) + self._save_as_global = False + + if not allow_to_close: + # With this the dialog can be closed when clicking the Cancel + # button + self.status = RunDialogStatus.Close + return # Get index associated with config if widget_conf == default_conf: From a1a7d7beac90d7748243e35c93b4eb04aabbb337 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 29 May 2024 12:16:59 -0500 Subject: [PATCH 34/55] Run: Add tip widgets in several places of RunDialog That will make easier for users to understand how to use it. --- spyder/plugins/run/widgets.py | 38 +++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index d924784d083..c24978d0282 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -53,6 +53,7 @@ from spyder.utils.qthelpers import create_toolbutton from spyder.utils.stylesheet import AppStyle from spyder.widgets.collapsible import CollapsibleWidget +from spyder.widgets.helperwidgets import TipWidget # Main constants @@ -521,17 +522,39 @@ def setup(self): # --- Executor and parameters widgets executor_label = QLabel(_("Run this file in:")) self.executor_combo = SpyderComboBox(self) + self.executor_combo.setMinimumWidth(250) + executor_tip = TipWidget( + _( + "This is the plugin that will be used for execution when you " + "click on the Run button" + ), + icon=ima.icon('question_tip'), + hover_icon=ima.icon('question_tip_hover'), + size=23, + wrap_text=True + ) + parameters_label = QLabel(_("Preset configuration:")) self.parameters_combo = SpyderComboBox(self) - - self.executor_combo.setMinimumWidth(250) self.parameters_combo.setMinimumWidth(250) + parameters_tip = TipWidget( + _( + "Select here between global or local (i.e. for this file) " + "execution parameters. You can set the latter below" + ), + icon=ima.icon('question_tip'), + hover_icon=ima.icon('question_tip_hover'), + size=23, + wrap_text=True + ) executor_g_layout = QGridLayout() executor_g_layout.addWidget(executor_label, 0, 0) executor_g_layout.addWidget(self.executor_combo, 0, 1) + executor_g_layout.addWidget(executor_tip, 0, 2) executor_g_layout.addWidget(parameters_label, 1, 0) executor_g_layout.addWidget(self.parameters_combo, 1, 1) + executor_g_layout.addWidget(parameters_tip, 1, 2) executor_layout = QHBoxLayout() executor_layout.addLayout(executor_g_layout) @@ -552,6 +575,16 @@ def setup(self): self.name_params_text.setPlaceholderText( _("Set a name for this configuration") ) + name_params_tip = TipWidget( + _( + "Select a name for the execution parameters you want to set. " + "They will be saved after clicking the Ok button below" + ), + icon=ima.icon('question_tip'), + hover_icon=ima.icon('question_tip_hover'), + size=23, + wrap_text=True + ) # This action needs to be added before setting an icon for it so that # it doesn't show up in the line edit (despite being set as not visible @@ -578,6 +611,7 @@ def setup(self): config_props_layout.addWidget(name_params_label, 0, 0) config_props_layout.addWidget(self.name_params_text, 0, 1) + config_props_layout.addWidget(name_params_tip, 0, 2) config_props_layout.addLayout(buttons_layout, 1, 1) # --- Runner settings From df272ecbb96efcb9bb274ef6f543eb211b815ead Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 29 May 2024 12:24:52 -0500 Subject: [PATCH 35/55] Run: Fix content's right margin of custom_config widget in RunDialog Also, only center that dialog vertically when uncollapsing custom_config. --- spyder/plugins/run/widgets.py | 10 +++++----- spyder/widgets/collapsible.py | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index c24978d0282..b2b059378fa 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -658,8 +658,9 @@ def setup(self): custom_config.addWidget(self.executor_group) custom_config.addWidget(self.wdir_group) - # Remove unnecessary margin at the bottom. + # Fix bottom and left margins. custom_config.set_content_bottom_margin(0) + custom_config.set_content_right_margin(AppStyle.MarginSize) # Center dialog after custom_config is expanded/collapsed custom_config._animation.finished.connect(self._center_dialog) @@ -1054,10 +1055,9 @@ def _center_dialog(self): main_window = getattr(QApplication.instance(), 'main_window', None) if main_window: - x = ( - main_window.pos().x() - + ((main_window.width() - self.width()) // 2) - ) + # We only center the dialog vertically because there's no need to + # do it horizontally. + x = self.x() y = ( main_window.pos().y() diff --git a/spyder/widgets/collapsible.py b/spyder/widgets/collapsible.py index 54edf64828e..2ed377421c9 100644 --- a/spyder/widgets/collapsible.py +++ b/spyder/widgets/collapsible.py @@ -58,6 +58,12 @@ def set_content_bottom_margin(self, bottom_margin): margins.setBottom(bottom_margin) self.content().layout().setContentsMargins(margins) + def set_content_right_margin(self, right_margin): + """Set right margin of the content area to `right_margin`.""" + margins = self.content().layout().contentsMargins() + margins.setRight(right_margin) + self.content().layout().setContentsMargins(margins) + def _generate_stylesheet(self): """Generate base stylesheet for this widget.""" css = qstylizer.style.StyleSheet() From 16b070c537a6a659361528c4dc1592922e97508a Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 29 May 2024 12:29:37 -0500 Subject: [PATCH 36/55] App: Filter warnings shown when uncollapsing CollapsibleWidget --- spyder/app/utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spyder/app/utils.py b/spyder/app/utils.py index c45619901a6..67f1a21384a 100644 --- a/spyder/app/utils.py +++ b/spyder/app/utils.py @@ -187,6 +187,15 @@ def qt_message_handler(msg_type, msg_log_context, msg_string): # This is shown when expanding/collpasing folders in the Files plugin # after spyder-ide/spyder# "QFont::setPixelSize: Pixel size <= 0 (0)", + # These warnings are shown uncollapsing CollapsibleWidget + "QPainter::begin: Paint device returned engine == 0, type: 2", + "QPainter::save: Painter not active", + "QPainter::setPen: Painter not active", + "QPainter::setWorldTransform: Painter not active", + "QPainter::setOpacity: Painter not active", + "QFont::setPixelSize: Pixel size <= 0 (-3)", + "QPainter::setFont: Painter not active", + "QPainter::restore: Unbalanced save/restore", ] if msg_string not in BLACKLIST: print(msg_string) # spyder: test-skip From 2d14675ec4eb97beaf74916b11ede7a3088b599f Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 31 May 2024 18:28:26 -0500 Subject: [PATCH 37/55] Run: Fix a couple of errors with its dialogs - The Reset button in RunDialog was failing. - Trying to edit some configurations in ExecutionParametersDialog was also failing. --- spyder/plugins/run/widgets.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index b2b059378fa..a0bd2ef7227 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -209,6 +209,7 @@ def setup(self): context_combo_label = QLabel(_("Select a run context:")) self.extension_combo = SpyderComboBox(self) + self.extension_combo.addItems(self.extensions) self.extension_combo.currentIndexChanged.connect( self.extension_changed) @@ -285,18 +286,21 @@ def setup(self): ) self.layout().setSizeConstraint(QLayout.SetFixedSize) - self.extension_combo.addItems(self.extensions) - extension_index = 0 if self.extension is not None: extension_index = self.extensions.index(self.extension) self.extension_combo.setEnabled(False) + self.extension_combo.setCurrentIndex(extension_index) + + # This is necessary because extension_changed is not triggered + # automatically for this extension_index. + if extension_index == 0: + self.extension_changed(extension_index) + if self.context is not None: self.context_combo.setEnabled(False) - self.extension_combo.setCurrentIndex(extension_index) - if self.parameters_name: self.store_params_text.setText(self.parameters_name) @@ -831,10 +835,7 @@ def select_executor(self, executor_name: str): self.executors_model.get_run_executor_index(executor_name)) def reset_btn_clicked(self): - self.parameters_combo.setCurrentIndex(-1) - index = self.executor_combo.currentIndex() - self.display_executor_configuration(index) - self.name_params_text.setText('') + self.parameters_combo.setCurrentIndex(0) def run_btn_clicked(self): self.status |= RunDialogStatus.Run From ebab9c6f35c72ce4d16d4e16a74fc9ee991d9055 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 31 May 2024 19:29:59 -0500 Subject: [PATCH 38/55] External terminal: Improve UI of ExternalTerminalShConfiguration widget Also, change default in ExternalTerminalPyConfiguration so that users can interact with the interpreter by default. That will easily allow them to see the execution results in the terminal. --- .../externalterminal/widgets/run_conf.py | 58 +++++++++++++------ 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/spyder/plugins/externalterminal/widgets/run_conf.py b/spyder/plugins/externalterminal/widgets/run_conf.py index cba5fd6f517..1712d3d08d0 100644 --- a/spyder/plugins/externalterminal/widgets/run_conf.py +++ b/spyder/plugins/externalterminal/widgets/run_conf.py @@ -103,7 +103,7 @@ def get_default_configuration() -> dict: return { 'args_enabled': False, 'args': '', - 'interact': False, + 'interact': True, 'python_args_enabled': False, 'python_args': '', } @@ -144,45 +144,65 @@ def __init__( # --- Interpreter --- interpreter_group = QGroupBox(_("Interpreter")) - interpreter_layout = QGridLayout(interpreter_group) + interpreter_layout = QVBoxLayout(interpreter_group) + interpreter_layout.setContentsMargins( + *((3 * AppStyle.MarginSize,) * 4) + ) interpreter_label = QLabel(_("Shell interpreter:")) - interpreter_layout.addWidget(interpreter_label, 0, 0) - edit_layout = QHBoxLayout() - self.interpreter_edit = QLineEdit() + self.interpreter_edit = QLineEdit(self) browse_btn = create_toolbutton( self, triggered=self.select_directory, icon=ima.icon('DirOpenIcon'), tip=_("Select directory") ) - edit_layout.addWidget(self.interpreter_edit) - edit_layout.addWidget(browse_btn) - interpreter_layout.addLayout(edit_layout, 0, 1) + + shell_layout = QHBoxLayout() + shell_layout.addWidget(interpreter_label) + shell_layout.addWidget(self.interpreter_edit) + shell_layout.addWidget(browse_btn) + interpreter_layout.addLayout(shell_layout) self.interpreter_opts_cb = QCheckBox(_("Interpreter arguments:")) - interpreter_layout.addWidget(self.interpreter_opts_cb, 1, 0) - self.interpreter_opts_edit = QLineEdit() + self.interpreter_opts_edit = QLineEdit(self) + self.interpreter_opts_edit.setMinimumWidth(250) self.interpreter_opts_cb.toggled.connect( - self.interpreter_opts_edit.setEnabled) + self.interpreter_opts_edit.setEnabled + ) self.interpreter_opts_edit.setEnabled(False) - interpreter_layout.addWidget(self.interpreter_opts_edit, 1, 1) + + interpreter_opts_layout = QHBoxLayout() + interpreter_opts_layout.addWidget(self.interpreter_opts_cb) + interpreter_opts_layout.addWidget(self.interpreter_opts_edit) + interpreter_layout.addLayout(interpreter_opts_layout) # --- Script --- script_group = QGroupBox(_('Script')) - script_layout = QGridLayout(script_group) + script_layout = QVBoxLayout(script_group) + script_layout.setContentsMargins( + 3 * AppStyle.MarginSize, + 3 * AppStyle.MarginSize, + 3 * AppStyle.MarginSize, + 0, + ) self.script_opts_cb = QCheckBox(_("Script arguments:")) - script_layout.addWidget(self.script_opts_cb, 1, 0) - self.script_opts_edit = QLineEdit() + self.script_opts_edit = QLineEdit(self) self.script_opts_cb.toggled.connect( - self.script_opts_edit.setEnabled) + self.script_opts_edit.setEnabled + ) self.script_opts_edit.setEnabled(False) - script_layout.addWidget(self.script_opts_edit, 1, 1) + + script_args_layout = QHBoxLayout() + script_args_layout.addWidget(self.script_opts_cb) + script_args_layout.addWidget(self.script_opts_edit) + script_layout.addLayout(script_args_layout) self.close_after_exec_cb = QCheckBox( - _('Close terminal after execution')) - script_layout.addWidget(self.close_after_exec_cb, 2, 0) + _("Close terminal after execution") + ) + script_layout.addWidget(self.close_after_exec_cb) layout = QVBoxLayout(self) layout.addWidget(interpreter_group) From 9910e1b56aaba694ed78191664e548d370b59f01 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 2 Jun 2024 18:53:47 -0500 Subject: [PATCH 39/55] Testing: Fix failing main windows tests - Also, remove unnecessary usage of generate_run_parameters. Now that we have default parameters saved to our config system, using that function is not necessary in most tests. - Change skip condition of test_connection_to_external_kernel to be able to run it locally. - Fix minor style issues. --- spyder/app/tests/test_mainwindow.py | 94 ++++------------------------ spyder/plugins/run/models.py | 2 +- spyder/plugins/run/tests/test_run.py | 11 ++-- spyder/plugins/run/widgets.py | 2 +- 4 files changed, 20 insertions(+), 89 deletions(-) diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index 45a11bb7860..ae740e2d46f 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -438,9 +438,6 @@ def test_get_help_ipython_console_dot_notation(main_window, qtbot, tmpdir): main_window.editor.load(test_file) code_editor = main_window.editor.get_focus_widget() - run_parameters = generate_run_parameters(main_window, test_file) - CONF.set('run', 'last_used_parameters', run_parameters) - # Run test file qtbot.keyClick(code_editor, Qt.Key_F5) qtbot.wait(500) @@ -483,9 +480,6 @@ def test_get_help_ipython_console_special_characters( main_window.editor.load(test_file) code_editor = main_window.editor.get_focus_widget() - run_parameters = generate_run_parameters(main_window, test_file) - CONF.set('run', 'last_used_parameters', run_parameters) - # Run test file qtbot.keyClick(code_editor, Qt.Key_F5) qtbot.wait(500) @@ -726,7 +720,8 @@ def test_runconfig_workdir(main_window, qtbot, tmpdir): exec_uuid = str(uuid.uuid4()) ext_exec_conf = ExtendedRunExecutionParameters( - uuid=exec_uuid, name='TestConf', params=exec_conf) + uuid=exec_uuid, name='TestConf', params=exec_conf, default=False + ) ipy_dict = {ipyconsole.NAME: { ('py', RunContext.File): {'params': {exec_uuid: ext_exec_conf}} @@ -809,7 +804,8 @@ def test_dedicated_consoles(main_window, qtbot): exec_uuid = str(uuid.uuid4()) ext_exec_conf = ExtendedRunExecutionParameters( - uuid=exec_uuid, name='TestConf', params=exec_conf) + uuid=exec_uuid, name='TestConf', params=exec_conf, default=False + ) ipy_dict = {ipyconsole.NAME: { ('py', RunContext.File): {'params': {exec_uuid: ext_exec_conf}} @@ -918,7 +914,8 @@ def test_shell_execution(main_window, qtbot, tmpdir): exec_uuid = str(uuid.uuid4()) ext_exec_conf = ExtendedRunExecutionParameters( - uuid=exec_uuid, name='TestConf', params=exec_conf) + uuid=exec_uuid, name='TestConf', params=exec_conf, default=False + ) ipy_dict = {external_terminal.NAME: { (ext, RunContext.File): {'params': {exec_uuid: ext_exec_conf}} @@ -946,8 +943,10 @@ def test_shell_execution(main_window, qtbot, tmpdir): @flaky(max_runs=3) -@pytest.mark.skipif(sys.platform.startswith('linux'), - reason="Fails frequently on Linux") +@pytest.mark.skipif( + sys.platform.startswith('linux') and running_in_ci(), + reason="Fails frequently on Linux" +) @pytest.mark.order(after="test_debug_unsaved_function") def test_connection_to_external_kernel(main_window, qtbot): """Test that only Spyder kernels are connected to the Variable Explorer.""" @@ -993,10 +992,6 @@ def test_connection_to_external_kernel(main_window, qtbot): "print(2 + 1)" ) - file_path = main_window.editor.get_current_filename() - run_parameters = generate_run_parameters(main_window, file_path) - CONF.set('run', 'last_used_parameters', run_parameters) - # Start running with qtbot.waitSignal(shell.executed): qtbot.mouseClick(main_window.run_button, Qt.LeftButton) @@ -1161,10 +1156,6 @@ def test_run_cython_code(main_window, qtbot): file_path = osp.join(LOCATION, 'pyx_script.pyx') main_window.editor.load(file_path) - # --- Set run options for this file --- - run_parameters = generate_run_parameters(main_window, file_path) - CONF.set('run', 'last_used_parameters', run_parameters) - # Run file qtbot.keyClick(code_editor, Qt.Key_F5) @@ -1188,10 +1179,6 @@ def test_run_cython_code(main_window, qtbot): file_path = osp.join(LOCATION, 'pyx_lib_import.py') main_window.editor.load(file_path) - # --- Set run options for this file -- - run_parameters = generate_run_parameters(main_window, file_path) - CONF.set('run', 'last_used_parameters', run_parameters) - # Run file qtbot.keyClick(code_editor, Qt.Key_F5) @@ -1436,9 +1423,6 @@ def test_run_code(main_window, qtbot, tmpdir): # Get a reference to the namespace browser widget nsb = main_window.variableexplorer.current_widget() - run_parameters = generate_run_parameters(main_window, filepath) - CONF.set('run', 'last_used_parameters', run_parameters) - # ---- Run file ---- with qtbot.waitSignal(shell.executed): qtbot.keyClick(code_editor, Qt.Key_F5) @@ -1847,9 +1831,6 @@ def test_maximize_minimize_plugins(main_window, qtbot): exclude=[Plugins.Editor, Plugins.IPythonConsole] ) qtbot.mouseClick(max_button, Qt.LeftButton) - - run_parameters = generate_run_parameters(main_window, test_file) - CONF.set('run', 'last_used_parameters', run_parameters) qtbot.mouseClick(main_window.run_button, Qt.LeftButton) assert not plugin_3.get_widget().get_maximized_state() @@ -3295,10 +3276,6 @@ def test_preferences_empty_shortcut_regression(main_window, qtbot): code_editor = main_window.editor.get_focus_widget() code_editor.set_text(u'print(0)\n#%%\nprint(ññ)') - fname = main_window.editor.get_current_filename() - run_parameters = generate_run_parameters(main_window, fname) - CONF.set('run', 'last_used_parameters', run_parameters) - with qtbot.waitSignal(shell.executed): qtbot.keyClick(code_editor, Qt.Key_Return, modifier=Qt.ShiftModifier) qtbot.waitUntil(lambda: u'print(0)' in shell._control.toPlainText()) @@ -3602,10 +3579,6 @@ def test_varexp_rename(main_window, qtbot, tmpdir): # Get a reference to the namespace browser widget nsb = main_window.variableexplorer.current_widget() - # --- Set run options for this file --- - run_parameters = generate_run_parameters(main_window, filepath) - CONF.set('run', 'last_used_parameters', run_parameters) - # ---- Run file ---- with qtbot.waitSignal(shell.executed): qtbot.mouseClick(main_window.run_button, Qt.LeftButton) @@ -3673,10 +3646,6 @@ def test_varexp_remove(main_window, qtbot, tmpdir): # Get a reference to the namespace browser widget nsb = main_window.variableexplorer.current_widget() - # --- Set run options for this file --- - run_parameters = generate_run_parameters(main_window, filepath) - CONF.set('run', 'last_used_parameters', run_parameters) - # ---- Run file ---- with qtbot.waitSignal(shell.executed, timeout=SHELL_TIMEOUT): qtbot.mouseClick(main_window.run_button, Qt.LeftButton) @@ -3757,10 +3726,6 @@ def test_runcell_edge_cases(main_window, qtbot, tmpdir): lambda: shell.spyder_kernel_ready and shell._prompt_html is not None, timeout=SHELL_TIMEOUT) - fname = main_window.editor.get_current_filename() - run_parameters = generate_run_parameters(main_window, fname) - CONF.set('run', 'last_used_parameters', run_parameters) - # call runcell with qtbot.waitSignal(shell.executed): qtbot.mouseClick(main_window.run_cell_and_advance_button, @@ -3810,10 +3775,6 @@ def test_runcell_pdb(main_window, qtbot): code_editor = main_window.editor.get_focus_widget() code_editor.set_text(code) - fname = main_window.editor.get_current_filename() - run_parameters = generate_run_parameters(main_window, fname) - CONF.set('run', 'last_used_parameters', run_parameters) - # Start debugging with qtbot.waitSignal(shell.executed, timeout=SHELL_TIMEOUT): qtbot.mouseClick(debug_button, Qt.LeftButton) @@ -3858,10 +3819,6 @@ def test_runcell_cache(main_window, qtbot, debug): code_editor = main_window.editor.get_focus_widget() code_editor.set_text(code) - fname = main_window.editor.get_current_filename() - run_parameters = generate_run_parameters(main_window, fname) - CONF.set('run', 'last_used_parameters', run_parameters) - if debug: # Start debugging with qtbot.waitSignal(shell.executed): @@ -4112,10 +4069,6 @@ def test_runcell_after_restart(main_window, qtbot): code_editor = main_window.editor.get_focus_widget() code_editor.set_text(code) - fname = main_window.editor.get_current_filename() - run_parameters = generate_run_parameters(main_window, fname) - CONF.set('run', 'last_used_parameters', run_parameters) - # Restart Kernel widget = main_window.ipyconsole.get_widget() with qtbot.waitSignal(shell.sig_prompt_ready, timeout=10000): @@ -4435,10 +4388,6 @@ def test_run_unsaved_file_multiprocessing(main_window, qtbot): code_editor.set_text(text) # This code should run even on windows - fname = main_window.editor.get_current_filename() - run_parameters = generate_run_parameters(main_window, fname) - CONF.set('run', 'last_used_parameters', run_parameters) - # Start running qtbot.mouseClick(main_window.run_button, Qt.LeftButton) @@ -5515,10 +5464,6 @@ def test_func(): timeout=SHELL_TIMEOUT) control = main_window.ipyconsole.get_widget().get_focus_widget() - fname = main_window.editor.get_current_filename() - run_parameters = generate_run_parameters(main_window, fname) - CONF.set('run', 'last_used_parameters', run_parameters) - main_window.editor.get_widget().update_run_focus_file() qtbot.wait(2000) @@ -5554,10 +5499,6 @@ def crash_func(): timeout=SHELL_TIMEOUT) control = main_window.ipyconsole.get_widget().get_focus_widget() - fname = main_window.editor.get_current_filename() - run_parameters = generate_run_parameters(main_window, fname) - CONF.set('run', 'last_used_parameters', run_parameters) - main_window.editor.get_widget().update_run_focus_file() qtbot.wait(2000) @@ -5707,10 +5648,6 @@ def test_debug_unsaved_function(main_window, qtbot): code_editor = main_window.editor.get_focus_widget() code_editor.set_text('def foo():\n print(1)') - fname = main_window.editor.get_current_filename() - run_parameters = generate_run_parameters(main_window, fname) - CONF.set('run', 'last_used_parameters', run_parameters) - main_window.editor.get_widget().update_run_focus_file() qtbot.wait(2000) @@ -5757,10 +5694,6 @@ def test_out_runfile_runcell(main_window, qtbot): code_editor = main_window.editor.get_focus_widget() code_editor.set_text(code) - fname = main_window.editor.get_current_filename() - run_parameters = generate_run_parameters(main_window, fname) - CONF.set('run', 'last_used_parameters', run_parameters) - with qtbot.waitSignal(shell.executed): qtbot.mouseClick(main_window.run_cell_button, Qt.LeftButton) @@ -5809,10 +5742,6 @@ def test_print_frames(main_window, qtbot, tmpdir, thread): debugger = main_window.debugger.get_widget() frames_browser = debugger.current_widget().results_browser - # --- Set run options for this file --- - run_parameters = generate_run_parameters(main_window, str(p)) - CONF.set('run', 'last_used_parameters', run_parameters) - # Click the run button qtbot.mouseClick(main_window.run_button, Qt.LeftButton) qtbot.wait(1000) @@ -6496,6 +6425,7 @@ def test_PYTHONPATH_in_consoles(main_window, qtbot, tmp_path, @flaky(max_runs=10) @pytest.mark.skipif(sys.platform == 'darwin', reason="Fails on Mac") +@pytest.mark.order(before='test_shell_execution') def test_clickable_ipython_tracebacks(main_window, qtbot, tmp_path): """ Test that file names in IPython console tracebacks are clickable. @@ -6526,8 +6456,6 @@ def test_clickable_ipython_tracebacks(main_window, qtbot, tmp_path): qtbot.keyClicks(code_editor, '1/0') # Run test file - run_parameters = generate_run_parameters(main_window, test_file) - CONF.set('run', 'last_used_parameters', run_parameters) qtbot.mouseClick(main_window.run_button, Qt.LeftButton) qtbot.wait(500) diff --git a/spyder/plugins/run/models.py b/spyder/plugins/run/models.py index 5fe84cdb55b..4bf62378441 100644 --- a/spyder/plugins/run/models.py +++ b/spyder/plugins/run/models.py @@ -451,7 +451,7 @@ def set_parameters( # Reorder params so that Python and IPython extensions are shown first # and second by default, respectively. ordered_params = [] - for k,v in params.items(): + for k, v in params.items(): if k[0] == "py": ordered_params.insert(0, (k, v)) elif k[0] == "ipy": diff --git a/spyder/plugins/run/tests/test_run.py b/spyder/plugins/run/tests/test_run.py index 2a2674fd708..4e23a854324 100644 --- a/spyder/plugins/run/tests/test_run.py +++ b/spyder/plugins/run/tests/test_run.py @@ -641,7 +641,7 @@ def test_run_plugin(qtbot, run_mock): # saved in the Custom config _, _, _, exec_conf = sig.args[0] params = exec_conf['params'] - assert params['executor_params']['opt1'] == False + assert params['executor_params']['opt1'] is False assert exec_conf['uuid'] == current_exec_uuid # Focus into another configuration @@ -811,9 +811,12 @@ def test_run_plugin(qtbot, run_mock): # Check that adding new parameters preserves the previous ones current_exec_params = container.get_conf('parameters')[executor_name] assert ( - len(current_exec_params[('ext1', RunContext.RegisteredContext)] - ['params'] - ) == 2 + len( + current_exec_params[("ext1", RunContext.RegisteredContext)][ + "params" + ] + ) + == 2 ) # Check that we have one config in this context new_exec_conf_uuid = str(uuid4()) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index a0bd2ef7227..70b96ac308a 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -816,7 +816,7 @@ def display_executor_configuration(self, index: int): # Only show global parameters (i.e. those with file_uuid = None) or # those that correspond to the current file. stored_params = { - k:v for (k, v) in stored_params.items() + k: v for (k, v) in stored_params.items() if v.get("file_uuid") in [None, uuid] } From ce669024670f520466621d979bb314624badeb4f Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 3 Jun 2024 12:05:19 -0500 Subject: [PATCH 40/55] Run: UI fixes for Mac and Windows Also, remove background color of header label in RunDialog, which makes it look cleaner. --- spyder/plugins/run/widgets.py | 51 ++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 70b96ac308a..f1dd54d3a9a 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -49,9 +49,8 @@ ) from spyder.utils.icon_manager import ima from spyder.utils.misc import getcwd_or_home -from spyder.utils.palette import SpyderPalette from spyder.utils.qthelpers import create_toolbutton -from spyder.utils.stylesheet import AppStyle +from spyder.utils.stylesheet import AppStyle, MAC from spyder.widgets.collapsible import CollapsibleWidget from spyder.widgets.helperwidgets import TipWidget @@ -108,6 +107,7 @@ def add_button_box(self, stdbtns): """Create dialog button box and add it to the dialog layout""" self.bbox = SpyderDialogButtonBox(stdbtns) + if not self.disable_run_btn: run_btn = self.bbox.addButton( _("Run"), QDialogButtonBox.ActionRole) @@ -116,13 +116,14 @@ def add_button_box(self, stdbtns): reset_deafults_btn = self.bbox.addButton( _('Reset'), QDialogButtonBox.ResetRole) reset_deafults_btn.clicked.connect(self.reset_btn_clicked) + + # Align this button to the text above it + reset_deafults_btn.setStyleSheet("margin-left: 5px") + self.bbox.accepted.connect(self.accept) self.bbox.rejected.connect(self.reject) - btnlayout = QHBoxLayout() - btnlayout.addStretch(1) - btnlayout.addWidget(self.bbox) - self.layout().addLayout(btnlayout) + self.layout().addWidget(self.bbox) def resizeEvent(self, event): """ @@ -205,8 +206,8 @@ def setup(self): store_params_layout.addStretch(1) # --- Extension and context widgets - ext_combo_label = QLabel(_("Select a file extension:")) - context_combo_label = QLabel(_("Select a run context:")) + ext_combo_label = QLabel(_("File extension:")) + context_combo_label = QLabel(_("Run context:")) self.extension_combo = SpyderComboBox(self) self.extension_combo.addItems(self.extensions) @@ -233,11 +234,23 @@ def setup(self): self.stack = QStackedWidget(self) self.executor_group = QGroupBox(_("Runner settings")) executor_layout = QVBoxLayout(self.executor_group) + executor_layout.setContentsMargins( + 3 * AppStyle.MarginSize, + 3 * AppStyle.MarginSize, + 3 * AppStyle.MarginSize, + 0 if MAC else AppStyle.MarginSize, + ) executor_layout.addWidget(self.stack) # --- Working directory settings self.wdir_group = QGroupBox(_("Working directory settings")) wdir_layout = QVBoxLayout(self.wdir_group) + wdir_layout.setContentsMargins( + 3 * AppStyle.MarginSize, + 3 * AppStyle.MarginSize, + 3 * AppStyle.MarginSize, + AppStyle.MarginSize if MAC else 0, + ) self.file_dir_radio = QRadioButton(FILE_DIR) wdir_layout.addWidget(self.file_dir_radio) @@ -262,18 +275,15 @@ def setup(self): fixed_dir_layout.addWidget(browse_btn) wdir_layout.addLayout(fixed_dir_layout) - # --- All settings layout - all_settings_layout = QVBoxLayout() - all_settings_layout.addWidget(self.executor_group) - all_settings_layout.addWidget(self.wdir_group) - # --- Final layout layout = self.add_widgets( store_params_layout, - 5 * AppStyle.MarginSize, + 4 * AppStyle.MarginSize, ext_context_layout, - 5 * AppStyle.MarginSize, - all_settings_layout + (3 if MAC else 4) * AppStyle.MarginSize, + self.executor_group, + self.wdir_group, + (-2 if MAC else 1) * AppStyle.MarginSize, ) layout.addStretch() layout.setContentsMargins(*((AppStyle.InnerContentPadding,) * 4)) @@ -675,7 +685,7 @@ def setup(self): self.configuration_combo, # Hidden for simplicity executor_layout, custom_config, - 2 * AppStyle.MarginSize, + (-2 if MAC else 1) * AppStyle.MarginSize, ) layout.setContentsMargins( AppStyle.InnerContentPadding, @@ -1025,13 +1035,6 @@ def showEvent(self, event): def _stylesheet(self): # --- Style for the header self._css["QLabel#run-header-label"].setValues( - # Give it a background color to make it highlight over the other - # widgets. - backgroundColor=SpyderPalette.COLOR_BACKGROUND_2, - # The left and right margins are a bit bigger to prevent the file - # name from being too close to the borders in case it's too long. - padding=f"{2 * AppStyle.MarginSize} {4 * AppStyle.MarginSize}", - borderRadius=SpyderPalette.SIZE_BORDER_RADIUS, # Add good enough margin with the widgets below it. marginBottom=f"{3 * AppStyle.MarginSize}px", # This is necessary to align the label to the widgets below it. From 36dea534cd6064628f3ee89ab40d487ec8fa836d Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 3 Jun 2024 20:11:05 -0400 Subject: [PATCH 41/55] API: Add kwarg to force rendering a SpyderMenu And use that to fix a glitch with the Run main menu on Mac. --- spyder/api/widgets/menus.py | 9 +++++++-- spyder/plugins/mainmenu/plugin.py | 7 +++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/spyder/api/widgets/menus.py b/spyder/api/widgets/menus.py index 61f0c1c6117..3ccc18edd56 100644 --- a/spyder/api/widgets/menus.py +++ b/spyder/api/widgets/menus.py @@ -295,12 +295,17 @@ def _add_missing_actions(self): self._unintroduced_actions = {} - def render(self): + def render(self, force=False): """ Create the menu prior to showing it. This takes into account sections and location of menus. + + Parameters + ---------- + force: bool, optional + Whether to force rendering the menu. """ - if self._dirty: + if self._dirty or force: self.clear() self._add_missing_actions() diff --git a/spyder/plugins/mainmenu/plugin.py b/spyder/plugins/mainmenu/plugin.py index 2fd5603657a..84148d8f84b 100644 --- a/spyder/plugins/mainmenu/plugin.py +++ b/spyder/plugins/mainmenu/plugin.py @@ -154,6 +154,13 @@ def create_application_menu( if sys.platform == 'darwin': menu.aboutToShow.connect(self._hide_options_menus) + # This is necessary because for some strange reason the + # "Configuration per file" entry disappears after showing other + # dialogs and the only way to make it visible again is by + # re-rendering the menu. + if menu_id == ApplicationMenus.Run: + menu.aboutToShow.connect(lambda: menu.render(force=True)) + if menu_id in self._ITEM_QUEUE: pending_items = self._ITEM_QUEUE.pop(menu_id) for pending in pending_items: From 4c660c29fb71992d8f7b9f3e60beab92d44f2885 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 3 Jun 2024 19:46:03 -0500 Subject: [PATCH 42/55] Run: Fix style of buttons used to select a directory Also, improve and remove code related to that in the run conf widgets of the External Terminal plugin. --- .../externalterminal/widgets/run_conf.py | 40 +++++++++---------- spyder/plugins/run/widgets.py | 36 ++++++++--------- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/spyder/plugins/externalterminal/widgets/run_conf.py b/spyder/plugins/externalterminal/widgets/run_conf.py index 1712d3d08d0..56c3f8fb5f6 100644 --- a/spyder/plugins/externalterminal/widgets/run_conf.py +++ b/spyder/plugins/externalterminal/widgets/run_conf.py @@ -10,10 +10,19 @@ import os.path as osp # Third-party imports -from qtpy.compat import getexistingdirectory, getopenfilename +from qtpy.compat import getopenfilename +from qtpy.QtCore import QSize from qtpy.QtWidgets import ( - QWidget, QGroupBox, QVBoxLayout, QGridLayout, QCheckBox, QLineEdit, - QHBoxLayout, QLabel) + QCheckBox, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QVBoxLayout, + QWidget, +) # Local imports from spyder.api.translations import _ @@ -23,7 +32,6 @@ RunExecutorConfigurationGroupFactory) from spyder.utils.icon_manager import ima from spyder.utils.misc import getcwd_or_home -from spyder.utils.qthelpers import create_toolbutton from spyder.utils.stylesheet import AppStyle @@ -88,16 +96,6 @@ def __init__(self, parent, context: Context, input_extension: str, layout.addWidget(common_group) layout.addStretch(100) - def select_directory(self): - """Select directory""" - basedir = str(self.wd_edit.text()) - if not osp.isdir(basedir): - basedir = getcwd_or_home() - directory = getexistingdirectory(self, _("Select directory"), basedir) - if directory: - self.wd_edit.setText(directory) - self.dir = directory - @staticmethod def get_default_configuration() -> dict: return { @@ -151,11 +149,11 @@ def __init__( interpreter_label = QLabel(_("Shell interpreter:")) self.interpreter_edit = QLineEdit(self) - browse_btn = create_toolbutton( - self, - triggered=self.select_directory, - icon=ima.icon('DirOpenIcon'), - tip=_("Select directory") + browse_btn = QPushButton(ima.icon('DirOpenIcon'), '', self) + browse_btn.setToolTip(_("Select interpreter")) + browse_btn.clicked.connect(self.select_interpreter) + browse_btn.setIconSize( + QSize(AppStyle.ConfigPageIconSize, AppStyle.ConfigPageIconSize) ) shell_layout = QHBoxLayout() @@ -209,8 +207,8 @@ def __init__( layout.addWidget(script_group) layout.addStretch(100) - def select_directory(self): - """Select directory""" + def select_interpreter(self): + """Select an interpreter.""" basedir = str(self.interpreter_edit.text()) if not osp.isdir(basedir): basedir = getcwd_or_home() diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index f1dd54d3a9a..c7d0f9b2924 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -49,7 +49,6 @@ ) from spyder.utils.icon_manager import ima from spyder.utils.misc import getcwd_or_home -from spyder.utils.qthelpers import create_toolbutton from spyder.utils.stylesheet import AppStyle, MAC from spyder.widgets.collapsible import CollapsibleWidget from spyder.widgets.helperwidgets import TipWidget @@ -258,20 +257,20 @@ def setup(self): self.cwd_radio = QRadioButton(CW_DIR) wdir_layout.addWidget(self.cwd_radio) - fixed_dir_layout = QHBoxLayout() self.fixed_dir_radio = QRadioButton(FIXED_DIR) - fixed_dir_layout.addWidget(self.fixed_dir_radio) - self.wd_edit = QLineEdit(self) self.fixed_dir_radio.toggled.connect(self.wd_edit.setEnabled) self.wd_edit.setEnabled(False) - fixed_dir_layout.addWidget(self.wd_edit) - browse_btn = create_toolbutton( - self, - triggered=self.select_directory, - icon=ima.icon('DirOpenIcon'), - tip=_("Select directory") + browse_btn = QPushButton(ima.icon('DirOpenIcon'), '', self) + browse_btn.setToolTip(_("Select directory")) + browse_btn.clicked.connect(self.select_directory) + browse_btn.setIconSize( + QSize(AppStyle.ConfigPageIconSize, AppStyle.ConfigPageIconSize) ) + + fixed_dir_layout = QHBoxLayout() + fixed_dir_layout.addWidget(self.fixed_dir_radio) + fixed_dir_layout.addWidget(self.wd_edit) fixed_dir_layout.addWidget(browse_btn) wdir_layout.addLayout(fixed_dir_layout) @@ -650,19 +649,20 @@ def setup(self): self.cwd_radio = QRadioButton(CW_DIR) wdir_layout.addWidget(self.cwd_radio) - fixed_dir_layout = QHBoxLayout() self.fixed_dir_radio = QRadioButton(FIXED_DIR) - fixed_dir_layout.addWidget(self.fixed_dir_radio) self.wd_edit = QLineEdit(self) self.fixed_dir_radio.toggled.connect(self.wd_edit.setEnabled) self.wd_edit.setEnabled(False) - fixed_dir_layout.addWidget(self.wd_edit) - browse_btn = create_toolbutton( - self, - triggered=self.select_directory, - icon=ima.icon('DirOpenIcon'), - tip=_("Select directory") + browse_btn = QPushButton(ima.icon('DirOpenIcon'), '', self) + browse_btn.setToolTip(_("Select directory")) + browse_btn.clicked.connect(self.select_directory) + browse_btn.setIconSize( + QSize(AppStyle.ConfigPageIconSize, AppStyle.ConfigPageIconSize) ) + + fixed_dir_layout = QHBoxLayout() + fixed_dir_layout.addWidget(self.fixed_dir_radio) + fixed_dir_layout.addWidget(self.wd_edit) fixed_dir_layout.addWidget(browse_btn) wdir_layout.addLayout(fixed_dir_layout) From ffc08a55a246a17578a412cb8216a7fc50664c7e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 7 Jun 2024 19:26:17 -0500 Subject: [PATCH 43/55] Run: Remove "Save globally" button and move "Delete" one to button box - This helps to simplify the dialog and avoid some serious UX issues found in review. - Also, enable the delete button only for file configurations and fix tests that were affected by this change. --- spyder/app/tests/test_mainwindow.py | 18 ++++++++-- spyder/plugins/run/widgets.py | 54 +++++++---------------------- 2 files changed, 28 insertions(+), 44 deletions(-) diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index ae740e2d46f..b6fa914e01c 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -720,7 +720,11 @@ def test_runconfig_workdir(main_window, qtbot, tmpdir): exec_uuid = str(uuid.uuid4()) ext_exec_conf = ExtendedRunExecutionParameters( - uuid=exec_uuid, name='TestConf', params=exec_conf, default=False + uuid=exec_uuid, + name="TestConf", + params=exec_conf, + default=False, + file_uuid=None, ) ipy_dict = {ipyconsole.NAME: { @@ -804,7 +808,11 @@ def test_dedicated_consoles(main_window, qtbot): exec_uuid = str(uuid.uuid4()) ext_exec_conf = ExtendedRunExecutionParameters( - uuid=exec_uuid, name='TestConf', params=exec_conf, default=False + uuid=exec_uuid, + name="TestConf", + params=exec_conf, + default=False, + file_uuid=None, ) ipy_dict = {ipyconsole.NAME: { @@ -914,7 +922,11 @@ def test_shell_execution(main_window, qtbot, tmpdir): exec_uuid = str(uuid.uuid4()) ext_exec_conf = ExtendedRunExecutionParameters( - uuid=exec_uuid, name='TestConf', params=exec_conf, default=False + uuid=exec_uuid, + name="TestConf", + params=exec_conf, + default=False, + file_uuid=None, ) ipy_dict = {external_terminal.NAME: { diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index c7d0f9b2924..5cfb1cd0d87 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -106,7 +106,6 @@ def add_button_box(self, stdbtns): """Create dialog button box and add it to the dialog layout""" self.bbox = SpyderDialogButtonBox(stdbtns) - if not self.disable_run_btn: run_btn = self.bbox.addButton( _("Run"), QDialogButtonBox.ActionRole) @@ -518,7 +517,6 @@ def __init__( self.current_widget = None self.status = RunDialogStatus.Close self._is_shown = False - self._save_as_global = False # ---- Public methods # ------------------------------------------------------------------------- @@ -611,21 +609,9 @@ def setup(self): status_action.setIcon(ima.icon("error")) status_action.setVisible(False) - # Buttons - delete_button = QPushButton(_("Delete")) - save_global_button = QPushButton(_("Save globally")) - delete_button.clicked.connect(self.delete_btn_clicked) - save_global_button.clicked.connect(self.save_global_btn_clicked) - - # Buttons layout - buttons_layout = QHBoxLayout() - buttons_layout.addWidget(delete_button) - buttons_layout.addWidget(save_global_button) - config_props_layout.addWidget(name_params_label, 0, 0) config_props_layout.addWidget(self.name_params_text, 0, 1) config_props_layout.addWidget(name_params_tip, 0, 2) - config_props_layout.addLayout(buttons_layout, 1, 1) # --- Runner settings self.stack = QStackedWidget() @@ -697,6 +683,9 @@ def setup(self): ) self.add_button_box(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.delete_button = QPushButton(_("Delete")) + self.delete_button.clicked.connect(self.delete_btn_clicked) + self.bbox.addButton(self.delete_button, QDialogButtonBox.ActionRole) # --- Settings self.executor_combo.currentIndexChanged.connect( @@ -760,6 +749,12 @@ def update_parameter_set(self, index: int): else: self.name_params_text.setText(stored_params["name"]) + # Disable delete button for default or global configs + if stored_params["default"] or stored_params["file_uuid"] is None: + self.delete_button.setEnabled(False) + else: + self.delete_button.setEnabled(True) + # Set parameters in their corresponding graphical elements params = stored_params["params"] working_dir_params = params['working_dir'] @@ -883,20 +878,6 @@ def delete_btn_clicked(self): # updating its contents after this config is deleted self.reject() - def save_global_btn_clicked(self): - answer = QMessageBox.question( - self, - _("Save globally"), - _( - "Do you want to save the current configuration for other " - "files?" - ), - ) - - if answer == QMessageBox.Yes: - self._save_as_global = True - self.accept() - def get_configuration( self ) -> Tuple[str, str, ExtendedRunExecutionParameters, bool]: @@ -924,20 +905,14 @@ def accept(self) -> None: _("You need to provide a name to save this configuration") ) allow_to_close = False - self._save_as_global = False - elif self._save_as_global and ( - params_name == _("Custom for this file") - or params_name == self.parameters_combo.lineEdit().text() - ): - # Don't allow to save a global config named "Custom for this - # file" or with the current config name because it'll end up - # being confusing. + elif params_name == self.parameters_combo.lineEdit().text(): + # Don't allow to save a config named with the current config + # name because it'd end up being confusing. allow_to_close = False self.name_params_text.status_action.setVisible(True) self.name_params_text.status_action.setToolTip( _("Select a different name for this configuration") ) - self._save_as_global = False if not allow_to_close: # With this the dialog can be closed when clicking the Cancel @@ -988,7 +963,7 @@ def accept(self) -> None: uuid=uuid, name=name, params=exec_params, - file_uuid=None if self._save_as_global else metadata_info['uuid'], + file_uuid=metadata_info['uuid'], default=True if (widget_conf == default_conf) else False ) @@ -999,9 +974,6 @@ def accept(self) -> None: self.saved_conf = (metadata_info['uuid'], executor_name, ext_exec_params) - # Reset attribute for next time - self._save_as_global = False - return super().accept() def showEvent(self, event): From 9e87ea536b9928d3c2271a47c6428eb0bd87a520 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 7 Jun 2024 19:44:42 -0500 Subject: [PATCH 44/55] Run: Don't allow to save file parameters with a repeated name --- spyder/plugins/run/models.py | 8 ++++++++ spyder/plugins/run/widgets.py | 14 ++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/spyder/plugins/run/models.py b/spyder/plugins/run/models.py index 4bf62378441..1015e0c1493 100644 --- a/spyder/plugins/run/models.py +++ b/spyder/plugins/run/models.py @@ -334,6 +334,14 @@ def get_parameters_index_by_name(self, parameters_name: str) -> int: return index + def get_parameter_names(self) -> List[str]: + """Get all parameter names for this executor.""" + names = [] + for params in self.executor_conf_params.values(): + names.append(params["name"]) + + return names + def __len__(self) -> int: return len(self.executor_conf_params) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 5cfb1cd0d87..e8ac2c86802 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -905,13 +905,19 @@ def accept(self) -> None: _("You need to provide a name to save this configuration") ) allow_to_close = False - elif params_name == self.parameters_combo.lineEdit().text(): - # Don't allow to save a config named with the current config - # name because it'd end up being confusing. + elif ( + params_name != self.parameters_combo.lineEdit().text() + and params_name in self.parameter_model.get_parameter_names() + ): + # Don't allow to save a config with the same name of an + # existing one because it doesn't make sense. allow_to_close = False self.name_params_text.status_action.setVisible(True) self.name_params_text.status_action.setToolTip( - _("Select a different name for this configuration") + _( + "You need to select a different name for this " + "configuration" + ) ) if not allow_to_close: From a6eb582ebc459e4937ace6740433ec652368191c Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 7 Jun 2024 23:18:06 -0500 Subject: [PATCH 45/55] Run: Improve validation of global configuration names - Show error status icon when name is empty or already taken. - Disable changing the config name if it's a saved one. - Show the same messages in both ExecutionParametersDialog and RunDialog. --- spyder/plugins/run/confpage.py | 1 + spyder/plugins/run/models.py | 11 ++++++ spyder/plugins/run/widgets.py | 66 +++++++++++++++++++++++++--------- 3 files changed, 61 insertions(+), 17 deletions(-) diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index 9f271c7b1be..d9cbdf1a329 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -114,6 +114,7 @@ def show_editor(self, new=False, clone=False): self, plugin_name, executor_params, + self.model().get_parameter_names(), extensions, contexts, params, diff --git a/spyder/plugins/run/models.py b/spyder/plugins/run/models.py index 1015e0c1493..245a55df80c 100644 --- a/spyder/plugins/run/models.py +++ b/spyder/plugins/run/models.py @@ -534,6 +534,17 @@ def apply_changes( self.endResetModel() self.sig_data_changed.emit() + def get_parameter_names(self) -> Dict[Tuple[str, str], List[str]]: + """Get all parameter names per extension and context.""" + names = {} + for k, v in self.executor_conf_params.items(): + extension_context = (k[0], k[1]) + current_names = names.get(extension_context, []) + current_names.append(_("Default") if v["default"] else v["name"]) + names[extension_context] = current_names + + return names + def __len__(self): return len(self.inverse_index) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index e8ac2c86802..1a22987f32a 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -58,6 +58,9 @@ FILE_DIR = _("The directory of the configuration being executed") CW_DIR = _("The current working directory") FIXED_DIR = _("The following directory:") +EMPTY_NAME = _("Provide a name for this configuration") +REPEATED_NAME = _("Select a different name for this configuration") + class RunDialogStatus: Close = 0 @@ -155,7 +158,10 @@ def __init__( self, parent, executor_name, - executor_params: Dict[Tuple[str, str], SupportedExecutionRunConfiguration], + executor_params: Dict[ + Tuple[str, str], SupportedExecutionRunConfiguration + ], + param_names: Dict[Tuple[str, str], List[str]], extensions: Optional[List[str]] = None, contexts: Optional[Dict[str, List[str]]] = None, default_params: Optional[ExtendedRunExecutionParameters] = None, @@ -167,6 +173,7 @@ def __init__( self.executor_name = executor_name self.executor_params = executor_params + self.param_names = param_names self.default_params = default_params self.extensions = extensions or [] self.contexts = contexts or {} @@ -203,6 +210,26 @@ def setup(self): store_params_layout.addWidget(self.store_params_text) store_params_layout.addStretch(1) + # This action needs to be added before setting an icon for it so that + # it doesn't show up in the line edit (despite being set as not visible + # below). That's probably a Qt bug. + status_action = QAction(self) + self.store_params_text.addAction( + status_action, QLineEdit.TrailingPosition + ) + self.store_params_text.status_action = status_action + + status_action.setIcon(ima.icon("error")) + status_action.setVisible(False) + + # This is necessary to fix the style of the tooltip shown inside the + # lineedit + store_params_css = qstylizer.style.StyleSheet() + store_params_css["QLineEdit QToolTip"].setValues( + padding="1px 2px", + ) + self.store_params_text.setStyleSheet(store_params_css.toString()) + # --- Extension and context widgets ext_combo_label = QLabel(_("File extension:")) context_combo_label = QLabel(_("Run context:")) @@ -312,8 +339,8 @@ def setup(self): if self.parameters_name: self.store_params_text.setText(self.parameters_name) - # Don't allow to change name if params are default ones. - if self.default_params["default"]: + # Don't allow to change name for default or already saved params. + if self.default_params["default"] or not self.new_config: self.store_params_text.setEnabled(False) # --- Stylesheet @@ -441,6 +468,7 @@ def get_configuration( def accept(self) -> None: self.status |= RunDialogStatus.Save widget_conf = self.current_widget.get_configuration() + self.store_params_text.status_action.setVisible(False) path = None source = None @@ -462,12 +490,23 @@ def accept(self) -> None: else: uuid = str(uuid4()) + # Validate name only for new configurations name = self.store_params_text.text() - if name == '': - self.store_params_text.setPlaceholderText( - _("Set a name here to proceed!") - ) - return + if self.new_config: + if name == '': + self.store_params_text.status_action.setVisible(True) + self.store_params_text.status_action.setToolTip(EMPTY_NAME) + return + else: + extension = self.extension_combo.lineEdit().text() + context = self.context_combo.lineEdit().text() + current_names = self.param_names[(extension, context)] + if name in current_names: + self.store_params_text.status_action.setVisible(True) + self.store_params_text.status_action.setToolTip( + REPEATED_NAME + ) + return ext_exec_params = ExtendedRunExecutionParameters( uuid=uuid, @@ -901,9 +940,7 @@ def accept(self) -> None: if not params_name: # Don't allow to save configs without a name self.name_params_text.status_action.setVisible(True) - self.name_params_text.status_action.setToolTip( - _("You need to provide a name to save this configuration") - ) + self.name_params_text.status_action.setToolTip(EMPTY_NAME) allow_to_close = False elif ( params_name != self.parameters_combo.lineEdit().text() @@ -913,12 +950,7 @@ def accept(self) -> None: # existing one because it doesn't make sense. allow_to_close = False self.name_params_text.status_action.setVisible(True) - self.name_params_text.status_action.setToolTip( - _( - "You need to select a different name for this " - "configuration" - ) - ) + self.name_params_text.status_action.setToolTip(REPEATED_NAME) if not allow_to_close: # With this the dialog can be closed when clicking the Cancel From 06e35bd439c460253bd73bda106935549ff3a10e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 8 Jun 2024 11:20:04 -0500 Subject: [PATCH 46/55] Run: Fix disabling edit/delete/clone buttons after some operations --- spyder/plugins/run/confpage.py | 52 ++++++++++++++++++---------------- spyder/plugins/run/models.py | 4 ++- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index d9cbdf1a329..d04525e5a5d 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -47,6 +47,7 @@ def move_file_to_front(contexts: List[str]) -> List[str]: class RunParametersTableView(HoverRowsTableView): + def __init__(self, parent, model): super().__init__(parent) self._parent = parent @@ -78,7 +79,7 @@ def selection(self, index): params = self._parent.table_model.executor_conf_params[params_id] is_default = True if params.get("default") else False - self._parent.set_clone_delete_btn_status(is_default=is_default) + self._parent.set_buttons_status(is_default=is_default) # Always enable edit button self._parent.edit_configuration_btn.setEnabled(True) @@ -194,7 +195,7 @@ def setup_page(self): self, self.plugin_container.executor_model) self.table_model = ExecutorRunParametersTableModel(self) self.table_model.sig_data_changed.connect( - lambda: self.set_modified(True) + self.on_table_data_changed ) self.all_executor_model: Dict[ @@ -241,18 +242,15 @@ def setup_page(self): self.edit_configuration_btn = QPushButton(_("Edit selected")) self.clone_configuration_btn = QPushButton(_("Clone selected")) self.delete_configuration_btn = QPushButton(_("Delete selected")) - self.reset_changes_btn = QPushButton(_("Reset changes")) + self.reset_changes_btn = QPushButton(_("Reset current changes")) self.edit_configuration_btn.setEnabled(False) self.delete_configuration_btn.setEnabled(False) self.clone_configuration_btn.setEnabled(False) self.new_configuration_btn.clicked.connect( self.create_new_configuration) - self.edit_configuration_btn.clicked.connect( - lambda checked: self.params_table.show_editor() - ) - self.clone_configuration_btn.clicked.connect( - self.clone_configuration) + self.edit_configuration_btn.clicked.connect(self.edit_configuration) + self.clone_configuration_btn.clicked.connect(self.clone_configuration) self.delete_configuration_btn.clicked.connect( self.delete_configuration) self.reset_changes_btn.clicked.connect(self.reset_changes) @@ -339,29 +337,32 @@ def executor_index_changed(self, index: int): self.table_model.set_parameters(executor_conf_params) self.previous_executor_index = index - self.set_clone_delete_btn_status() + self.set_buttons_status() - # Repopulating the params table removes any selection, so we need to - # disable the edit button. - if hasattr(self, "edit_configuration_btn"): - self.edit_configuration_btn.setEnabled(False) + def on_table_data_changed(self): + # Buttons need to be disabled because the table model is reset when + # data is changed and focus is lost + self.set_buttons_status(False) + self.set_modified(True) - def set_clone_delete_btn_status(self, is_default=False): + def set_buttons_status(self, status=None, is_default=False): # We need to enclose the code below in a try/except because these # buttons might not be created yet, which gives an AttributeError. try: - if is_default: - # Don't allow to delete default configurations, only to clone - # them - self.delete_configuration_btn.setEnabled(False) - self.clone_configuration_btn.setEnabled(True) - else: + if status is None: status = ( self.table_model.rowCount() != 0 and self.params_table.currentIndex().isValid() ) + + # Don't allow to delete default configurations + if is_default: + self.delete_configuration_btn.setEnabled(False) + else: self.delete_configuration_btn.setEnabled(status) - self.clone_configuration_btn.setEnabled(status) + + self.edit_configuration_btn.setEnabled(status) + self.clone_configuration_btn.setEnabled(status) except AttributeError: pass @@ -408,9 +409,11 @@ def get_executor_configurations(self) -> Dict[ def create_new_configuration(self): self.params_table.show_editor(new=True) + def edit_configuration(self): + self.params_table.show_editor() + def clone_configuration(self): self.params_table.clone_configuration() - self.edit_configuration_btn.setEnabled(False) def delete_configuration(self): executor_name, _ = self.executor_model.selected_executor( @@ -429,8 +432,7 @@ def delete_configuration(self): self.table_model.set_parameters(executor_params) self.table_model.reset_model() self.set_modified(True) - self.set_clone_delete_btn_status() - self.edit_configuration_btn.setEnabled(False) + self.set_buttons_status() def reset_changes(self): """Reset changes to the parameters loaded when the page was created.""" @@ -442,7 +444,7 @@ def reset_changes(self): self.table_model.set_parameters(executor_params) self.table_model.reset_model() self.set_modified(True) - self.set_clone_delete_btn_status() + self.set_buttons_status() def apply_settings(self): prev_executor_info = self.table_model.get_current_view() diff --git a/spyder/plugins/run/models.py b/spyder/plugins/run/models.py index 245a55df80c..8953d5ea803 100644 --- a/spyder/plugins/run/models.py +++ b/spyder/plugins/run/models.py @@ -531,7 +531,9 @@ def apply_changes( self.params_index = dict(enumerate(self.executor_conf_params)) self.inverse_index = {v: k for k, v in self.params_index.items()} + self.endResetModel() + self.sig_data_changed.emit() def get_parameter_names(self) -> Dict[Tuple[str, str], List[str]]: @@ -557,5 +559,5 @@ def __getitem__( ) -> Tuple[str, str, ExtendedRunExecutionParameters]: tuple_index = self.params_index[index] - (ext, context, _) = tuple_index + (ext, context, __) = tuple_index return (ext, context, self.executor_conf_params[tuple_index]) From dcc838a31a8caad6db39b6a6e31aac565793cc6e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 8 Jun 2024 12:21:21 -0500 Subject: [PATCH 47/55] Run: Simplify layout of buttons in its confpage - Show current text on their tooltips and use icons instead. - That allows us to organize the buttons in a horizontal layout, which is more compact and lean. - For this it was useful to extend the create_button method of SpyderConfigPage to simplify the creation of buttons in confpages. - Also, fix extra padding in tooltips of SidebarDialog. --- spyder/plugins/editor/confpage.py | 7 ++- spyder/plugins/run/confpage.py | 92 ++++++++++++++++++------------- spyder/widgets/config.py | 31 +++++++++-- spyder/widgets/sidebardialog.py | 5 ++ 4 files changed, 90 insertions(+), 45 deletions(-) diff --git a/spyder/plugins/editor/confpage.py b/spyder/plugins/editor/confpage.py index 387d932dece..01e6ee15841 100644 --- a/spyder/plugins/editor/confpage.py +++ b/spyder/plugins/editor/confpage.py @@ -245,8 +245,11 @@ 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) + template_btn = self.create_button( + text=_("Edit template for new files"), + callback=self.plugin.edit_template, + set_modified_on_click=True + ) templates_layout = QVBoxLayout() templates_layout.addSpacing(3) diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index d04525e5a5d..47dd406b9f0 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -16,11 +16,9 @@ from qtpy.QtCore import Qt from qtpy.QtWidgets import ( QAbstractItemView, - QGridLayout, QHBoxLayout, QHeaderView, QLabel, - QPushButton, QVBoxLayout, QWidget, ) @@ -36,6 +34,7 @@ RunExecutorNamesListModel, ExecutorRunParametersTableModel) from spyder.plugins.run.widgets import ( ExecutionParametersDialog, RunDialogStatus) +from spyder.utils.icon_manager import ima from spyder.utils.stylesheet import AppStyle from spyder.widgets.helperwidgets import HoverRowsTableView @@ -190,6 +189,7 @@ class RunConfigPage(PluginConfigPage): def setup_page(self): self._params_to_delete = {} + # --- Executors tab --- self.plugin_container: RunContainer = self.plugin.get_container() self.executor_model = RunExecutorNamesListModel( self, self.plugin_container.executor_model) @@ -238,24 +238,40 @@ def setup_page(self): executor_layout.addWidget(self.executor_combo) executor_layout.addStretch() - self.new_configuration_btn = QPushButton(_("New parameters")) - self.edit_configuration_btn = QPushButton(_("Edit selected")) - self.clone_configuration_btn = QPushButton(_("Clone selected")) - self.delete_configuration_btn = QPushButton(_("Delete selected")) - self.reset_changes_btn = QPushButton(_("Reset current changes")) - self.edit_configuration_btn.setEnabled(False) - self.delete_configuration_btn.setEnabled(False) - self.clone_configuration_btn.setEnabled(False) - - self.new_configuration_btn.clicked.connect( - self.create_new_configuration) - self.edit_configuration_btn.clicked.connect(self.edit_configuration) - self.clone_configuration_btn.clicked.connect(self.clone_configuration) - self.delete_configuration_btn.clicked.connect( - self.delete_configuration) - self.reset_changes_btn.clicked.connect(self.reset_changes) + # Buttons + self.new_configuration_btn = self.create_button( + icon=ima.icon("edit_add"), + callback=self.create_new_configuration, + tooltip=_("New parameters"), + ) + self.edit_configuration_btn = self.create_button( + icon=ima.icon("edit"), + callback=self.edit_configuration, + tooltip=_("Edit selected"), + ) + self.clone_configuration_btn = self.create_button( + icon=ima.icon("editcopy"), + callback=self.clone_configuration, + tooltip=_("Clone selected"), + ) + self.delete_configuration_btn = self.create_button( + icon=ima.icon("editclear"), + callback=self.delete_configuration, + tooltip=_("Delete selected"), + ) + self.reset_changes_btn = self.create_button( + icon=ima.icon("restart"), + callback=self.reset_changes, + tooltip=_("Reset current changes"), + ) + + # Disable edition button at startup + self.set_buttons_status(status=False) # Buttons layout + buttons_layout = QHBoxLayout() + buttons_layout.addStretch() + btns = [ self.new_configuration_btn, self.edit_configuration_btn, @@ -263,12 +279,24 @@ def setup_page(self): self.clone_configuration_btn, self.reset_changes_btn, ] - sn_buttons_layout = QGridLayout() - for i, btn in enumerate(btns): - sn_buttons_layout.addWidget(btn, i, 1) - sn_buttons_layout.setColumnStretch(0, 1) - sn_buttons_layout.setColumnStretch(1, 2) - sn_buttons_layout.setColumnStretch(2, 1) + for btn in btns: + buttons_layout.addWidget(btn) + + buttons_layout.addStretch() + + # Final layout + vlayout = QVBoxLayout() + vlayout.addWidget(about_label) + vlayout.addSpacing(3 * AppStyle.MarginSize) + vlayout.addLayout(executor_layout) + vlayout.addSpacing(3 * AppStyle.MarginSize) + vlayout.addWidget(params_label) + vlayout.addLayout(params_table_layout) + vlayout.addSpacing(AppStyle.MarginSize) + vlayout.addLayout(buttons_layout) + vlayout.addStretch() + executor_widget = QWidget(self) + executor_widget.setLayout(vlayout) # --- Editor interactions tab --- newcb = self.create_checkbox @@ -280,22 +308,10 @@ def setup_page(self): run_layout = QVBoxLayout() run_layout.addWidget(saveall_box) run_layout.addWidget(run_cell_box) - run_widget = QWidget() + run_widget = QWidget(self) run_widget.setLayout(run_layout) - vlayout = QVBoxLayout() - vlayout.addWidget(about_label) - vlayout.addSpacing(3 * AppStyle.MarginSize) - vlayout.addLayout(executor_layout) - vlayout.addSpacing(3 * AppStyle.MarginSize) - vlayout.addWidget(params_label) - vlayout.addLayout(params_table_layout) - vlayout.addSpacing(2 * AppStyle.MarginSize) - vlayout.addLayout(sn_buttons_layout) - vlayout.addStretch() - executor_widget = QWidget() - executor_widget.setLayout(vlayout) - + # --- Tabs --- self.create_tab(_("Run executors"), executor_widget) self.create_tab(_("Editor interactions"), run_widget) diff --git a/spyder/widgets/config.py b/spyder/widgets/config.py index 86678dc7681..68bb6e8ca24 100644 --- a/spyder/widgets/config.py +++ b/spyder/widgets/config.py @@ -1041,12 +1041,33 @@ def create_fontgroup(self, option=None, text=None, title=None, return widget - def create_button(self, text, callback): - btn = QPushButton(text) + def create_button( + self, + callback, + text=None, + icon=None, + tooltip=None, + set_modified_on_click=False, + ): + if icon is not None: + btn = QPushButton(icon, "", parent=self) + btn.setIconSize( + QSize(AppStyle.ConfigPageIconSize, AppStyle.ConfigPageIconSize) + ) + else: + btn = QPushButton(text, parent=self) + btn.clicked.connect(callback) - btn.clicked.connect( - lambda checked=False, opt='': self.has_been_modified( - self.CONF_SECTION, opt)) + if tooltip is not None: + btn.setToolTip(tooltip) + + if set_modified_on_click: + btn.clicked.connect( + lambda checked=False, opt="": self.has_been_modified( + self.CONF_SECTION, opt + ) + ) + return btn def create_tab(self, name, widgets): diff --git a/spyder/widgets/sidebardialog.py b/spyder/widgets/sidebardialog.py index 86593cf747b..1da9af01cb8 100644 --- a/spyder/widgets/sidebardialog.py +++ b/spyder/widgets/sidebardialog.py @@ -482,6 +482,11 @@ def _main_stylesheet(self): marginBottom='15px', ) + # Substract extra padding + css["QToolTip"].setValues( + paddingRight="-2px", + ) + # Substract extra padding that comes from QLineEdit css["QLineEdit QToolTip"].setValues( padding="-2px -3px", From 5598cf19c22e61bdfe411299862b132beda6a24d Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 9 Jun 2024 12:59:33 -0500 Subject: [PATCH 48/55] Run: Fix Reset button in ExecutionParametersDialog It was doing nothing before. --- spyder/plugins/run/widgets.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 1a22987f32a..bf3cf5cb2d0 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -362,7 +362,7 @@ def extension_changed(self, index: int): context_index = contexts.index(self.context) self.context_combo.setCurrentIndex(context_index) - def context_changed(self, index: int): + def context_changed(self, index: int, reset: bool = False): if index < 0: return @@ -406,7 +406,11 @@ def context_changed(self, index: int): working_dir_params = params['working_dir'] exec_params = params - params_set = exec_params['executor_params'] or default_params + params_set = ( + default_params + if reset + else (exec_params["executor_params"] or default_params) + ) if params_set.keys() == default_params.keys(): self.current_widget.set_configuration(params_set) @@ -449,7 +453,7 @@ def select_directory(self): def reset_btn_clicked(self): index = self.context_combo.currentIndex() - self.context_changed(index) + self.context_changed(index, reset=True) def run_btn_clicked(self): self.status |= RunDialogStatus.Run From 3d85d44a77cda77e4e98b3932659e02c544f2445 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 9 Jun 2024 13:40:17 -0500 Subject: [PATCH 49/55] Run: Remove "Runner settings" groupbox to simplify its dialogs This avoids an additional level of nesting that probably doesn't make much sense to users (as suggested in review). --- .../externalterminal/widgets/run_conf.py | 20 +--------- .../ipythonconsole/widgets/run_conf.py | 13 +------ spyder/plugins/profiler/widgets/run_conf.py | 8 +--- spyder/plugins/run/widgets.py | 37 +++++-------------- 4 files changed, 14 insertions(+), 64 deletions(-) diff --git a/spyder/plugins/externalterminal/widgets/run_conf.py b/spyder/plugins/externalterminal/widgets/run_conf.py index 56c3f8fb5f6..8e3130558f2 100644 --- a/spyder/plugins/externalterminal/widgets/run_conf.py +++ b/spyder/plugins/externalterminal/widgets/run_conf.py @@ -47,9 +47,6 @@ def __init__(self, parent, context: Context, input_extension: str, # --- Interpreter --- interpreter_group = QGroupBox(_("Python interpreter")) interpreter_layout = QVBoxLayout(interpreter_group) - interpreter_layout.setContentsMargins( - *((3 * AppStyle.MarginSize,) * 4) - ) # --- System terminal --- external_group = QWidget(self) @@ -76,12 +73,6 @@ def __init__(self, parent, context: Context, input_extension: str, # --- General settings ---- common_group = QGroupBox(_("Bash/Batch script settings")) common_layout = QGridLayout(common_group) - common_layout.setContentsMargins( - 3 * AppStyle.MarginSize, - 3 * AppStyle.MarginSize, - 3 * AppStyle.MarginSize, - 0, - ) self.clo_cb = QCheckBox(_("Command line options:")) common_layout.addWidget(self.clo_cb, 0, 0) @@ -92,6 +83,7 @@ def __init__(self, parent, context: Context, input_extension: str, common_layout.addWidget(self.clo_edit, 0, 1) layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(interpreter_group) layout.addWidget(common_group) layout.addStretch(100) @@ -143,9 +135,6 @@ def __init__( # --- Interpreter --- interpreter_group = QGroupBox(_("Interpreter")) interpreter_layout = QVBoxLayout(interpreter_group) - interpreter_layout.setContentsMargins( - *((3 * AppStyle.MarginSize,) * 4) - ) interpreter_label = QLabel(_("Shell interpreter:")) self.interpreter_edit = QLineEdit(self) @@ -178,12 +167,6 @@ def __init__( # --- Script --- script_group = QGroupBox(_('Script')) script_layout = QVBoxLayout(script_group) - script_layout.setContentsMargins( - 3 * AppStyle.MarginSize, - 3 * AppStyle.MarginSize, - 3 * AppStyle.MarginSize, - 0, - ) self.script_opts_cb = QCheckBox(_("Script arguments:")) self.script_opts_edit = QLineEdit(self) @@ -203,6 +186,7 @@ def __init__( script_layout.addWidget(self.close_after_exec_cb) layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(interpreter_group) layout.addWidget(script_group) layout.addStretch(100) diff --git a/spyder/plugins/ipythonconsole/widgets/run_conf.py b/spyder/plugins/ipythonconsole/widgets/run_conf.py index 0d3060c0e37..b2d4457af5a 100644 --- a/spyder/plugins/ipythonconsole/widgets/run_conf.py +++ b/spyder/plugins/ipythonconsole/widgets/run_conf.py @@ -19,7 +19,6 @@ from spyder.plugins.run.api import ( RunExecutorConfigurationGroup, Context, RunConfigurationMetadata) from spyder.utils.misc import getcwd_or_home -from spyder.utils.stylesheet import AppStyle # Main constants @@ -42,9 +41,6 @@ def __init__(self, parent, context: Context, input_extension: str, # --- Interpreter --- interpreter_group = QGroupBox(_("Console")) interpreter_layout = QVBoxLayout(interpreter_group) - interpreter_layout.setContentsMargins( - *((3 * AppStyle.MarginSize,) * 4) - ) self.current_radio = QRadioButton(CURRENT_INTERPRETER) interpreter_layout.addWidget(self.current_radio) @@ -53,14 +49,8 @@ def __init__(self, parent, context: Context, input_extension: str, interpreter_layout.addWidget(self.dedicated_radio) # --- General settings ---- - common_group = QGroupBox(_("General settings")) + common_group = QGroupBox(_("Advanced settings")) common_layout = QVBoxLayout(common_group) - common_layout.setContentsMargins( - 3 * AppStyle.MarginSize, - 3 * AppStyle.MarginSize, - 3 * AppStyle.MarginSize, - 0, - ) self.clear_var_cb = QCheckBox(CLEAR_ALL_VARIABLES) common_layout.addWidget(self.clear_var_cb) @@ -83,6 +73,7 @@ def __init__(self, parent, context: Context, input_extension: str, common_layout.addLayout(cli_layout) layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(interpreter_group) layout.addWidget(common_group) layout.addStretch(100) diff --git a/spyder/plugins/profiler/widgets/run_conf.py b/spyder/plugins/profiler/widgets/run_conf.py index de5feb68fa9..20ea9c8b3f3 100644 --- a/spyder/plugins/profiler/widgets/run_conf.py +++ b/spyder/plugins/profiler/widgets/run_conf.py @@ -19,7 +19,6 @@ from spyder.plugins.run.api import ( RunExecutorConfigurationGroup, Context, RunConfigurationMetadata) from spyder.utils.misc import getcwd_or_home -from spyder.utils.stylesheet import AppStyle class ProfilerPyConfigurationGroup(RunExecutorConfigurationGroup): @@ -34,12 +33,6 @@ def __init__(self, parent, context: Context, input_extension: str, # --- General settings ---- common_group = QGroupBox(_("File settings")) common_layout = QGridLayout(common_group) - common_layout.setContentsMargins( - 3 * AppStyle.MarginSize, - 3 * AppStyle.MarginSize, - 3 * AppStyle.MarginSize, - 0, - ) self.clo_cb = QCheckBox(_("Command line options:")) common_layout.addWidget(self.clo_cb, 0, 0) @@ -50,6 +43,7 @@ def __init__(self, parent, context: Context, input_extension: str, common_layout.addWidget(self.clo_edit, 0, 1) layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(common_group) layout.addStretch(100) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index bf3cf5cb2d0..12db3f4b694 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -55,7 +55,7 @@ # Main constants -FILE_DIR = _("The directory of the configuration being executed") +FILE_DIR = _("The directory of the file being executed") CW_DIR = _("The current working directory") FIXED_DIR = _("The following directory:") EMPTY_NAME = _("Provide a name for this configuration") @@ -257,15 +257,6 @@ def setup(self): # --- Runner settings self.stack = QStackedWidget(self) - self.executor_group = QGroupBox(_("Runner settings")) - executor_layout = QVBoxLayout(self.executor_group) - executor_layout.setContentsMargins( - 3 * AppStyle.MarginSize, - 3 * AppStyle.MarginSize, - 3 * AppStyle.MarginSize, - 0 if MAC else AppStyle.MarginSize, - ) - executor_layout.addWidget(self.stack) # --- Working directory settings self.wdir_group = QGroupBox(_("Working directory settings")) @@ -306,7 +297,7 @@ def setup(self): 4 * AppStyle.MarginSize, ext_context_layout, (3 if MAC else 4) * AppStyle.MarginSize, - self.executor_group, + self.stack, self.wdir_group, (-2 if MAC else 1) * AppStyle.MarginSize, ) @@ -382,9 +373,9 @@ def context_changed(self, index: int, reset: bool = False): RunExecutorConfigurationGroup) if executor_conf_metadata['configuration_widget'] is None: - self.executor_group.setEnabled(False) + self.stack.setEnabled(False) else: - self.executor_group.setEnabled(True) + self.stack.setEnabled(True) self.wdir_group.setEnabled(requires_cwd) @@ -434,8 +425,7 @@ def context_changed(self, index: int, reset: bool = False): self.fixed_dir_radio.setChecked(True) self.wd_edit.setText(path) - if (not self.executor_group.isEnabled() and not - self.wdir_group.isEnabled()): + if not self.stack.isEnabled() and not self.wdir_group.isEnabled(): ok_btn = self.bbox.button(QDialogButtonBox.Ok) ok_btn.setEnabled(False) @@ -657,16 +647,7 @@ def setup(self): config_props_layout.addWidget(name_params_tip, 0, 2) # --- Runner settings - self.stack = QStackedWidget() - self.executor_group = QGroupBox(_("Runner settings")) - - parameters_layout = QVBoxLayout(self.executor_group) - parameters_layout.addWidget(self.stack) - - # Remove bottom margin because it adds unnecessary space - parameters_layout_margins = parameters_layout.contentsMargins() - parameters_layout_margins.setBottom(0) - parameters_layout.setContentsMargins(parameters_layout_margins) + self.stack = QStackedWidget(self) # --- Working directory settings self.wdir_group = QGroupBox(_("Working directory settings")) @@ -698,7 +679,7 @@ def setup(self): # --- Group all customization widgets into a collapsible one custom_config = CollapsibleWidget(self, _("Custom configuration")) custom_config.addWidget(config_props_group) - custom_config.addWidget(self.executor_group) + custom_config.addWidget(self.stack) custom_config.addWidget(self.wdir_group) # Fix bottom and left margins. @@ -842,9 +823,9 @@ def display_executor_configuration(self, index: int): RunExecutorConfigurationGroup) if executor_info['configuration_widget'] is None: - self.executor_group.setVisible(False) + self.stack.setVisible(False) else: - self.executor_group.setVisible(True) + self.stack.setVisible(True) metadata = self.run_conf_model.get_selected_metadata() context = metadata['context'] From b741f4e287e1b8979349ece09406a17d8ee4c410 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 9 Jun 2024 19:00:41 -0500 Subject: [PATCH 50/55] Run: Add menu entry to go to its Preferences page This will allow users to easily understand where they need to go to set global configurations. --- spyder/plugins/run/api.py | 1 + spyder/plugins/run/confpage.py | 2 +- spyder/plugins/run/container.py | 7 +++++++ spyder/plugins/run/plugin.py | 29 ++++++++++++++++++++++++++--- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/spyder/plugins/run/api.py b/spyder/plugins/run/api.py index 8b0d0c01b4a..2e186939f93 100644 --- a/spyder/plugins/run/api.py +++ b/spyder/plugins/run/api.py @@ -26,6 +26,7 @@ class RunActions: Run = 'run' Configure = 'configure' ReRun = 're-run last script' + GlobalConfigurations = "global configurations" class RunContextType(dict): diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index 47dd406b9f0..49455170f05 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -312,7 +312,7 @@ def setup_page(self): run_widget.setLayout(run_layout) # --- Tabs --- - self.create_tab(_("Run executors"), executor_widget) + self.create_tab(_("Global configurations"), executor_widget) self.create_tab(_("Editor interactions"), run_widget) def executor_index_changed(self, index: int): diff --git a/spyder/plugins/run/container.py b/spyder/plugins/run/container.py index 9b65e0cf50b..60951b083cc 100644 --- a/spyder/plugins/run/container.py +++ b/spyder/plugins/run/container.py @@ -38,6 +38,7 @@ class RunContainer(PluginMainContainer): """Non-graphical container used to spawn dialogs and creating actions.""" sig_run_action_created = Signal(str, bool, str) + sig_open_preferences_requested = Signal() # ---- PluginMainContainer API # ------------------------------------------------------------------------- @@ -89,6 +90,12 @@ def setup(self): context=Qt.ApplicationShortcut ) + self.create_action( + RunActions.GlobalConfigurations, + _("Global configurations"), + triggered=self.sig_open_preferences_requested + ) + self.re_run_action = self.create_action( RunActions.ReRun, _('Re-run &last file'), diff --git a/spyder/plugins/run/plugin.py b/spyder/plugins/run/plugin.py index 507abd10ab1..4ed75f90c5d 100644 --- a/spyder/plugins/run/plugin.py +++ b/spyder/plugins/run/plugin.py @@ -88,7 +88,11 @@ def on_initialize(self): container = self.get_container() container.sig_run_action_created.connect( - self.register_action_shortcuts) + self.register_action_shortcuts + ) + container.sig_open_preferences_requested.connect( + self._open_run_preferences + ) @on_plugin_available(plugin=Plugins.WorkingDirectory) def on_working_directory_available(self): @@ -101,7 +105,12 @@ def on_working_directory_available(self): def on_main_menu_available(self): main_menu = self.get_plugin(Plugins.MainMenu) - for action in [RunActions.Run, RunActions.ReRun, RunActions.Configure]: + for action in [ + RunActions.Run, + RunActions.ReRun, + RunActions.Configure, + RunActions.GlobalConfigurations, + ]: main_menu.add_item_to_application_menu( self.get_action(action), ApplicationMenus.Run, @@ -154,7 +163,12 @@ def on_working_directory_teardown(self): def on_main_menu_teardown(self): main_menu = self.get_plugin(Plugins.MainMenu) - for action in [RunActions.Run, RunActions.ReRun, RunActions.Configure]: + for action in [ + RunActions.Run, + RunActions.ReRun, + RunActions.Configure, + RunActions.GlobalConfigurations, + ]: main_menu.remove_item_from_application_menu( action, ApplicationMenus.Run @@ -800,3 +814,12 @@ def register_action_shortcuts( else: self.pending_shortcut_actions.append( (action, shortcut_context, action_name)) + + def _open_run_preferences(self): + preferences = self.get_plugin(Plugins.Preferences) + preferences.open_dialog() + + container = preferences.get_container() + dlg = container.dialog + index = dlg.get_index_by_name("run") + dlg.set_current_index(index) From 554ed059aa63381432b48cfaeff09315bf2f3007 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 10 Jun 2024 20:25:07 -0500 Subject: [PATCH 51/55] Run: Use table_model current executor params when deleting confs That allows to correctly delete confs that haven't been saved yet. --- spyder/plugins/run/confpage.py | 5 +++-- spyder/plugins/run/models.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spyder/plugins/run/confpage.py b/spyder/plugins/run/confpage.py index 49455170f05..64cce2b65c1 100644 --- a/spyder/plugins/run/confpage.py +++ b/spyder/plugins/run/confpage.py @@ -432,13 +432,13 @@ def clone_configuration(self): self.params_table.clone_configuration() def delete_configuration(self): - executor_name, _ = self.executor_model.selected_executor( + executor_name, __ = self.executor_model.selected_executor( self.previous_executor_index ) index = self.params_table.currentIndex().row() conf_index = self.table_model.get_tuple_index(index) - executor_params = self.all_executor_model[executor_name] + executor_params = self.table_model.executor_conf_params executor_params.pop(conf_index, None) if executor_name not in self._params_to_delete: @@ -447,6 +447,7 @@ def delete_configuration(self): self.table_model.set_parameters(executor_params) self.table_model.reset_model() + self.set_modified(True) self.set_buttons_status() diff --git a/spyder/plugins/run/models.py b/spyder/plugins/run/models.py index 8953d5ea803..81b4d18e224 100644 --- a/spyder/plugins/run/models.py +++ b/spyder/plugins/run/models.py @@ -397,7 +397,8 @@ class ExecutorRunParametersTableModel(QAbstractTableModel): def __init__(self, parent): super().__init__(parent) self.executor_conf_params: Dict[ - Tuple[str, str, str], ExtendedRunExecutionParameters] = {} + Tuple[str, str, str], ExtendedRunExecutionParameters + ] = {} self.params_index: Dict[int, Tuple[str, str, str]] = {} self.inverse_index: Dict[Tuple[str, str, str], int] = {} From 353e4bda9540647caebe31d2e73cf75f83a4536e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 10 Jun 2024 20:57:18 -0500 Subject: [PATCH 52/55] Run: Make customizing global configs for files easier in RunDialog - To do that we automatically set a custom name for global configs. That way users will be able to customize those configs as many times as they want. - Don't run config name validations for global configs if they haven't been customized. --- spyder/plugins/run/models.py | 31 ++++- spyder/plugins/run/tests/test_run.py | 2 +- spyder/plugins/run/widgets.py | 166 +++++++++++++++++++-------- 3 files changed, 147 insertions(+), 52 deletions(-) diff --git a/spyder/plugins/run/models.py b/spyder/plugins/run/models.py index 81b4d18e224..ca67fd30bf5 100644 --- a/spyder/plugins/run/models.py +++ b/spyder/plugins/run/models.py @@ -334,14 +334,39 @@ def get_parameters_index_by_name(self, parameters_name: str) -> int: return index - def get_parameter_names(self) -> List[str]: - """Get all parameter names for this executor.""" + def get_parameter_names(self, filter_global: bool = False) -> List[str]: + """ + Get all parameter names for this executor. + + Parameters + ---------- + filter_global: bool, optional + Whether to filter global parameters from the results. + """ names = [] for params in self.executor_conf_params.values(): - names.append(params["name"]) + if filter_global: + if params["file_uuid"] is not None: + names.append(params["name"]) + else: + names.append(params["name"]) return names + def get_number_of_custom_params(self, global_params_name: str) -> int: + """ + Get the number of custom parameters derived from a set of global ones. + + Parameters + ---------- + global_params_name: str + Name of the global parameters. + """ + names = self.get_parameter_names(filter_global=True) + return len( + [name for name in names if name.startswith(global_params_name)] + ) + def __len__(self) -> int: return len(self.executor_conf_params) diff --git a/spyder/plugins/run/tests/test_run.py b/spyder/plugins/run/tests/test_run.py index 4e23a854324..871c6caa597 100644 --- a/spyder/plugins/run/tests/test_run.py +++ b/spyder/plugins/run/tests/test_run.py @@ -599,7 +599,7 @@ def test_run_plugin(qtbot, run_mock): # Ensure that the executor run configuration was saved assert exec_conf['uuid'] is not None - assert exec_conf['name'] == "Custom for this file" + assert exec_conf['name'] == "Default (custom)" # Check that the configuration parameters are the ones defined by the # dialog diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index 12db3f4b694..a76a159a080 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -8,6 +8,7 @@ # Standard library imports import os.path as osp +import textwrap from typing import Optional, Tuple, List, Dict from uuid import uuid4 @@ -54,12 +55,17 @@ from spyder.widgets.helperwidgets import TipWidget -# Main constants +# ---- Main constants +# ----------------------------------------------------------------------------- FILE_DIR = _("The directory of the file being executed") CW_DIR = _("The current working directory") FIXED_DIR = _("The following directory:") EMPTY_NAME = _("Provide a name for this configuration") REPEATED_NAME = _("Select a different name for this configuration") +SAME_PARAMETERS = _( + "You are trying to save a configuration that is exactly the same as the " + "current one" +) class RunDialogStatus: @@ -68,6 +74,8 @@ class RunDialogStatus: Run = 2 +# ---- Base class +# ----------------------------------------------------------------------------- class BaseRunConfigDialog(QDialog): """Run configuration dialog box, base widget""" size_change = Signal(QSize) @@ -151,6 +159,8 @@ def setup(self): raise NotImplementedError +# ---- Dialogs +# ----------------------------------------------------------------------------- class ExecutionParametersDialog(BaseRunConfigDialog): """Run execution parameters edition dialog.""" @@ -489,7 +499,9 @@ def accept(self) -> None: if self.new_config: if name == '': self.store_params_text.status_action.setVisible(True) - self.store_params_text.status_action.setToolTip(EMPTY_NAME) + self.store_params_text.status_action.setToolTip( + '\n'.join(textwrap.wrap(EMPTY_NAME, 50)) + ) return else: extension = self.extension_combo.lineEdit().text() @@ -498,7 +510,7 @@ def accept(self) -> None: if name in current_names: self.store_params_text.status_action.setVisible(True) self.store_params_text.status_action.setToolTip( - REPEATED_NAME + '\n'.join(textwrap.wrap(REPEATED_NAME, 50)) ) return @@ -583,7 +595,7 @@ def setup(self): self.parameters_combo.setMinimumWidth(250) parameters_tip = TipWidget( _( - "Select here between global or local (i.e. for this file) " + "Select between global or local (i.e. for this file) " "execution parameters. You can set the latter below" ), icon=ima.icon('question_tip'), @@ -621,8 +633,9 @@ def setup(self): ) name_params_tip = TipWidget( _( - "Select a name for the execution parameters you want to set. " - "They will be saved after clicking the Ok button below" + "You can set as many configurations as you want by providing " + "different names. Each one will be saved after clicking the " + "Ok button below" ), icon=ima.icon('question_tip'), hover_icon=ima.icon('question_tip_hover'), @@ -764,17 +777,20 @@ def update_parameter_set(self, index: int): # Get parameters stored_params = self.parameter_model.get_parameters(index) + global_params = stored_params["file_uuid"] is None # Set parameters name - if stored_params["default"]: - # We set this name for default paramaters so users don't have to - # think about selecting one when customizing them - self.name_params_text.setText(_("Custom for this file")) + if global_params: + # We set this name for global params so users don't have to think + # about selecting one when customizing them + custom_name = self._get_auto_custom_name(stored_params["name"]) + self.name_params_text.setText(custom_name) else: + # We show the actual name for file params self.name_params_text.setText(stored_params["name"]) - # Disable delete button for default or global configs - if stored_params["default"] or stored_params["file_uuid"] is None: + # Disable delete button for global configs + if global_params: self.delete_button.setEnabled(False) else: self.delete_button.setEnabled(True) @@ -913,29 +929,74 @@ def get_configuration( def accept(self) -> None: self.status |= RunDialogStatus.Save - default_conf = self.current_widget.get_default_configuration() + # Configuration to save/execute widget_conf = self.current_widget.get_configuration() + + # Hide status action in case users fix the problem reported through it + # on a successive try self.name_params_text.status_action.setVisible(False) - # Different checks for the config name + # Detect if the current params are global + current_index = self.parameters_combo.currentIndex() + params = self.parameter_model.get_parameters(current_index) + global_params = params["file_uuid"] is None + + if global_params: + custom_name = self._get_auto_custom_name(params["name"]) + else: + custom_name = "" + + # Working directory params + path = None + source = None + if self.file_dir_radio.isChecked(): + source = WorkingDirSource.ConfigurationDirectory + elif self.cwd_radio.isChecked(): + source = WorkingDirSource.CurrentDirectory + else: + source = WorkingDirSource.CustomDirectory + path = self.wd_edit.text() + + cwd_opts = WorkingDirOpts(source=source, path=path) + + # Execution params + exec_params = RunExecutionParameters( + working_dir=cwd_opts, executor_params=widget_conf + ) + + # Different validations for the params name params_name = self.name_params_text.text() if self.isVisible(): allow_to_close = True if not params_name: - # Don't allow to save configs without a name + # Don't allow to save params without a name self.name_params_text.status_action.setVisible(True) - self.name_params_text.status_action.setToolTip(EMPTY_NAME) - allow_to_close = False - elif ( - params_name != self.parameters_combo.lineEdit().text() - and params_name in self.parameter_model.get_parameter_names() - ): - # Don't allow to save a config with the same name of an - # existing one because it doesn't make sense. + self.name_params_text.status_action.setToolTip( + '\n'.join(textwrap.wrap(EMPTY_NAME, 50)) + ) allow_to_close = False - self.name_params_text.status_action.setVisible(True) - self.name_params_text.status_action.setToolTip(REPEATED_NAME) + elif global_params and params_name == custom_name: + # We don't need to perform a validation in this case because we + # set the params name on behalf of users + pass + elif params_name != self.parameters_combo.lineEdit().text(): + if params_name in self.parameter_model.get_parameter_names(): + # Don't allow to save params with the same name of an + # existing one because it doesn't make sense. + allow_to_close = False + self.name_params_text.status_action.setVisible(True) + self.name_params_text.status_action.setToolTip( + '\n'.join(textwrap.wrap(REPEATED_NAME, 50)) + ) + elif params["params"] == exec_params: + # Don't allow to save params that are exactly the same as + # the current ones. + allow_to_close = False + self.name_params_text.status_action.setVisible(True) + self.name_params_text.status_action.setToolTip( + '\n'.join(textwrap.wrap(SAME_PARAMETERS, 50)) + ) if not allow_to_close: # With this the dialog can be closed when clicking the Cancel @@ -944,10 +1005,11 @@ def accept(self) -> None: return # Get index associated with config - if widget_conf == default_conf: - # This avoids saving an unnecessary "Custom for this file" config - # when the parameters haven't been modified - idx = 0 + if params["params"] == exec_params: + # This avoids saving an unnecessary custom config when the current + # parameters haven't been modified with respect to the selected + # config + idx = current_index else: idx = self.parameter_model.get_parameters_index_by_name( params_name @@ -963,21 +1025,7 @@ def accept(self) -> None: # Retrieve uuid and name from our config system uuid, name = self.parameter_model.get_parameters_uuid_name(idx) - path = None - source = None - if self.file_dir_radio.isChecked(): - source = WorkingDirSource.ConfigurationDirectory - elif self.cwd_radio.isChecked(): - source = WorkingDirSource.CurrentDirectory - else: - source = WorkingDirSource.CustomDirectory - path = self.wd_edit.text() - - cwd_opts = WorkingDirOpts(source=source, path=path) - - exec_params = RunExecutionParameters( - working_dir=cwd_opts, executor_params=widget_conf) - + # Build configuration to be saved or executed metadata_info = self.run_conf_model.get_metadata( self.configuration_combo.currentIndex() ) @@ -986,16 +1034,23 @@ def accept(self) -> None: uuid=uuid, name=name, params=exec_params, - file_uuid=metadata_info['uuid'], - default=True if (widget_conf == default_conf) else False + file_uuid=None + if (global_params and idx >= 0) + else metadata_info["uuid"], + default=True + if (global_params and params["default"] and idx >= 0) + else False, ) executor_name, __ = self.executors_model.get_selected_run_executor( self.executor_combo.currentIndex() ) - self.saved_conf = (metadata_info['uuid'], executor_name, - ext_exec_params) + self.saved_conf = ( + metadata_info["uuid"], + executor_name, + ext_exec_params, + ) return super().accept() @@ -1064,3 +1119,18 @@ def _center_dialog(self): ) self.move(x, y) + + def _get_auto_custom_name(self, global_params_name: str) -> str: + """ + Get an auto-generated custom name given the a global parameters one. + """ + n_custom = self.parameter_model.get_number_of_custom_params( + global_params_name + ) + + return ( + global_params_name + + " (" + + _("custom") + + (")" if n_custom == 0 else f" {n_custom})") + ) From ef830c7990f997701566b1c1d3b43fe42cef2a1e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 17 Jun 2024 17:52:13 -0500 Subject: [PATCH 53/55] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel Althviz Moré <16781833+dalthviz@users.noreply.github.com> --- spyder/app/tests/test_mainwindow.py | 2 +- spyder/plugins/run/container.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index b6fa914e01c..fd3c793de84 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -957,7 +957,7 @@ def test_shell_execution(main_window, qtbot, tmpdir): @flaky(max_runs=3) @pytest.mark.skipif( sys.platform.startswith('linux') and running_in_ci(), - reason="Fails frequently on Linux" + reason="Fails frequently on Linux and CI" ) @pytest.mark.order(after="test_debug_unsaved_function") def test_connection_to_external_kernel(main_window, qtbot): diff --git a/spyder/plugins/run/container.py b/spyder/plugins/run/container.py index 60951b083cc..b70d0c93bea 100644 --- a/spyder/plugins/run/container.py +++ b/spyder/plugins/run/container.py @@ -92,7 +92,7 @@ def setup(self): self.create_action( RunActions.GlobalConfigurations, - _("Global configurations"), + _("&Global configurations"), triggered=self.sig_open_preferences_requested ) From b1e6280f941d1dab9902aa746946142067d0d1fb Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 17 Jun 2024 21:07:12 -0500 Subject: [PATCH 54/55] Utils: Add accessors to SpyderApplication for main window properties Also, use those accessors in RunDialog to center it when uncollapsing the custom config widget. --- spyder/app/utils.py | 8 +++--- spyder/plugins/run/widgets.py | 31 +++++++++++----------- spyder/utils/qthelpers.py | 50 ++++++++++++++++++++++++++++++----- 3 files changed, 63 insertions(+), 26 deletions(-) diff --git a/spyder/app/utils.py b/spyder/app/utils.py index 67f1a21384a..05137b3aa24 100644 --- a/spyder/app/utils.py +++ b/spyder/app/utils.py @@ -367,15 +367,13 @@ def create_window(WindowClass, app, splash, options, args): main.show() main.post_visible_setup() - # Add a reference to the main window so it can be accessed from anywhere. + # Add a reference to the main window so it can be accessed from the + # application. # # Notes # ----- - # * This should be used to get main window properties (such as width, - # height or position) that other widgets can need to position relative to - # it. # * **DO NOT** use it to access other plugins functionality through it. - app.main_window = main + app._main_window = main if main.console: main.console.start_interpreter(namespace={}) diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py index a76a159a080..f607ff268d3 100644 --- a/spyder/plugins/run/widgets.py +++ b/spyder/plugins/run/widgets.py @@ -18,7 +18,6 @@ from qtpy.QtGui import QFontMetrics from qtpy.QtWidgets import ( QAction, - QApplication, QDialog, QDialogButtonBox, QGridLayout, @@ -37,6 +36,7 @@ # Local imports from spyder.api.translations import _ +from spyder.config.base import running_under_pytest from spyder.api.config.fonts import SpyderFontType, SpyderFontsMixin from spyder.api.widgets.comboboxes import SpyderComboBox from spyder.api.widgets.dialogs import SpyderDialogButtonBox @@ -50,6 +50,7 @@ ) from spyder.utils.icon_manager import ima from spyder.utils.misc import getcwd_or_home +from spyder.utils.qthelpers import qapplication from spyder.utils.stylesheet import AppStyle, MAC from spyder.widgets.collapsible import CollapsibleWidget from spyder.widgets.helperwidgets import TipWidget @@ -1104,21 +1105,21 @@ def _center_dialog(self): Center dialog relative to the main window after collapsing/expanding the custom configuration widget. """ - # main_window is usually not available in our tests, so we need to - # check for this. - main_window = getattr(QApplication.instance(), 'main_window', None) - - if main_window: - # We only center the dialog vertically because there's no need to - # do it horizontally. - x = self.x() - - y = ( - main_window.pos().y() - + ((main_window.height() - self.height()) // 2) - ) + # This doesn't work in our tests because the main window is usually + # not available in them. + if running_under_pytest(): + return + + qapp = qapplication() + main_window_pos = qapp.get_mainwindow_position() + main_window_height = qapp.get_mainwindow_height() + + # We only center the dialog vertically because there's no need to + # do it horizontally. + x = self.x() + y = main_window_pos.y() + ((main_window_height - self.height()) // 2) - self.move(x, y) + self.move(x, y) def _get_auto_custom_name(self, global_params_name: str) -> str: """ diff --git a/spyder/utils/qthelpers.py b/spyder/utils/qthelpers.py index f78b8b2850e..6788cdf53a4 100644 --- a/spyder/utils/qthelpers.py +++ b/spyder/utils/qthelpers.py @@ -21,14 +21,37 @@ # Third party imports from qtpy.compat import from_qvariant, to_qvariant -from qtpy.QtCore import (QEvent, QLibraryInfo, QLocale, QObject, Qt, QTimer, - QTranslator, QUrl, Signal, Slot) +from qtpy.QtCore import ( + QEvent, + QLibraryInfo, + QLocale, + QObject, + QPoint, + Qt, + QTimer, + QTranslator, + QUrl, + Signal, + Slot, +) from qtpy.QtGui import ( QDesktopServices, QFontMetrics, QKeyEvent, QKeySequence, QPixmap) -from qtpy.QtWidgets import (QAction, QApplication, QDialog, QHBoxLayout, - QLabel, QLineEdit, QMenu, QPlainTextEdit, - QPushButton, QStyle, QToolButton, QVBoxLayout, - QWidget) +from qtpy.QtWidgets import ( + QAction, + QApplication, + QDialog, + QHBoxLayout, + QLabel, + QLineEdit, + QMainWindow, + QMenu, + QPlainTextEdit, + QPushButton, + QStyle, + QToolButton, + QVBoxLayout, + QWidget, +) # Local imports from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType @@ -762,6 +785,9 @@ def __init__(self, *args): self._pending_file_open = [] self._original_handlers = {} + # This is filled at startup in spyder.app.utils.create_window + self._main_window: QMainWindow = None + def event(self, event): if sys.platform == 'darwin' and event.type() == QEvent.FileOpen: @@ -835,6 +861,18 @@ def set_monospace_interface_font(self, app_font): self.set_conf('monospace_app_font/size', monospace_size, section='appearance') + def get_mainwindow_position(self) -> QPoint: + """Get main window position.""" + return self._main_window.pos() + + def get_mainwindow_width(self) -> int: + """Get main window width.""" + return self._main_window.width() + + def get_mainwindow_height(self) -> int: + """Get main window height.""" + return self._main_window.height() + def restore_launchservices(): """Restore LaunchServices to the previous state""" From 5e801e39133045925f17efd8314086473078aaaa Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 18 Jun 2024 10:13:27 -0500 Subject: [PATCH 55/55] Testing: Fix failing tests due to the Numpy 2.0 release --- spyder/widgets/collectionseditor.py | 2 +- spyder/widgets/tests/test_collectioneditor.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index fbba71d0610..1c98b677001 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -2192,7 +2192,7 @@ def __init__(self): 'ddataframe': test_df, 'None': None, 'unsupported1': np.arccos, - 'unsupported2': np.cast, + 'unsupported2': np.asarray, # Test for spyder-ide/spyder#3518. 'big_struct_array': np.zeros(1000, dtype=[('ID', 'f8'), ('param1', 'f8', 5000)]), diff --git a/spyder/widgets/tests/test_collectioneditor.py b/spyder/widgets/tests/test_collectioneditor.py index 18d8defa30a..8a65b279e93 100644 --- a/spyder/widgets/tests/test_collectioneditor.py +++ b/spyder/widgets/tests/test_collectioneditor.py @@ -20,6 +20,7 @@ # Third party imports import numpy +from packaging.version import parse import pandas import pytest from flaky import flaky @@ -351,6 +352,9 @@ def test_shows_dataframeeditor_when_editing_index(monkeypatch): def test_sort_numpy_numeric_collectionsmodel(): + if parse(numpy.__version__) >= parse("2.0.0"): + numpy.set_printoptions(legacy="1.25") + var_list = [ numpy.float64(1e16), numpy.float64(10), numpy.float64(1), numpy.float64(0.1), numpy.float64(1e-6),