Repository: spyder-ide/spyder

Issue:
Use new functionality in Qt 5.6 to resize dockwidgets programatically
This way we could avoid the kind of hacky methods we have right now to create the Matlab, RStudio, etc, layouts.

This new change is mentioned here:

https://woboq.com/blog/qdockwidget-changes-in-56.html


Discussion:

Resolving patch:
diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py
--- a/spyder/app/mainwindow.py
+++ b/spyder/app/mainwindow.py
@@ -68,7 +68,8 @@
 from qtpy import API, PYQT5
 from qtpy.compat import from_qvariant
 from qtpy.QtCore import (QByteArray, QCoreApplication, QPoint, QSize, Qt,
-                         QThread, QTimer, QUrl, Signal, Slot)
+                         QThread, QTimer, QUrl, Signal, Slot,
+                         qInstallMessageHandler)
 from qtpy.QtGui import QColor, QDesktopServices, QIcon, QKeySequence, QPixmap
 from qtpy.QtWidgets import (QAction, QApplication, QDockWidget, QMainWindow,
                             QMenu, QMessageBox, QShortcut, QSplashScreen,
@@ -246,6 +247,28 @@ def setup_logging(cli_options):
                             filename=log_file,
                             filemode='w+')
 
+
+def qt_message_handler(msg_type, msg_log_context, msg_string):
+    """
+    Qt warning messages are intercepted by this handler.
+
+    On some operating systems, warning messages might be displayed
+    even if the actual message does not apply. This filter adds a
+    blacklist for messages that are being printed for no apparent
+    reason. Anything else will get printed in the internal console.
+
+    In DEV mode, all messages are printed.
+    """
+    BLACKLIST = [
+        'QMainWidget::resizeDocks: all sizes need to be larger than 0',
+    ]
+    if DEV or msg_string not in BLACKLIST:
+        print(msg_string)  # spyder: test-skip
+
+
+qInstallMessageHandler(qt_message_handler)
+
+
 # =============================================================================
 # Dependencies
 # =============================================================================
@@ -287,8 +310,9 @@ class MainWindow(QMainWindow):
     all_actions_defined = Signal()
     sig_pythonpath_changed = Signal()
     sig_open_external_file = Signal(str)
-    sig_resized = Signal("QResizeEvent")  # related to interactive tour
-    sig_moved = Signal("QMoveEvent")      # related to interactive tour
+    sig_resized = Signal("QResizeEvent")     # Related to interactive tour
+    sig_moved = Signal("QMoveEvent")         # Related to interactive tour
+    sig_layout_setup_ready = Signal(object)  # Related to default layouts
 
     def __init__(self, options=None):
         QMainWindow.__init__(self)
@@ -298,7 +322,6 @@ def __init__(self, options=None):
             # Enabling scaling for high dpi
             qapp.setAttribute(Qt.AA_UseHighDpiPixmaps)
         self.default_style = str(qapp.style().objectName())
-
         self.dialog_manager = DialogManager()
 
         self.init_workdir = options.working_directory
@@ -1528,15 +1551,30 @@ def setup_layout(self, default=False):
                 traceback.print_exc(file=STDERR)
 
     def setup_default_layouts(self, index, settings):
-        """Setup default layouts when run for the first time"""
-        self.maximize_dockwidget(restore=True)
-        self.set_window_settings(*settings)
+        """Setup default layouts when run for the first time."""
         self.setUpdatesEnabled(False)
 
+        first_spyder_run = bool(self.first_spyder_run)  # Store copy
+
+        if first_spyder_run:
+            self.set_window_settings(*settings)
+        else:
+            if self.last_plugin:
+                if self.last_plugin.ismaximized:
+                    self.maximize_dockwidget(restore=True)
+
+            if not (self.isMaximized() or self.maximized_flag):
+                self.showMaximized()
+
+            min_width = self.minimumWidth()
+            max_width = self.maximumWidth()
+            base_width = self.width()
+            self.setFixedWidth(base_width)
+
         # IMPORTANT: order has to be the same as defined in the config file
         MATLAB, RSTUDIO, VERTICAL, HORIZONTAL = range(self.DEFAULT_LAYOUTS)
 
-        # define widgets locally
+        # Define widgets locally
         editor = self.editor
         console_ipy = self.ipyconsole
         console_int = self.console
