Repository: spyder-ide/spyder

Issue:
Separator on plots plugin acts more like a button than a resize area

The separator has the appearance of allowing the user to change the width gradually, but currently the behavior is more of something being toggled (a button instead of a draggable area)

![plots](https://user-images.githubusercontent.com/3627835/60556568-99ab8f00-9d07-11e9-935d-6781402304c3.gif)



Discussion:

Resolving patch:
diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py
--- a/spyder/plugins/plots/widgets/figurebrowser.py
+++ b/spyder/plugins/plots/widgets/figurebrowser.py
@@ -13,6 +13,7 @@
 # ---- Standard library imports
 from __future__ import division
 import os.path as osp
+import sys
 
 # ---- Third library imports
 from qtconsole.svg import svg_to_image, svg_to_clipboard
@@ -21,8 +22,8 @@
 from qtpy.QtGui import QPixmap, QPainter, QKeySequence
 from qtpy.QtWidgets import (QApplication, QHBoxLayout, QMenu,
                             QVBoxLayout, QWidget, QGridLayout, QFrame,
-                            QScrollArea, QPushButton, QScrollBar, QSizePolicy,
-                            QSpinBox, QSplitter, QStyle)
+                            QScrollArea, QPushButton, QScrollBar, QSpinBox,
+                            QSplitter, QStyle)
 
 # ---- Local library imports
 from spyder.config.base import _
@@ -32,7 +33,7 @@
                                     create_toolbutton, create_plugin_layout,
                                     MENU_SEPARATOR)
 from spyder.utils.misc import getcwd_or_home
-from spyder.config.gui import config_shortcut, get_shortcut
+from spyder.config.gui import config_shortcut, get_shortcut, is_dark_interface
 
 
 def save_figure_tofile(fig, fmt, fname):
@@ -512,7 +513,7 @@ def scale_image(self):
             new_height = int(fheight * self._scalestep ** self._scalefactor)
 
         # Auto fit plotting
-        # Scale the image to fit figviewer size while respect the ratio
+        # Scale the image to fit the figviewer size while respecting the ratio.
         else:
             size = self.size()
             style = self.style()
