Repository: spyder-ide/spyder

Issue:
Spyder 4.2.4 Regression - Custom Layouts Broken
<!--- **PLEASE READ:** When submitting here, please ensure you've completed the following checklist and checked the boxes to confirm. Issue reports without it may be closed. Thanks! --->

### Issue Report Checklist

* [x] Searched the [issues page](https://github.com/spyder-ide/spyder/issues?q=is%3Aissue) for similar reports
* [x] Read the relevant sections of the [Spyder Troubleshooting Guide](https://github.com/spyder-ide/spyder/wiki/Troubleshooting-Guide-and-FAQ) and followed its advice
* [x] Reproduced the issue after updating with ``conda update spyder`` (or ``pip``, if not using Anaconda) (I used windowsx64-full installer)
* [ ] Could not reproduce inside ``jupyter qtconsole`` (if console-related)
* [x] Tried basic troubleshooting (if a bug/error)
    * [x] Restarted Spyder
    * [x] Reset preferences with ``spyder --reset``
    * [ ] Reinstalled the latest version of [Anaconda](https://www.anaconda.com/download/)
    * [x] Tried the other applicable steps from the Troubleshooting Guide
* [x] Completed the **Problem Description**, **Steps to Reproduce** and **Version** sections below


## Problem Description

Upon updating to Spyder 4.2.4, custom layouts no longer survive. When saving a new layout, it shows up in `View > Window Layouts`. Selecting the new layout to load it causes the Spyder window to flash, then revert to the default layout. Checking `View > Window Layouts` reveals that the new layout is no longer present. The new layout is similarly deleted when restarting Spyder.

Similarly, a rearranged but not saved layout should remain when restarting Spyder, but it does not.

Loading any of the default layouts works just fine.

Reverting to Spyder 4.2.3 resolves this issue.

### What steps reproduce the problem?

1. Open Spyder 4.2.4
2. Save a new window layout (you don't even need to rearrange, but rearranging has the same effect)
3. Go to `View > Window Layouts` and attempt to load the newly saved layout.
4. Check `View > Window Layouts` after Spyder stops flashing.

### What is the expected output? What do you see instead?

I expect a saved layout to save, and to load when requested. I similarly expect Spyder to recall how my windows were laid out when I next open the program.

Instead, loading a saved layout or restarting Spyder yields the default layout, with no recollection of my rearrangement.

### Paste Traceback/Error Below (if applicable)
<!--- Copy from error dialog or View > Panes > Internal Console --->

```python-traceback

PASTE TRACEBACK HERE

```

## Versions
<!--- You can get this information from Help > About Spyder...
or (if Spyder won't launch) the "conda list" command
from the Anaconda Prompt/Terminal/command line. --->

* Spyder version: 4.2.4 None
* Python version: 3.7.9 64-bit
* Qt version: 5.12.10
* PyQt5 version: 5.12.3
* Operating System: Windows 10

### Dependencies
<!--- Please go to the menu entry Help > Dependencies,
press the Copy to clipboard button and paste below --->
```

# Mandatory:
atomicwrites >=1.2.0            :  1.4.0 (OK)
chardet >=2.0.0                 :  4.0.0 (OK)
cloudpickle >=0.5.0             :  1.6.0 (OK)
diff_match_patch >=20181111     :  20200713 (OK)
intervaltree                    :  None (OK)
IPython >=7.6.0                 :  7.21.0 (OK)
jedi =0.17.2                    :  0.17.2 (OK)
jsonschema >=3.2.0              :  3.2.0 (OK)
keyring >=17.0.0                :  23.0.0 (OK)
nbconvert >=4.0                 :  6.0.7 (OK)
numpydoc >=0.6.0                :  1.1.0 (OK)
paramiko >=2.4.0                :  2.7.2 (OK)
parso =0.7.0                    :  0.7.0 (OK)
pexpect >=4.4.0                 :  4.8.0 (OK)
pickleshare >=0.4               :  0.7.5 (OK)
psutil >=5.3                    :  5.8.0 (OK)
pygments >=2.0                  :  2.8.1 (OK)
pylint >=1.0                    :  2.7.2 (OK)
pyls >=0.36.2;<1.0.0            :  0.36.2 (OK)
pyls_black >=0.4.6              :  0.4.6 (OK)
pyls_spyder >=0.3.2             :  0.3.2 (OK)
qdarkstyle >=2.8;<3.0           :  2.8.1 (OK)
qtawesome >=0.5.7               :  1.0.2 (OK)
qtconsole >=5.0.3               :  5.0.3 (OK)
qtpy >=1.5.0                    :  1.9.0 (OK)
rtree >=0.8.3                   :  0.9.4 (OK)
setuptools >=39.0.0             :  54.1.2 (OK)
sphinx >=0.6.6                  :  3.5.2 (OK)
spyder_kernels >=1.10.2;<1.11.0 :  1.10.2 (OK)
textdistance >=4.2.0            :  4.2.1 (OK)
three_merge >=0.1.1             :  0.1.1 (OK)
watchdog                        :  1.0.2 (OK)
zmq >=17                        :  22.0.3 (OK)

# Optional:
cython >=0.21                   :  0.29.22 (OK)
matplotlib >=2.0.0              :  3.3.4 (OK)
numpy >=1.7                     :  1.19.3 (OK)
pandas >=1.1.1                  :  1.2.3 (OK)
scipy >=0.17.0                  :  1.6.1 (OK)
sympy >=0.7.3                   :  1.7.1 (OK)

```



Discussion:
Thanks for reporting this @brhubbar. I have another bug when trying to save the layouts, it seems like the menu options get deactivated when saving any layout. This might be a Mac OS issue though.
<img width="1440" alt="Screenshot 2021-03-19 at 7 26 48 PM" src="https://user-images.githubusercontent.com/18587879/111853574-8d0b0800-88e9-11eb-98f3-e847cfc29974.png">

@dalthviz can you try testing this in windows and let me know if you see the same behaviour that @brhubbar is describing?

Thanks for the quick reply! I am using Windows, I installed using the Spyder_64bit_full installer

I am *not* seeing the menus disable as you pictured there.

Resolving patch:
diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py
--- a/spyder/app/mainwindow.py
+++ b/spyder/app/mainwindow.py
@@ -134,6 +134,9 @@
 # Set the index for the default tour
 DEFAULT_TOUR = 0
 
+# Version passed to saveState/restoreState
+WINDOW_STATE_VERSION = 0
+
 #==============================================================================
 # Install Qt messaage handler
 #==============================================================================
@@ -1518,7 +1521,9 @@ def get_window_settings(self):
         pos = (self.window_position.x(), self.window_position.y())
         prefs_dialog_size = (self.prefs_dialog_size.width(),
                              self.prefs_dialog_size.height())
-        hexstate = qbytearray_to_str(self.saveState(version=1))
+        hexstate = qbytearray_to_str(
+            self.saveState(version=WINDOW_STATE_VERSION)
+        )
         return (hexstate, window_size, prefs_dialog_size, pos, is_maximized,
                 is_fullscreen)
 
@@ -1542,11 +1547,11 @@ def set_window_settings(self, hexstate, window_size, prefs_dialog_size,
         if hexstate:
             hexstate_valid = self.restoreState(
                 QByteArray().fromHex(str(hexstate).encode('utf-8')),
-                version=1
+                version=WINDOW_STATE_VERSION
             )
 
-            # Check layout validity. Spyder 4 uses the version 1 state,
-            # whereas Spyder 5 will use version 2 state. For more info see the
+            # Check layout validity. Spyder 4 uses the version 0 state,
+            # whereas Spyder 5 will use version 1 state. For more info see the
             # version argument for QMainWindow.restoreState:
             # https://doc.qt.io/qt-5/qmainwindow.html#restoreState
             if not hexstate_valid:
@@ -1596,7 +1601,7 @@ def save_current_window_settings(self, prefix, section='main',
         if none_state:
             CONF.set(section, prefix + 'state', None)
         else:
-            qba = self.saveState()
+            qba = self.saveState(version=WINDOW_STATE_VERSION)
             CONF.set(section, prefix + 'state', qbytearray_to_str(qba))
         CONF.set(section, prefix + 'statusbar',
                  not self.statusBar().isHidden())
@@ -2542,7 +2547,9 @@ def maximize_dockwidget(self, restore=False):
                 return
 
             # Select plugin to maximize
-            self.state_before_maximizing = self.saveState()
+            self.state_before_maximizing = self.saveState(
+                version=WINDOW_STATE_VERSION
+            )
             focus_widget = QApplication.focusWidget()
             for plugin in (self.widgetlist + self.thirdparty_plugins):
                 plugin.dockwidget.hide()
@@ -2579,7 +2586,8 @@ def maximize_dockwidget(self, restore=False):
             self.last_plugin.dockwidget.toggleViewAction().setEnabled(True)
             self.setCentralWidget(None)
             self.last_plugin._ismaximized = False
-            self.restoreState(self.state_before_maximizing)
+            self.restoreState(self.state_before_maximizing,
+                              version=WINDOW_STATE_VERSION)
             self.state_before_maximizing = None
             self.last_plugin.get_focus_widget().setFocus()
         self.__update_maximize_action()


Test patch:
diff --git a/spyder/plugins/editor/widgets/tests/test_decorations.py b/spyder/plugins/editor/widgets/tests/test_decorations.py
--- a/spyder/plugins/editor/widgets/tests/test_decorations.py
+++ b/spyder/plugins/editor/widgets/tests/test_decorations.py
@@ -9,7 +9,6 @@
 # Third party imports
 import os.path as osp
 import random
-import sys
 from unittest.mock import patch
 
 from flaky import flaky
@@ -187,8 +186,6 @@ def test_update_decorations_when_scrolling(qtbot):
         # Simulate continuously pressing the down arrow key.
         for __ in range(200):
             qtbot.keyPress(editor, Qt.Key_Down)
-            if sys.platform.startswith('linux'):
-                qtbot.wait(5)
 
         # Only one call to _update should be done, after releasing the key.
         qtbot.wait(editor.UPDATE_DECORATIONS_TIMEOUT + 100)
@@ -197,8 +194,6 @@ def test_update_decorations_when_scrolling(qtbot):
         # Simulate continuously pressing the up arrow key.
         for __ in range(200):
             qtbot.keyPress(editor, Qt.Key_Up)
-            if sys.platform.startswith('linux'):
-                qtbot.wait(5)
 
         # Only one call to _update should be done, after releasing the key.
         qtbot.wait(editor.UPDATE_DECORATIONS_TIMEOUT + 100)