Repository: spyder-ide/spyder

Issue:
Generating new plots always overrides currently selected plot
Consider the following code:

```
import matplotlib.pyplot as plt, time
for i in range(100):
    plt.figure()
    plt.plot([0, 1, i])
    plt.show()
    time.sleep(0.1)
```

Every time a new plot is generated, it is selected in the plots panel. If you want to look at a plot, you must wait untill all the plots are generated.

It would be great if, when you select a plot, the selected plot is not replaced by a newly plotted plot.


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
@@ -243,6 +243,7 @@ def set_shellwidget(self, shellwidget):
         self.shellwidget = shellwidget
         self.shellwidget.set_mute_inline_plotting(self.mute_inline_plotting)
         shellwidget.sig_new_inline_figure.connect(self._handle_new_figure)
+        shellwidget.executing.connect(self._handle_new_execution)
 
     def _handle_new_figure(self, fig, fmt):
         """
@@ -251,6 +252,10 @@ def _handle_new_figure(self, fig, fmt):
         """
         self.thumbnails_sb.add_thumbnail(fig, fmt)
 
+    def _handle_new_execution(self):
+        """Handle a new execution in the console."""
+        self.thumbnails_sb._first_thumbnail_shown = False
+
     # ---- Toolbar Handlers
     def zoom_in(self):
         """Zoom the figure in by a single step in the figure viewer."""
@@ -587,7 +592,8 @@ def __init__(self, figure_viewer, parent=None, background_color=None):
         # is added to the scrollarea, we need to do it instead in a slot
         # connected to the scrollbar's rangeChanged signal.
         # See spyder-ide/spyder#10914 for more details.
-        self._new_thumbnail_added = False
+        self._scroll_to_last_thumbnail = False
+        self._first_thumbnail_shown = False
         self.scrollarea.verticalScrollBar().rangeChanged.connect(
             self._scroll_to_newest_item)
 
@@ -768,6 +774,15 @@ def add_thumbnail(self, fig, fmt):
         """
         Add a new thumbnail to that thumbnail scrollbar.
         """
+        # Always stick at end for the first thumbnail
+        is_first = not self._first_thumbnail_shown
+
+        # Stick at end if we are already at the end
+        stick_at_end = False
+        sb = self.scrollarea.verticalScrollBar()
+        if sb.value() == sb.maximum():
+            stick_at_end = True
+
         thumbnail = FigureThumbnail(
             parent=self, background_color=self.background_color)
         thumbnail.canvas.load_figure(fig, fmt)
@@ -777,16 +792,28 @@ def add_thumbnail(self, fig, fmt):
         thumbnail.sig_context_menu_requested.connect(
             lambda point: self.show_context_menu(point, thumbnail))
         self._thumbnails.append(thumbnail)
-        self._new_thumbnail_added = True
+        self._scroll_to_last_thumbnail = True
+        self._first_thumbnail_shown = True
 
         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)
+
+        # Only select a new thumbnail if the last one was selected
+        select_last = (
+            len(self._thumbnails) < 2
+            or self.current_thumbnail == self._thumbnails[-2]
+            or is_first
+        )
+        if select_last:
+            self.set_current_thumbnail(thumbnail)
 
         thumbnail.show()
         self._setup_thumbnail_size(thumbnail)
 
+        if not is_first and not stick_at_end:
+            self._scroll_to_last_thumbnail = False
+
     def remove_current_thumbnail(self):
         """Remove the currently selected thumbnail."""
         if self.current_thumbnail is not None:
@@ -912,8 +939,8 @@ def _scroll_to_newest_item(self, vsb_min, vsb_max):
         Note that this method is called each time the rangeChanged signal
         is emitted by the scrollbar.
         """
-        if self._new_thumbnail_added:
-            self._new_thumbnail_added = False
+        if self._scroll_to_last_thumbnail:
+            self._scroll_to_last_thumbnail = False
             self.scrollarea.verticalScrollBar().setValue(vsb_max)
 
     # ---- ScrollBar Handlers


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
@@ -2049,6 +2049,81 @@ def test_plots_plugin(main_window, qtbot, tmpdir, mocker):
     assert compare_images(ipython_figname, plots_figname, 0.1) is None
 
 
+def test_plots_scroll(main_window, qtbot):
+    """Test plots plugin scrolling"""
+    CONF.set('plots', 'mute_inline_plotting', True)
+    shell = main_window.ipyconsole.get_current_shellwidget()
+    figbrowser = main_window.plots.current_widget()
+
+    # Wait until the window is fully up.
+    qtbot.waitUntil(
+        lambda: shell.spyder_kernel_ready and shell._prompt_html is not None,
+        timeout=SHELL_TIMEOUT)
+
+    # Generate a plot inline.
+    with qtbot.waitSignal(shell.executed, timeout=SHELL_TIMEOUT):
+        shell.execute(("import matplotlib.pyplot as plt\n"
+                       "fig = plt.plot([1, 2, 3, 4], '.')\n"))
+
+    # Make sure the plot is selected
+    sb = figbrowser.thumbnails_sb
+    assert len(sb._thumbnails) == 1
+    assert sb._thumbnails[-1] == sb.current_thumbnail
+
+    # Generate 4 more plots
+    with qtbot.waitSignal(shell.executed, timeout=SHELL_TIMEOUT):
+        shell.execute(
+            "for i in range(4):\n"
+            "    plt.figure()\n"
+            "    plt.plot([1, 2, 3, 4], '.')")
+
+    # we now have 5 plots and the last one is selected
+    assert len(sb._thumbnails) == 5
+    assert sb._thumbnails[-1] == sb.current_thumbnail
+
+    # Generate 20 plots
+    with qtbot.waitSignal(shell.executed, timeout=SHELL_TIMEOUT):
+        shell.execute(
+            "for i in range(20):\n"
+            "    plt.figure()\n"
+            "    plt.plot([1, 2, 3, 4], '.')")
+
+    # Make sure we scrolled down
+    scrollbar = sb.scrollarea.verticalScrollBar()
+    assert len(sb._thumbnails) == 25
+    assert sb._thumbnails[-1] == sb.current_thumbnail
+    assert scrollbar.value() == scrollbar.maximum()
+
+    # Generate 20 plots and select a plot in the middle
+    with qtbot.waitSignal(shell.executed, timeout=SHELL_TIMEOUT):
+        shell.execute(
+            "import time\n"
+            "for i in range(20):\n"
+            "    plt.figure()\n"
+            "    plt.plot([1, 2, 3, 4], '.')\n"
+            "    plt.show()\n"
+            "    time.sleep(.1)")
+        qtbot.waitUntil(lambda: sb._first_thumbnail_shown,
+                        timeout=SHELL_TIMEOUT)
+        sb.set_current_index(5)
+        scrollbar.setValue(scrollbar.minimum())
+
+    # make sure we didn't scroll to the end and a new thumbnail was not selected
+    assert len(sb._thumbnails) == 45
+    assert sb._thumbnails[-1] != sb.current_thumbnail
+    assert scrollbar.value() != scrollbar.maximum()
+
+    # One more plot
+    with qtbot.waitSignal(shell.executed, timeout=SHELL_TIMEOUT):
+        shell.execute(("fig = plt.plot([1, 2, 3, 4], '.')\n"))
+
+    # Make sure everything scrolled at the end
+    assert len(sb._thumbnails) == 46
+    assert sb._thumbnails[-1] == sb.current_thumbnail
+    assert scrollbar.value() == scrollbar.maximum()
+    CONF.set('plots', 'mute_inline_plotting', False)
+
+
 @flaky(max_runs=3)
 @pytest.mark.skipif(
     (parse_version(ipy_release.version) >= parse_version('7.23.0') and