@@ -1551,154 +1589,198 @@ def setup_default_layouts(self, index, settings):
         helper = self.onlinehelp
         plugins = self.thirdparty_plugins
 
+        # Stored for tests
         global_hidden_widgets = [finder, console_int, explorer_project,
                                  helper] + plugins
         global_hidden_toolbars = [self.source_toolbar, self.edit_toolbar,
                                   self.search_toolbar]
         # Layout definition
-        # layouts are organized by columns, each colum is organized by rows
-        # widths have to add 1.0, height per column have to add 1.0
+        # --------------------------------------------------------------------
+        # Layouts are organized by columns, each column is organized by rows.
+        # Widths have to add 1.0 (except if hidden), height per column has to
+        # add 1.0 as well
+
         # Spyder Default Initial Layout
-        s_layout = {'widgets': [
-                    # column 0
-                    [[explorer_project]],
-                    # column 1
-                    [[editor]],
-                    # column 2
-                    [[outline]],
-                    # column 3
-                    [[help_plugin, explorer_variable, plots, helper,
-                      explorer_file, finder] + plugins,
-                     [console_int, console_ipy, history]]
-                    ],
-                    'width fraction': [0.0,             # column 0 width
-                                       0.55,            # column 1 width
-                                       0.0,             # column 2 width
-                                       0.45],           # column 3 width
-                    'height fraction': [[1.0],          # column 0, row heights
-                                        [1.0],          # column 1, row heights
-                                        [1.0],          # column 2, row heights
-                                        [0.46, 0.54]],  # column 3, row heights
-                    'hidden widgets': [outline],
-                    'hidden toolbars': [],
-                    }
-        r_layout = {'widgets': [
-                    # column 0
-                    [[editor],
-                     [console_ipy, console_int]],
-                    # column 1
-                    [[explorer_variable, plots, history, outline,
-                      finder] + plugins,
-                     [explorer_file, explorer_project, help_plugin, helper]]
-                    ],
-                    'width fraction': [0.55,            # column 0 width
-                                       0.45],           # column 1 width
-                    'height fraction': [[0.55, 0.45],   # column 0, row heights
-                                        [0.55, 0.45]],  # column 1, row heights
-                    'hidden widgets': [outline],
-                    'hidden toolbars': [],
-                    }
+        s_layout = {
+            'widgets': [
+                # Column 0
+                [[explorer_project]],
+                # Column 1
+                [[editor]],
+                # Column 2
+                [[outline]],
+                # Column 3
+                [[help_plugin, explorer_variable, plots,     # Row 0
+                  helper, explorer_file, finder] + plugins,
+                 [console_int, console_ipy, history]]        # Row 1
+                ],
+            'width fraction': [0.05,            # Column 0 width
+                               0.55,            # Column 1 width
+                               0.05,            # Column 2 width
+                               0.45],           # Column 3 width
+            'height fraction': [[1.0],          # Column 0, row heights
+                                [1.0],          # Column 1, row heights
+                                [1.0],          # Column 2, row heights
+                                [0.46, 0.54]],  # Column 3, row heights
+            'hidden widgets': [outline] + global_hidden_widgets,
+            'hidden toolbars': [],
+        }
+
+        # RStudio
+        r_layout = {
+            'widgets': [
+                # column 0
+                [[editor],                            # Row 0
+                 [console_ipy, console_int]],         # Row 1
+                # column 1
+                [[explorer_variable, plots, history,  # Row 0
+                  outline, finder] + plugins,
+                 [explorer_file, explorer_project,    # Row 1
+                  help_plugin, helper]]
+                ],
+            'width fraction': [0.55,            # Column 0 width
+                               0.45],           # Column 1 width
+            'height fraction': [[0.55, 0.45],   # Column 0, row heights
+                                [0.55, 0.45]],  # Column 1, row heights
+            'hidden widgets': [outline] + global_hidden_widgets,
+            'hidden toolbars': [],
+        }
+
         # Matlab