@@ -563,6 +564,7 @@ class ThumbnailScrollBar(QFrame):
     that controls what is displayed in the FigureViewer.
     """
     redirect_stdio = Signal(bool)
+    _min_scrollbar_width = 100
 
     def __init__(self, figure_viewer, parent=None, background_color=None):
         super(ThumbnailScrollBar, self).__init__(parent)
@@ -577,7 +579,6 @@ def setup_gui(self):
         scrollarea = self.setup_scrollarea()
         up_btn, down_btn = self.setup_arrow_buttons()
 
-        self.setFixedWidth(150)
         layout = QVBoxLayout(self)
         layout.setContentsMargins(0, 0, 0, 0)
         layout.setSpacing(0)
@@ -590,23 +591,25 @@ def setup_scrollarea(self):
         self.view = QWidget()
 
         self.scene = QGridLayout(self.view)
-        self.scene.setColumnStretch(0, 100)
-        self.scene.setColumnStretch(2, 100)
+        self.scene.setContentsMargins(0, 0, 0, 0)
 
         self.scrollarea = QScrollArea()
         self.scrollarea.setWidget(self.view)
         self.scrollarea.setWidgetResizable(True)
         self.scrollarea.setFrameStyle(0)
+        self.scrollarea.setViewportMargins(2, 2, 2, 2)
         self.scrollarea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
         self.scrollarea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
-        self.scrollarea.setSizePolicy(QSizePolicy(QSizePolicy.Ignored,
-                                                  QSizePolicy.Preferred))
+        self.scrollarea.setMinimumWidth(self._min_scrollbar_width)
 
-        # Set the vertical scrollbar explicitely :
+        # Set the vertical scrollbar explicitely.
         # This is required to avoid a "RuntimeError: no access to protected
         # functions or signals for objects not created from Python" in Linux.
         self.scrollarea.setVerticalScrollBar(QScrollBar())
 
+        # Install an event filter on the scrollbar.
+        self.scrollarea.installEventFilter(self)
+
         return self.scrollarea
 
     def setup_arrow_buttons(self):
@@ -634,6 +637,15 @@ def set_figureviewer(self, figure_viewer):
         """Set the bamespace for the FigureViewer."""
         self.figure_viewer = figure_viewer
 
+    def eventFilter(self, widget, event):
+        """
+        An event filter to trigger an update of the thumbnails size so that
+        their width fit that of the scrollarea.
+        """
+        if event.type() == QEvent.Resize:
+            self._update_thumbnail_size()
+        return super(ThumbnailScrollBar, self).eventFilter(widget, event)
+
     # ---- Save Figure
     def save_all_figures_as(self):
         """Save all the figures to a file."""
@@ -685,34 +697,71 @@ def save_figure_as(self, fig, fmt):
             save_figure_tofile(fig, fmt, fname)
 
     # ---- Thumbails Handlers
-    def add_thumbnail(self, fig, fmt):
-        thumbnail = FigureThumbnail(background_color=self.background_color)
-        thumbnail.canvas.load_figure(fig, fmt)
+    def _calculate_figure_canvas_width(self, thumbnail):
+        """
+        Calculate the witdh the thumbnail's figure canvas need to have for the
+        thumbnail to fit the scrollarea.
+        """
+        extra_padding = 10 if sys.platform == 'darwin' else 0
+        figure_canvas_width = (
+            self.scrollarea.width() -
+            2 * self.lineWidth() -
+            self.scrollarea.viewportMargins().left() -
+            self.scrollarea.viewportMargins().right() -
+            thumbnail.savefig_btn.width() -
+            thumbnail.layout().spacing() - extra_padding
+            )
+        if is_dark_interface():
+            # This is required to take into account some hard-coded padding
+            # and margin in qdarkstyle.
+            figure_canvas_width = figure_canvas_width - 6
+        return figure_canvas_width
 
-        # Scale the thumbnail size, while respecting the figure
-        # dimension ratio.
-        fwidth = thumbnail.canvas.fwidth
-        fheight = thumbnail.canvas.fheight
+    def _setup_thumbnail_size(self, thumbnail):
+        """
+        Scale the thumbnail's canvas size so that it fits the thumbnail
+        scrollbar's width.
+        """
+        max_canvas_size = self._calculate_figure_canvas_width(thumbnail)
+        thumbnail.scale_canvas_size(max_canvas_size)
 
-        max_length = 100
-        if fwidth/fheight > 1:
-            canvas_width = max_length
-            canvas_height = canvas_width / fwidth * fheight
-        else:
-            canvas_height = max_length
-            canvas_width = canvas_height / fheight * fwidth
-        thumbnail.canvas.setFixedSize(int(canvas_width), int(canvas_height))
+    def _update_thumbnail_size(self):
+        """
+        Update the thumbnails size so that their width fit that of
+        the scrollarea.
+        """
+        # NOTE: We hide temporarily the thumbnails to prevent a repaint of
+        # each thumbnail as soon as their size is updated in the loop, which
+        # causes some flickering of the thumbnail scrollbar resizing animation.
+        # Once the size of all the thumbnails has been updated, we show them
+        # back so that they are repainted all at once instead of one after the
+        # other. This is just a trick to make the resizing animation of the
+        # thumbnail scrollbar look smoother.
+        self.view.hide()
+        for thumbnail in self._thumbnails:
+            self._setup_thumbnail_size(thumbnail)
+        self.view.show()
 
+    def add_thumbnail(self, fig, fmt):
+        """
+        Add a new thumbnail to that thumbnail scrollbar.
+        """
+        thumbnail = FigureThumbnail(
+            parent=self, background_color=self.background_color)
+        thumbnail.canvas.load_figure(fig, fmt)
         thumbnail.sig_canvas_clicked.connect(self.set_current_thumbnail)
         thumbnail.sig_remove_figure.connect(self.remove_thumbnail)
         thumbnail.sig_save_figure.connect(self.save_figure_as)
         self._thumbnails.append(thumbnail)
 
-        self.scene.setRowStretch(self.scene.rowCount()-1, 0)
-        self.scene.addWidget(thumbnail, self.scene.rowCount()-1, 1)
+        self.scene.setRowStretch(self.scene.rowCount() - 1, 0)
+        self.scene.addWidget(thumbnail, self.scene.rowCount() - 1, 0)
         self.scene.setRowStretch(self.scene.rowCount(), 100)
         self.set_current_thumbnail(thumbnail)
 
+        thumbnail.show()
+        self._setup_thumbnail_size(thumbnail)
+
     def remove_current_thumbnail(self):
         """Remove the currently selected thumbnail."""
         if self.current_thumbnail is not None:
@@ -833,12 +882,10 @@ def setup_gui(self):
         """Setup the main layout of the widget."""
         layout = QGridLayout(self)
         layout.setContentsMargins(0, 0, 0, 0)
-        layout.addWidget(self.canvas, 0, 1)
-        layout.addLayout(self.setup_toolbar(), 0, 3, 2, 1)
-
-        layout.setColumnStretch(0, 100)
-        layout.setColumnStretch(2, 100)
+        layout.addWidget(self.canvas, 0, 0, Qt.AlignCenter)
+        layout.addLayout(self.setup_toolbar(), 0, 1, 2, 1)
         layout.setRowStretch(1, 100)
+        layout.setSizeConstraint(layout.SetFixedSize)
 
     def setup_toolbar(self):
         """Setup the toolbar."""
@@ -871,6 +918,22 @@ def highlight_canvas(self, highlight):
         else:
             self.canvas.setStyleSheet("FigureCanvas{}")
 
+    def scale_canvas_size(self, max_canvas_size):
+        """
+        Scale this thumbnail canvas size, while respecting its associated
+        figure dimension ratio.
+        """
+        fwidth = self.canvas.fwidth
+        fheight = self.canvas.fheight
+        if fwidth / fheight > 1:
+            canvas_width = max_canvas_size
+            canvas_height = canvas_width / fwidth * fheight
+        else:
+            canvas_height = max_canvas_size
+            canvas_width = canvas_height / fheight * fwidth
+        self.canvas.setFixedSize(int(canvas_width), int(canvas_height))
+        self.layout().setColumnMinimumWidth(0, max_canvas_size)
+
     def eventFilter(self, widget, event):
         """
         A filter that is used to send a signal when the figure canvas is


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
@@ -1853,6 +1853,8 @@ def test_render_issue():
 
 @pytest.mark.slow
 @flaky(max_runs=3)
+@pytest.mark.skipif(
+    sys.platform.startswith('linux'), reason="It segfaults on Linux")
 def test_custom_layouts(main_window, qtbot):
     """Test that layout are showing the expected widgets visible."""
     mw = main_window
diff --git a/spyder/plugins/plots/widgets/tests/test_plots_widgets.py b/spyder/plugins/plots/widgets/tests/test_plots_widgets.py
--- a/spyder/plugins/plots/widgets/tests/test_plots_widgets.py
+++ b/spyder/plugins/plots/widgets/tests/test_plots_widgets.py
@@ -419,10 +419,10 @@ def test_autofit_figure_viewer(figbrowser, tmpdir, fmt):
         new_height = int(height)
         new_width = int(height / fheight * fwidth)
 
-    assert (figbrowser.zoom_disp.value() ==
-            np.round(figcanvas.width() / fwidth * 100))
     assert figcanvas.width() == new_width
     assert figcanvas.height() == new_height
+    assert (figbrowser.zoom_disp.value() ==
+            round(figcanvas.width() / fwidth * 100))
 
 
 if __name__ == "__main__":