From 8a1b3e85acb06126461eac682b3ecf122326d191 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 11 Apr 2025 17:06:17 -0500 Subject: [PATCH 1/8] Layout: Fix tabifying plugins that don't set the TABIFY class constant --- spyder/plugins/layout/plugin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spyder/plugins/layout/plugin.py b/spyder/plugins/layout/plugin.py index 5acfe14da80..cd3d91c410c 100644 --- a/spyder/plugins/layout/plugin.py +++ b/spyder/plugins/layout/plugin.py @@ -1243,3 +1243,8 @@ def tabify_new_plugins(self): for plugin in self.get_dockable_plugins(): if plugin.get_conf('first_time', True): self.tabify_plugin(plugin, Plugins.Console) + + # This is necessary in case the plugin doesn't set its TABIFY + # constant + plugin.set_conf("enable", True) + plugin.set_conf("first_time", False) From 920a84817191ce580397cedcc0acfc82a147bab4 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 11 Apr 2025 17:07:24 -0500 Subject: [PATCH 2/8] Layout: Fix removing custom layouts from LayoutSettingsDialog --- spyder/plugins/layout/widgets/dialog.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/spyder/plugins/layout/widgets/dialog.py b/spyder/plugins/layout/widgets/dialog.py index b717a10296e..e263707f130 100644 --- a/spyder/plugins/layout/widgets/dialog.py +++ b/spyder/plugins/layout/widgets/dialog.py @@ -273,29 +273,33 @@ def __init__(self, parent, names, ui_names, order, active, read_only): def delete_layout(self): """Delete layout from the config.""" names, ui_names, order, active, read_only = ( - self.names, self.ui_names, self.order, self.active, self.read_only) + self.names, self.ui_names, self.order, self.active, self.read_only + ) row = self.table.selectionModel().currentIndex().row() ui_name, name, state = self.table.model().row(row) if name not in read_only: - name = from_qvariant( - self.table.selectionModel().currentIndex().data(), - to_text_string) if ui_name in ui_names: index = ui_names.index(ui_name) else: # In case nothing has focus in the table return + if index != -1: - order.remove(ui_name) - names.remove(ui_name) + order.remove(name) + names.remove(name) ui_names.remove(ui_name) + if name in active: - active.remove(ui_name) + active.remove(name) + self.names, self.ui_names, self.order, self.active = ( - names, ui_names, order, active) + names, ui_names, order, active + ) self.table.model().set_data( - names, ui_names, order, active, read_only) + names, ui_names, order, active, read_only + ) + index = self.table.model().index(0, 0) self.table.setCurrentIndex(index) self.table.setFocus() From 19bfeeaf20192ab0170c2d53ed984f6bddb59633 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 12 Apr 2025 13:47:14 -0500 Subject: [PATCH 3/8] Layout: Improve style of LayoutSettingsDialog This now follows a smiliar style to the Pythonpath manager --- spyder/plugins/layout/widgets/dialog.py | 89 ++++++++++++++++++------- 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/spyder/plugins/layout/widgets/dialog.py b/spyder/plugins/layout/widgets/dialog.py index e263707f130..9c01c02c8e7 100644 --- a/spyder/plugins/layout/widgets/dialog.py +++ b/spyder/plugins/layout/widgets/dialog.py @@ -12,15 +12,29 @@ # Third party imports from qtpy.QtCore import QAbstractTableModel, QModelIndex, QSize, Qt from qtpy.compat import from_qvariant, to_qvariant -from qtpy.QtWidgets import (QAbstractItemView, QDialog, - QDialogButtonBox, QGroupBox, QHBoxLayout, - QPushButton, QTableView, QVBoxLayout) +from qtpy.QtWidgets import ( + QAbstractItemView, + QDialog, + QDialogButtonBox, + QHBoxLayout, + QLabel, + QTableView, + QVBoxLayout, +) # Local imports +from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.api.widgets.comboboxes import SpyderComboBox from spyder.api.widgets.dialogs import SpyderDialogButtonBox from spyder.config.base import _ from spyder.py3compat import to_text_string +from spyder.utils.stylesheet import AppStyle, PANES_TOOLBAR_STYLESHEET + + +class LayoutSettingsToolButtons: + MoveUp = 'move_up' + MoveDown = 'move_down' + Remove = 'remove' class LayoutModel(QAbstractTableModel): @@ -156,7 +170,7 @@ def __init__(self, parent, order): # widget setup self.button_ok.setEnabled(False) self.dialog_size = QSize(300, 100) - self.setWindowTitle('Save layout as') + self.setWindowTitle(_('Save layout as')) self.setModal(True) self.setMinimumSize(self.dialog_size) self.setFixedSize(self.dialog_size) @@ -180,8 +194,12 @@ def check_text(self, text): self.button_ok.setEnabled(True) -class LayoutSettingsDialog(QDialog): +class LayoutSettingsDialog(QDialog, SpyderWidgetMixin): """Layout settings dialog""" + + # Dummy variable to avoid a warning + CONF_SECTION = "" + def __init__(self, parent, names, ui_names, order, active, read_only): super(LayoutSettingsDialog, self).__init__(parent) # variables @@ -193,25 +211,51 @@ def __init__(self, parent, names, ui_names, order, active, read_only): self.active = active self.read_only = read_only + # To style the tool buttons + self.setStyleSheet(PANES_TOOLBAR_STYLESHEET.to_string()) + # widgets - self.button_move_up = QPushButton(_('Move up')) - self.button_move_down = QPushButton(_('Move down')) - self.button_delete = QPushButton(_('Delete layout')) + description_label = QLabel(_("Reorder or delete your saved layouts")) + description_label.setAlignment(Qt.AlignCenter) + description_label.setWordWrap(True) + + self.button_move_up = self.create_toolbutton( + LayoutSettingsToolButtons.MoveUp, + tip=_('Move up'), + icon=self.create_icon('1uparrow'), + triggered=lambda: self.move_layout(True), + register=False, + ) + self.button_move_down = self.create_toolbutton( + LayoutSettingsToolButtons.MoveDown, + tip=_('Move down'), + icon=self.create_icon('1downarrow'), + triggered=lambda: self.move_layout(False), + register=False, + ) + self.button_delete = self.create_toolbutton( + LayoutSettingsToolButtons.Remove, + tip=_('Delete layout'), + icon=self.create_icon('editclear'), + triggered=self.delete_layout, + register=False, + ) + self.button_box = SpyderDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self ) - self.group_box = QGroupBox(_("Layout display and order")) + self.table = QTableView(self) - self.ok_button = self.button_box.button(QDialogButtonBox.Ok) - self.cancel_button = self.button_box.button(QDialogButtonBox.Cancel) - self.cancel_button.setDefault(True) - self.cancel_button.setAutoDefault(True) # widget setup - self.dialog_size = QSize(300, 200) + self.dialog_size = QSize(320, 350) self.setMinimumSize(self.dialog_size) self.setFixedSize(self.dialog_size) - self.setWindowTitle('Layout Settings') + self.setWindowTitle(_("Layouts display and order")) + + cancel_button = self.button_box.button(QDialogButtonBox.Cancel) + cancel_button.setDefault(True) + cancel_button.setAutoDefault(True) self.table.setModel( LayoutModel(self.table, names, ui_names, order, active, read_only)) @@ -235,13 +279,15 @@ def __init__(self, parent, names, ui_names, order, active, read_only): buttons_layout.addStretch() buttons_layout.addWidget(self.button_delete) - group_layout = QHBoxLayout() - group_layout.addWidget(self.table) - group_layout.addLayout(buttons_layout) - self.group_box.setLayout(group_layout) + order_layout = QHBoxLayout() + order_layout.addWidget(self.table) + order_layout.addLayout(buttons_layout) layout = QVBoxLayout() - layout.addWidget(self.group_box) + layout.addWidget(description_label) + layout.addSpacing(2 * AppStyle.MarginSize) + layout.addLayout(order_layout) + layout.addSpacing(2 * AppStyle.MarginSize) layout.addWidget(self.button_box) self.setLayout(layout) @@ -249,9 +295,6 @@ def __init__(self, parent, names, ui_names, order, active, read_only): # signals and slots self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.close) - self.button_delete.clicked.connect(self.delete_layout) - self.button_move_up.clicked.connect(lambda: self.move_layout(True)) - self.button_move_down.clicked.connect(lambda: self.move_layout(False)) self.table.model().dataChanged.connect( lambda: self.selection_changed(None, None)) self._selection_model.selectionChanged.connect( From 9599d0d2ad0808e0d3b1067b411c211115d20fd1 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 12 Apr 2025 13:59:29 -0500 Subject: [PATCH 4/8] Layout: Ask before removing a layout in LayoutSettingsDialog --- spyder/plugins/layout/widgets/dialog.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spyder/plugins/layout/widgets/dialog.py b/spyder/plugins/layout/widgets/dialog.py index 9c01c02c8e7..3275e127a27 100644 --- a/spyder/plugins/layout/widgets/dialog.py +++ b/spyder/plugins/layout/widgets/dialog.py @@ -17,6 +17,7 @@ QDialog, QDialogButtonBox, QHBoxLayout, + QMessageBox, QLabel, QTableView, QVBoxLayout, @@ -321,6 +322,16 @@ def delete_layout(self): row = self.table.selectionModel().currentIndex().row() ui_name, name, state = self.table.model().row(row) + answer = QMessageBox.question( + self, + _("Remove layout"), + _("Do you want to remove the layout '{}'?").format(ui_name), + QMessageBox.Yes | QMessageBox.No + ) + + if not answer: + return + if name not in read_only: if ui_name in ui_names: index = ui_names.index(ui_name) From a3346f71dd87d42a849a104a6ff550a6a00ec61c Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 12 Apr 2025 21:29:18 -0500 Subject: [PATCH 5/8] Layout: Set placeholder text in LayoutSaveDialog Also, increase its width a bit so there's enough space for it in other languages. --- spyder/plugins/layout/widgets/dialog.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/spyder/plugins/layout/widgets/dialog.py b/spyder/plugins/layout/widgets/dialog.py index 3275e127a27..d6ec5d87e9f 100644 --- a/spyder/plugins/layout/widgets/dialog.py +++ b/spyder/plugins/layout/widgets/dialog.py @@ -150,7 +150,8 @@ def set_row(self, rownum, value): class LayoutSaveDialog(QDialog): - """ """ + """Dialog to save a custom layout with a given name.""" + def __init__(self, parent, order): super(LayoutSaveDialog, self).__init__(parent) @@ -162,6 +163,10 @@ def __init__(self, parent, order): self.combo_box.addItems(order) self.combo_box.setEditable(True) self.combo_box.clearEditText() + self.combo_box.lineEdit().setPlaceholderText( + _("Give a name to your layout") + ) + self.button_box = SpyderDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self ) @@ -170,7 +175,7 @@ def __init__(self, parent, order): # widget setup self.button_ok.setEnabled(False) - self.dialog_size = QSize(300, 100) + self.dialog_size = QSize(350, 100) self.setWindowTitle(_('Save layout as')) self.setModal(True) self.setMinimumSize(self.dialog_size) From 68f1f496716918b569fad17e85ace67519b6e68f Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 13 Apr 2025 11:30:08 -0500 Subject: [PATCH 6/8] Layout: Set _first_spyder_run only once at startup - Since that attribute was modified in several places, it was not possible to rely on it to really tell if Spyder has been run for the first time. - Make it a property so it's not modified in the future. --- spyder/plugins/layout/plugin.py | 37 ++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/spyder/plugins/layout/plugin.py b/spyder/plugins/layout/plugin.py index cd3d91c410c..eab8898e9ff 100644 --- a/spyder/plugins/layout/plugin.py +++ b/spyder/plugins/layout/plugin.py @@ -9,6 +9,7 @@ """ # Standard library imports import configparser as cp +from functools import lru_cache import logging import os @@ -95,7 +96,6 @@ def get_icon(cls): def on_initialize(self): self._last_plugin = None - self._first_spyder_run = False self._fullscreen_flag = None # The following flag remember the maximized state even when # the window is in fullscreen mode: @@ -106,6 +106,12 @@ def on_initialize(self): self._state_before_maximizing = None self._interface_locked = self.get_conf('panes_locked', section='main') + # If Spyder has already been run once, this option needs to be False. + # Note: _first_spyder_run needs to be accessed at least once in this + # method to be computed at startup. + if not self._first_spyder_run: + self.set_conf("first_time", False) + # Register default layouts self.register_layout(self, SpyderLayout) self.register_layout(self, RLayout) @@ -234,6 +240,25 @@ def on_mainwindow_visible(self): # ---- Private API # ------------------------------------------------------------------------- + @property + @lru_cache + def _first_spyder_run(self): + """ + Check if Spyder is run for the first time. + + Notes + ----- + * We declare this as a property to prevent reassignments in other + places of this class. + * It only needs to be computed once at startup (i.e. it needs to be + accessed in on_initialize). + """ + # We need to do this double check because we were not using the + # "first_time" option in 6.0 and older versions. + return ( + self.get_conf("first_time", True) and self.get_conf("names") == [] + ) + def _get_internal_dockable_plugins(self): """Get the list of internal dockable plugins""" return get_class_values(DockablePlugins) @@ -383,11 +408,9 @@ def setup_layout(self, default=False): settings = self.load_window_settings(prefix, default) hexstate = settings[0] - self._first_spyder_run = False if hexstate is None: # First Spyder execution: self.main.setWindowState(Qt.WindowMaximized) - self._first_spyder_run = True self.setup_default_layouts(DefaultLayouts.SpyderLayout, settings) # Restore the original defaults. This is necessary, for instance, @@ -428,9 +451,7 @@ def setup_default_layouts(self, layout_id, settings): main = self.main main.setUpdatesEnabled(False) - first_spyder_run = bool(self._first_spyder_run) # Store copy - - if first_spyder_run: + if self._first_spyder_run: self.set_window_settings(*settings) else: if self._last_plugin: @@ -451,8 +472,8 @@ def setup_default_layouts(self, layout_id, settings): # Apply selected layout layout.set_main_window_layout(self.main, self.get_dockable_plugins()) - if first_spyder_run: - self._first_spyder_run = False + if self._first_spyder_run: + self.set_conf("first_time", False) else: self.main.setMinimumWidth(min_width) self.main.setMaximumWidth(max_width) From 644ed48dec050b891f8e6cfc42d0ffca96acea99 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 13 Apr 2025 11:32:17 -0500 Subject: [PATCH 7/8] Layout: Call set_window_settings only once on the first run --- spyder/plugins/layout/plugin.py | 35 ++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/spyder/plugins/layout/plugin.py b/spyder/plugins/layout/plugin.py index eab8898e9ff..f21a3ea95e8 100644 --- a/spyder/plugins/layout/plugin.py +++ b/spyder/plugins/layout/plugin.py @@ -105,6 +105,9 @@ def on_initialize(self): self._saved_normal_geometry = None self._state_before_maximizing = None self._interface_locked = self.get_conf('panes_locked', section='main') + # The following flag is used to apply the window settings only once + # during the first run + self._window_settings_applied_on_first_run = False # If Spyder has already been run once, this option needs to be False. # Note: _first_spyder_run needs to be accessed at least once in this @@ -619,16 +622,27 @@ def get_window_settings(self): def set_window_settings(self, hexstate, window_size, pos, is_maximized, is_fullscreen): """ - Set window settings Symetric to the 'get_window_settings' accessor. + Set window settings. + + Symetric to the 'get_window_settings' accessor. """ - main = self.main - main.setUpdatesEnabled(False) - self.window_size = QSize(window_size[0], - window_size[1]) # width, height - self.window_position = QPoint(pos[0], pos[1]) # x,y - main.setWindowState(Qt.WindowNoState) - main.resize(self.window_size) - main.move(self.window_position) + # Prevent calling this method multiple times on first run because it + # causes main window flickering on Windows and Mac. + # Fixes spyder-ide/spyder#15074 + if ( + self._window_settings_applied_on_first_run + and self._first_spyder_run + ): + return + + self.main.setUpdatesEnabled(False) + self.window_size = QSize( + window_size[0], window_size[1] # width, height + ) + self.window_position = QPoint(pos[0], pos[1]) # x, y + self.main.setWindowState(Qt.WindowNoState) + self.main.resize(self.window_size) + self.main.move(self.window_position) # Window layout if hexstate: @@ -657,6 +671,9 @@ def set_window_settings(self, hexstate, window_size, pos, is_maximized, elif is_maximized: self.main.setWindowState(Qt.WindowMaximized) + # Settings applied at startup + self._window_settings_applied_on_first_run = True + self.main.setUpdatesEnabled(True) def save_current_window_settings(self, prefix, section='main', From 8d380f2d5e2dabaf403fc650e13b447de508ef82 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 13 Apr 2025 13:53:37 -0400 Subject: [PATCH 8/8] Layout: Fix main window flickering on Mac at startup This happened when the window was maximized in the previous session. --- spyder/plugins/layout/plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spyder/plugins/layout/plugin.py b/spyder/plugins/layout/plugin.py index f21a3ea95e8..0eeb1af9836 100644 --- a/spyder/plugins/layout/plugin.py +++ b/spyder/plugins/layout/plugin.py @@ -636,11 +636,12 @@ def set_window_settings(self, hexstate, window_size, pos, is_maximized, return self.main.setUpdatesEnabled(False) + + # Restore window properties self.window_size = QSize( window_size[0], window_size[1] # width, height ) self.window_position = QPoint(pos[0], pos[1]) # x, y - self.main.setWindowState(Qt.WindowNoState) self.main.resize(self.window_size) self.main.move(self.window_position)