-        m_layout = {'widgets': [
-                    # column 0
-                    [[explorer_file, explorer_project],
-                     [outline]],
-                    # column 1
-                    [[editor],
-                     [console_ipy, console_int]],
-                    # column 2
-                    [[explorer_variable, plots, finder] + plugins,
-                     [history, help_plugin, helper]]
-                    ],
-                    'width fraction': [0.20,            # column 0 width
-                                       0.40,            # column 1 width
-                                       0.40],           # column 2 width
-                    'height fraction': [[0.55, 0.45],   # column 0, row heights
-                                        [0.55, 0.45],   # column 1, row heights
-                                        [0.55, 0.45]],  # column 2, row heights
-                    'hidden widgets': [],
-                    'hidden toolbars': [],
-                    }
+        m_layout = {
+            'widgets': [
+                # column 0
+                [[explorer_file, explorer_project],
+                 [outline]],
+                # column 1
+                [[editor],
+                 [console_ipy, console_int]],
+                # column 2
+                [[explorer_variable, plots, finder] + plugins,
+                 [history, help_plugin, helper]]
+                ],
+            'width fraction': [0.10,            # Column 0 width
+                               0.45,            # Column 1 width
+                               0.45],           # Column 2 width
+            'height fraction': [[0.55, 0.45],   # Column 0, row heights
+                                [0.55, 0.45],   # Column 1, row heights
+                                [0.55, 0.45]],  # Column 2, row heights
+            'hidden widgets': global_hidden_widgets,
+            'hidden toolbars': [],
+        }
+
         # Vertically split
-        v_layout = {'widgets': [
-                    # column 0
-                    [[editor],
-                     [console_ipy, console_int, explorer_file,
-                      explorer_project, help_plugin, explorer_variable, plots,
-                      history, outline, finder, helper] + plugins]
-                    ],
-                    'width fraction': [1.0],            # column 0 width
-                    'height fraction': [[0.55, 0.45]],  # column 0, row heights
-                    'hidden widgets': [outline],
-                    'hidden toolbars': [],
-                    }
+        v_layout = {
+            'widgets': [
+                # column 0
+                [[editor],                                  # Row 0
+                 [console_ipy, console_int, explorer_file,  # Row 1
+                  explorer_project, help_plugin, explorer_variable, plots,
+                  history, outline, finder, helper] + plugins]
+                ],
+            'width fraction': [1.0],            # Column 0 width
+            'height fraction': [[0.55, 0.45]],  # Column 0, row heights
+            'hidden widgets': [outline] + global_hidden_widgets,
+            'hidden toolbars': [],
+        }
+
         # Horizontally split
-        h_layout = {'widgets': [
-                    # column 0
-                    [[editor]],
-                    # column 1
-                    [[console_ipy, console_int, explorer_file,
-                      explorer_project, help_plugin, explorer_variable, plots,
-                      history, outline, finder, helper] + plugins]
-                    ],
-                    'width fraction': [0.55,      # column 0 width
-                                       0.45],     # column 1 width
-                    'height fraction': [[1.0],    # column 0, row heights
-                                        [1.0]],   # column 1, row heights
-                    'hidden widgets': [outline],
-                    'hidden toolbars': []
-                    }
+        h_layout = {
+            'widgets': [
+                # column 0
+                [[editor]],                                 # Row 0
+                # column 1
+                [[console_ipy, console_int, explorer_file,  # Row 0
+                  explorer_project, help_plugin, explorer_variable, plots,
+                  history, outline, finder, helper] + plugins]
+                ],
+            'width fraction': [0.55,      # Column 0 width
+                               0.45],     # Column 1 width
+            'height fraction': [[1.0],    # Column 0, row heights
+                                [1.0]],   # Column 1, row heights
+            'hidden widgets': [outline] + global_hidden_widgets,
+            'hidden toolbars': []
+        }
 
         # Layout selection
-        layouts = {'default': s_layout,
-                   RSTUDIO: r_layout,
-                   MATLAB: m_layout,
-                   VERTICAL: v_layout,
-                   HORIZONTAL: h_layout}
+        layouts = {
+            'default': s_layout,
+            RSTUDIO: r_layout,
+            MATLAB: m_layout,
+            VERTICAL: v_layout,
+            HORIZONTAL: h_layout,
+        }
 
         layout = layouts[index]
 
+        # Remove None from widgets layout
         widgets_layout = layout['widgets']
+        widgets_layout_clean = []
+        for column in widgets_layout:
+            clean_col = []
+            for row in column:
+                clean_row = [w for w in row if w is not None]
+                if clean_row:
+                    clean_col.append(clean_row)
+            if clean_col:
+                widgets_layout_clean.append(clean_col)
+
+        # Flatten widgets list
         widgets = []
-        for column in widgets_layout :
+        for column in widgets_layout_clean:
             for row in column:
                 for widget in row:
-                    if widget is not None:
-                        widgets.append(widget)
+                    widgets.append(widget)
 
         # Make every widget visible
         for widget in widgets:
             widget.toggle_view(True)
-            action = widget.toggle_view_action
-            action.setChecked(widget.dockwidget.isVisible())
-
-        # Set the widgets horizontally
-        for i in range(len(widgets) - 1):
-            first, second = widgets[i], widgets[i+1]
-            if first is not None and second is not None:
-                self.splitDockWidget(first.dockwidget, second.dockwidget,
-                                     Qt.Horizontal)
-
-        # Arrange rows vertically
-        for column in widgets_layout :
-            for i in range(len(column) - 1):
-                first_row, second_row = column[i], column[i+1]
-                if first_row is not None and second_row is not None:
-                    self.splitDockWidget(first_row[0].dockwidget,
-                                         second_row[0].dockwidget,
-                                         Qt.Vertical)
+            widget.toggle_view_action.setChecked(True)
+
+        # We use both directions to ensure proper update when moving from
+        # 'Horizontal Split' to 'Spyder Default'
+        # This also seems to help on random cases where the display seems
+        # 'empty'
+        for direction in (Qt.Vertical, Qt.Horizontal):
+            # Arrange the widgets in one direction
+            for idx in range(len(widgets) - 1):
+                first, second = widgets[idx], widgets[idx+1]
+                if first is not None and second is not None:
+                    self.splitDockWidget(first.dockwidget, second.dockwidget,
+                                         direction)
+
+        # Arrange the widgets in the other direction
+        for column in widgets_layout_clean:
+            for idx in range(len(column) - 1):
+                first_row, second_row = column[idx], column[idx+1]
+                self.splitDockWidget(first_row[0].dockwidget,
+                                     second_row[0].dockwidget,
+                                     Qt.Vertical)
+
         # Tabify
-        for column in widgets_layout :
+        for column in widgets_layout_clean:
             for row in column:
-                for i in range(len(row) - 1):
-                    first, second = row[i], row[i+1]
-                    if first is not None and second is not None:
-                        self.tabify_plugins(first, second)
+                for idx in range(len(row) - 1):
+                    first, second = row[idx], row[idx+1]
+                    self.tabify_plugins(first, second)
 
                 # Raise front widget per row
                 row[0].dockwidget.show()
                 row[0].dockwidget.raise_()
 
+        # Set dockwidget widths
+        width_fractions = layout['width fraction']
+        if len(width_fractions) > 1:
+            _widgets = [col[0][0].dockwidget for col in widgets_layout]
+            self.resizeDocks(_widgets, width_fractions, Qt.Horizontal)
+
+        # Set dockwidget heights
+        height_fractions = layout['height fraction']
+        for idx, column in enumerate(widgets_layout_clean):
+            if len(column) > 1:
+                _widgets = [row[0].dockwidget for row in column]
+                self.resizeDocks(_widgets, height_fractions[idx], Qt.Vertical)
+
         # Hide toolbars
         hidden_toolbars = global_hidden_toolbars + layout['hidden toolbars']
         for toolbar in hidden_toolbars:
@@ -1706,64 +1788,24 @@ def setup_default_layouts(self, index, settings):
                 toolbar.close()
 
         # Hide widgets
-        hidden_widgets = global_hidden_widgets + layout['hidden widgets']
+        hidden_widgets = layout['hidden widgets']
         for widget in hidden_widgets:
             if widget is not None:
                 widget.dockwidget.close()
 
-        # set the width and height
-        self._layout_widget_info = []
-        width, height = self.window_size.width(), self.window_size.height()
-
-        # fix column width
-#        for c in range(len(widgets_layout)):
-#            widget = widgets_layout[c][0][0].dockwidget
-#            min_width, max_width = widget.minimumWidth(), widget.maximumWidth()
-#            info = {'widget': widget,
-#                    'min width': min_width,
-#                    'max width': max_width}
-#            self._layout_widget_info.append(info)
-#            new_width = int(layout['width fraction'][c] * width * 0.95)
-#            widget.setMinimumWidth(new_width)
-#            widget.setMaximumWidth(new_width)
-#            widget.updateGeometry()
-
-        # fix column height
-        for c, column in enumerate(widgets_layout):
-            for r in range(len(column) - 1):
-                widget = column[r][0]
-                dockwidget = widget.dockwidget
-                dock_min_h = dockwidget.minimumHeight()
-                dock_max_h = dockwidget.maximumHeight()
-                info = {'widget': widget,
-                        'dock min height': dock_min_h,
-                        'dock max height': dock_max_h}
-                self._layout_widget_info.append(info)
-                # The 0.95 factor is to adjust height based on usefull
-                # estimated area in the window
-                new_height = int(layout['height fraction'][c][r]*height*0.95)
-                dockwidget.setMinimumHeight(new_height)
-                dockwidget.setMaximumHeight(new_height)
-
-        self._custom_layout_timer = QTimer(self)
-        self._custom_layout_timer.timeout.connect(self.layout_fix_timer)
-        self._custom_layout_timer.setSingleShot(True)
-        self._custom_layout_timer.start(5000)
-
-    def layout_fix_timer(self):
-        """Fixes the height of docks after a new layout is set."""
-        info = self._layout_widget_info
-        for i in info:
-            dockwidget = i['widget'].dockwidget
-            if 'dock min width' in i:
-                dockwidget.setMinimumWidth(i['dock min width'])
-                dockwidget.setMaximumWidth(i['dock max width'])
-            if 'dock min height' in i:
-                dockwidget.setMinimumHeight(i['dock min height'])
-                dockwidget.setMaximumHeight(i['dock max height'])
-            dockwidget.updateGeometry()
+        if first_spyder_run:
+            self.first_spyder_run = False
+        else:
+            self.setMinimumWidth(min_width)
+            self.setMaximumWidth(max_width)
+
+            if not (self.isMaximized() or self.maximized_flag):
+                self.showMaximized()
 
         self.setUpdatesEnabled(True)
+        self.sig_layout_setup_ready.emit(layout)
+
+        return layout
 
     @Slot()
     def toggle_previous_layout(self):


Test patch:
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -156,7 +156,11 @@ def main_window(request):
 
     # Check if we need to use introspection in a given test
     # (it's faster and less memory consuming not to use it!)
-    use_introspection = request.node.get_marker('use_introspection')
+    try:
+        use_introspection = request.node.get_marker('use_introspection')
+    except AttributeError:
+        use_introspection = False
+
     if use_introspection:
         os.environ['SPY_TEST_USE_INTROSPECTION'] = 'True'
     else:
@@ -166,7 +170,11 @@ def main_window(request):
             pass
 
     # Only use single_instance mode for tests that require it
-    single_instance = request.node.get_marker('single_instance')
+    try:
+        single_instance = request.node.get_marker('single_instance')
+    except AttributeError:
+        single_instance = False
+
     if single_instance:
         CONF.set('main', 'single_instance', True)
     else:
@@ -1784,5 +1792,34 @@ def test_render_issue():
     assert test_traceback in test_issue_2
 
 
+@pytest.mark.slow
+@flaky(max_runs=3)
+def test_custom_layouts(main_window, qtbot):
+    """Test that layout are showing the expected widgets visible."""
+    mw = main_window
+    mw.first_spyder_run = False
+    prefix = 'window' + '/'
+    settings = mw.load_window_settings(prefix=prefix, default=True)
+
+    # Test layout changes
+    for layout_idx in ['default'] + list(range(4)):
+        with qtbot.waitSignal(mw.sig_layout_setup_ready, timeout=5000):
+            layout = mw.setup_default_layouts(layout_idx, settings=settings)
+
+            with qtbot.waitSignal(None, timeout=500, raising=False):
+                # Add a wait to see changes
+                pass
+
+            widgets_layout = layout['widgets']
+            hidden_widgets = layout['hidden widgets']
+            for column in widgets_layout:
+                for row in column:
+                    for idx, widget in enumerate(row):
+                        if idx == 0:
+                            if widget not in hidden_widgets:
+                                print(widget)  # spyder: test-skip
+                                assert widget.isVisible()
+
+
 if __name__ == "__main__":
     pytest.main()