Repository: spyder-ide/spyder

Issue:
Some issues with the Plot viewer
<!--- **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! --->

## Problem Description

I will group a couple of issues I see currently with the `Plots` pane:

1. Images on retina displays look distorted. (maybe this thread helps https://gist.github.com/minrk/3301035)
2. There seems to be some horizontal scrolling with the list of images to the right:
![problem1](https://user-images.githubusercontent.com/3627835/57983399-fdf3e700-7a16-11e9-820e-c83ebd9bdcc3.gif)
3. The down arrow should allow moving between images, instead it only move the list downwards
![problem2](https://user-images.githubusercontent.com/3627835/57983424-457a7300-7a17-11e9-8598-a5afcd20e188.gif)







Discussion:
@jnsebgosselin any thoughts here?
Hi @goanpeca. Thanks for taking the time to report these issues.

> Images on retina displays look distorted.

I will try to take a look at this. However I don't have access to a Mac. Is this doing this also with any high resolution monitors?

> There seems to be some horizontal scrolling with the list of images to the right

I will take care of this, I know what needs to be done.

> The down arrow should allow moving between images, instead it only move the list downwards

I don't know about this. The down/up arrows act like scrollbar buttons, so it makes sense to me that clicking on them doesn't change the current selected image.

This is similar to the caret not changing line when we use the arrows on the Editor's vertical scrollbar. 

Besides, There is already the `Previous Figure` and `Next Figure`  buttons and their associated shortcut to navigate/cycle through the images.

But that being said, if you still think it is a change that should be implemented, I can do it.


> Images on retina displays look distorted.

@goanpeca is this related to this https://github.com/spyder-ide/spyder/issues/9148?
Yes, I think they are related
I'm also having what appears to be an issue with the quality of the images plotted on the Plots window. See on the image below that the letters on the charts are blurry. If I save the image using the "Save" icon on the navigation bar, the quality is OK. This image was a snap from a 4K monitor (Windows 10). Note that I have "Enable auto high DPI scaling" checked (which indeed helped with the size of menus, icons, etc).
![Annotation 2019-11-11 145755](https://user-images.githubusercontent.com/57544001/68617995-64bdfe80-0496-11ea-95ed-2a9c392cab43.jpg)

@RIM-LLC, solving the problem you mention in the Plots pane is harder than you think (although it seems simple). We need to detect the screen resolution programatically and inform the IPython console if it's high or low res for it to adjust the plots size.

We'll try to address that for version 4.1, to be released in January 2020.
I would love to see a proper standard scroll bar and get rid of the up/down buttons. If a lot of plots have been accumulated during the day it is very hard to scroll this list. The save- and delete-icons could be moved into the context menu to remove needless clutter from the UI, the delete key should delete the selected plot, arrow up/down keys should move the selection and not scroll the list, shift-up/down should select multiple plots. A normal list widget with standard mouse and keyboard behavior would do all this out of the box already and would be much less confusing!
> 
> 
> I would love to see a proper standard scroll bar and get rid of the up/down buttons. If a lot of plots have been accumulated during the day it is very hard to scroll this list. The save- and delete-icons could be moved into the context menu to remove needless clutter from the UI. Arrow up/down keys should move the selection and not scroll the list. A normal list widget with standard mouse and keyboard behavior would do a better job and would be less confusing.

After playing with the plot plugin for some time now, I agree with this. @goanpeca what do you think?

The current implementation that I made was based on [Rodeo](https://rodeo.yhat.com/) design, but I think this is not ideal.
I agree! Go for it
> 
> 
> I agree! Go for it

Master or 4.x branch?
@ccordoba12 ?
4.x is fine for me because it solves very important usability issues.

Resolving patch:
diff --git a/spyder/config/main.py b/spyder/config/main.py
--- a/spyder/config/main.py
+++ b/spyder/config/main.py
@@ -469,6 +469,12 @@
               'plots/copy': 'Ctrl+C',
               'plots/previous figure': 'Ctrl+PgUp',
               'plots/next figure': 'Ctrl+PgDown',
+              'plots/save': 'Ctrl+S',
+              'plots/save all': 'Ctrl+Alt+S',
+              'plots/close': 'Ctrl+W',
+              'plots/close all': 'Ctrl+Shift+W',
+              'plots/zoom in': "Ctrl++",
+              'plots/zoom out': "Ctrl+-",
               # ---- In widgets/explorer ----
               'explorer/copy file': 'Ctrl+C',
               'explorer/paste file': 'Ctrl+V',
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
@@ -16,23 +16,24 @@
 import sys
 
 # ---- Third library imports
+import qdarkstyle
 from qtconsole.svg import svg_to_image, svg_to_clipboard
 from qtpy.compat import getsavefilename, getexistingdirectory
 from qtpy.QtCore import Qt, Signal, QRect, QEvent, QPoint, QSize, QTimer, Slot
 from qtpy.QtGui import QPixmap, QPainter, QKeySequence
 from qtpy.QtWidgets import (QApplication, QHBoxLayout, QMenu,
                             QVBoxLayout, QWidget, QGridLayout, QFrame,
-                            QScrollArea, QPushButton, QScrollBar, QSpinBox,
-                            QSplitter, QStyle)
+                            QScrollArea, QScrollBar, QSpinBox, QSplitter,
+                            QStyle)
 
 # ---- Local library imports
 from spyder.config.base import _
 from spyder.config.manager import CONF
 from spyder.py3compat import is_unicode, to_text_string
 from spyder.utils import icon_manager as ima
-from spyder.utils.qthelpers import (add_actions, create_action,
-                                    create_toolbutton, create_plugin_layout,
-                                    MENU_SEPARATOR)
+from spyder.utils.qthelpers import (
+    add_actions, add_shortcut_to_tooltip, create_action, create_toolbutton,
+    create_plugin_layout, MENU_SEPARATOR)
 from spyder.utils.misc import getcwd_or_home
 from spyder.config.gui import is_dark_interface
 
@@ -98,6 +99,15 @@ def __init__(self, parent=None, options_button=None, plugin_actions=[],
         self.plugin_actions = plugin_actions
         self.shortcuts = self.create_shortcuts()
 
+    def eventFilter(self, obj, event):
+        """
+        An event filter to add the shortcut associated with a given
+        toolbutton to its tooltip.
+        """
+        if event.type() == QEvent.ToolTip:
+            add_shortcut_to_tooltip(obj, *obj.shortcut_data)
+        return False
+
     def setup(self, mute_inline_plotting=None, show_plot_outline=None,
               auto_fit_plotting=None):
         """Setup the figure browser with provided settings."""
@@ -113,13 +123,14 @@ def setup(self, mute_inline_plotting=None, show_plot_outline=None,
             self.auto_fit_action.setChecked(auto_fit_plotting)
             return
 
+        # Setup the figure viewer.
         self.figviewer = FigureViewer(background_color=self.background_color)
-        self.figviewer.setStyleSheet("FigureViewer{"
-                                     "border: 1px solid lightgrey;"
-                                     "border-top-width: 0px;"
-                                     "border-bottom-width: 0px;"
-                                     "border-left-width: 0px;"
-                                     "}")
+        self.figviewer.figcanvas.sig_save_fig_requested.connect(
+            self.save_figure)
+        self.figviewer.figcanvas.sig_clear_fig_requested.connect(
+            self.close_figure)
+
+        # Setup the thumbnail scrollbar.
         self.thumbnails_sb = ThumbnailScrollBar(
             self.figviewer, background_color=self.background_color)
 
@@ -144,60 +155,75 @@ def setup(self, mute_inline_plotting=None, show_plot_outline=None,
         self.setLayout(layout)
 
     def setup_toolbar(self):
-        """Setup the toolbar"""
-        savefig_btn = create_toolbutton(
-                self, icon=ima.icon('filesave'),
-                tip=_("Save Image As..."),
-                triggered=self.save_figure)
+        """Setup the toolbar."""
+        self.savefig_btn = create_toolbutton(
+            self, icon=ima.icon('filesave'),
+            tip=_("Save plot as..."),
+            triggered=self.save_figure)
+        self.savefig_btn.shortcut_data = ('plots', 'save')
+        self.savefig_btn.installEventFilter(self)
 
         saveall_btn = create_toolbutton(
-                self, icon=ima.icon('save_all'),
-                tip=_("Save All Images..."),
-                triggered=self.save_all_figures)
+            self, icon=ima.icon('save_all'),
+            tip=_("Save all plots..."),
+            triggered=self.save_all_figures)
+        saveall_btn.shortcut_data = ('plots', 'save all')
+        saveall_btn.installEventFilter(self)
 
         copyfig_btn = create_toolbutton(
             self, icon=ima.icon('editcopy'),
-            tip=(_("Copy plot to clipboard as image (%s)") %
-                 CONF.get_shortcut('plots', 'copy')),
+            tip=_("Copy plot to clipboard as image"),
             triggered=self.copy_figure)
+        copyfig_btn.shortcut_data = ('plots', 'copy')
+        copyfig_btn.installEventFilter(self)
 
-        closefig_btn = create_toolbutton(
-                self, icon=ima.icon('editclear'),
-                tip=_("Remove image"),
-                triggered=self.close_figure)
+        self.closefig_btn = create_toolbutton(
+            self, icon=ima.icon('editclear'),
+            tip=_("Remove plot"),
+            triggered=self.close_figure)
+        self.closefig_btn.shortcut_data = ('plots', 'close')
+        self.closefig_btn.installEventFilter(self)
 
         closeall_btn = create_toolbutton(
-                self, icon=ima.icon('filecloseall'),
-                tip=_("Remove all images from the explorer"),
-                triggered=self.close_all_figures)
+            self, icon=ima.icon('filecloseall'),
+            tip=_("Remove all plots"),
+            triggered=self.close_all_figures)
+        closeall_btn.shortcut_data = ('plots', 'close all')
+        closeall_btn.installEventFilter(self)
 
-        vsep1 = QFrame()
-        vsep1.setFrameStyle(53)
+        separator1 = QFrame()
+        separator1.setFrameStyle(QFrame.VLine | QFrame.Sunken)
 
         goback_btn = create_toolbutton(
-                self, icon=ima.icon('ArrowBack'),
-                tip=_("Previous figure ({})").format(
-                      CONF.get_shortcut('plots', 'previous figure')),
-                triggered=self.go_previous_thumbnail)
+            self, icon=ima.icon('ArrowBack'),
+            tip=_("Previous plot"),
+            triggered=self.go_previous_thumbnail)
+        goback_btn.shortcut_data = ('plots', 'previous figure')
+        goback_btn.installEventFilter(self)
 
         gonext_btn = create_toolbutton(
-                self, icon=ima.icon('ArrowForward'),
-                tip=_("Next figure ({})").format(
-                      CONF.get_shortcut('plots', 'next figure')),
-                triggered=self.go_next_thumbnail)
+            self, icon=ima.icon('ArrowForward'),
+            tip=_("Next plot"),
+            triggered=self.go_next_thumbnail)
+        gonext_btn.shortcut_data = ('plots', 'next figure')
+        gonext_btn.installEventFilter(self)
 
-        vsep2 = QFrame()
-        vsep2.setFrameStyle(53)
+        separator2 = QFrame()
+        separator2.setFrameStyle(QFrame.VLine | QFrame.Sunken)
 
         self.zoom_out_btn = create_toolbutton(
-                self, icon=ima.icon('zoom_out'),
-                tip=_("Zoom out (Ctrl + mouse-wheel-down)"),
-                triggered=self.zoom_out)
+            self, icon=ima.icon('zoom_out'),
+            tip=_("Zoom out"),
+            triggered=self.zoom_out)
+        self.zoom_out_btn.shortcut_data = ('plots', 'zoom out')
+        self.zoom_out_btn.installEventFilter(self)
 
         self.zoom_in_btn = create_toolbutton(
-                self, icon=ima.icon('zoom_in'),
-                tip=_("Zoom in (Ctrl + mouse-wheel-up)"),
-                triggered=self.zoom_in)
+            self, icon=ima.icon('zoom_in'),
+            tip=_("Zoom in"),
+            triggered=self.zoom_in)
+        self.zoom_in_btn.shortcut_data = ('plots', 'zoom in')
+        self.zoom_in_btn.installEventFilter(self)
 
         self.zoom_disp = QSpinBox()
         self.zoom_disp.setAlignment(Qt.AlignCenter)
@@ -216,8 +242,9 @@ def setup_toolbar(self):
         layout.addWidget(self.zoom_in_btn)
         layout.addWidget(self.zoom_disp)
 
-        return [savefig_btn, saveall_btn, copyfig_btn, closefig_btn,
-                closeall_btn, vsep1, goback_btn, gonext_btn, vsep2, zoom_pan]
+        return [self.savefig_btn, saveall_btn, copyfig_btn, self.closefig_btn,
+                closeall_btn, separator1, goback_btn, gonext_btn,
+                separator2, zoom_pan]
 
     def setup_option_actions(self, mute_inline_plotting, show_plot_outline,
                              auto_fit_plotting):
@@ -272,8 +299,7 @@ def setup_options_button(self):
 
     def create_shortcuts(self):
         """Create shortcuts for this widget."""
-        # Configurable
-        copyfig = CONF.config_shortcut(
+        copyfig_sc = CONF.config_shortcut(
             self.copy_figure,
             context='plots',
             name='copy',
@@ -291,7 +317,44 @@ def create_shortcuts(self):
             name='next figure',
             parent=self)
 
-        return [copyfig, prevfig, nextfig]
+        savefig_sc = CONF.config_shortcut(
+            self.save_figure,
+            context='plots',
+            name='save',
+            parent=self)
+
+        saveallfig = CONF.config_shortcut(
+            self.save_all_figures,
+            context='plots',
+            name='save all',
+            parent=self)
+
+        closefig = CONF.config_shortcut(
+            self.close_figure,
+            context='plots',
+            name='close',
+            parent=self)
+
+        closeallfig = CONF.config_shortcut(
+            self.close_all_figures,
+            context='plots',
+            name='close all',
+            parent=self)
+
+        zoom_out = CONF.config_shortcut(
+            self.zoom_out,
+            context='plots',
+            name='zoom out',
+            parent=self)
+
+        zoom_in = CONF.config_shortcut(
+            self.zoom_in,
+            context='plots',
+            name='zoom in',
+            parent=self)
+
+        return [copyfig_sc, prevfig, nextfig, savefig_sc, saveallfig,
+                closefig, closeallfig, zoom_out, zoom_in]
 
     def get_shortcut_data(self):
         """
@@ -313,10 +376,17 @@ def option_changed(self, option, value):
     def show_fig_outline_in_viewer(self, state):
         """Draw a frame around the figure viewer if state is True."""
         if state is True:
-            self.figviewer.figcanvas.setStyleSheet(
-                    "FigureCanvas{border: 1px solid lightgrey;}")
+            if is_dark_interface():
+                self.figviewer.figcanvas.setStyleSheet(
+                    "FigureCanvas{border:1px solid %s;}" %
+                    qdarkstyle.palette.DarkPalette.COLOR_BACKGROUND_NORMAL)
+            else:
+                self.figviewer.figcanvas.setStyleSheet(
+                    "FigureCanvas{border: 1px solid %s;}" %
+                    self.figviewer.figcanvas.palette().shadow().color().name())
         else:
-            self.figviewer.figcanvas.setStyleSheet("FigureCanvas{}")
+            self.figviewer.figcanvas.setStyleSheet(
+                "FigureCanvas{border: 0px;}")
         self.option_changed('show_plot_outline', state)
 
     def change_auto_fit_plotting(self, state):
@@ -594,22 +664,17 @@ def __init__(self, figure_viewer, parent=None, background_color=None):
         # So to scroll programmatically to the latest item after it
         # is added to the scrollarea, we need to do it instead in a slot
         # connected to the scrollbar's rangeChanged signal.
-        # See spyder-ide/#10914 for more details.
+        # See spyder-ide/spyder#10914 for more details.
         self._new_thumbnail_added = False
         self.scrollarea.verticalScrollBar().rangeChanged.connect(
             self._scroll_to_newest_item)
 
     def setup_gui(self):
         """Setup the main layout of the widget."""
-        scrollarea = self.setup_scrollarea()
-        up_btn, down_btn = self.setup_arrow_buttons()
-
         layout = QVBoxLayout(self)
         layout.setContentsMargins(0, 0, 0, 0)
         layout.setSpacing(0)
-        layout.addWidget(up_btn)
-        layout.addWidget(scrollarea)
-        layout.addWidget(down_btn)
+        layout.addWidget(self.setup_scrollarea())
 
     def setup_scrollarea(self):
         """Setup the scrollarea that will contain the FigureThumbnails."""
@@ -617,14 +682,16 @@ def setup_scrollarea(self):
 
         self.scene = QGridLayout(self.view)
         self.scene.setContentsMargins(0, 0, 0, 0)
-        self.scene.setSpacing(3)
+        # The vertical spacing between the thumbnails.
+        # Note that we need to set this value explicitly or else the tests
+        # are failing on macOS. See spyder-ide/spyder#11576.
+        self.scene.setSpacing(5)
 
         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.setMinimumWidth(self._min_scrollbar_width)
 
@@ -638,27 +705,6 @@ def setup_scrollarea(self):
 
         return self.scrollarea
 
-    def setup_arrow_buttons(self):
-        """
-        Setup the up and down arrow buttons that are placed at the top and
-        bottom of the scrollarea.
-        """
-        # Get the size hint height of the horizontal scrollbar.
-        height = self.scrollarea.horizontalScrollBar().sizeHint().height()
-
-        # Setup the up and down arrow button.
-        up_btn = up_btn = QPushButton(icon=ima.icon('last_edit_location'))
-        up_btn.setFlat(True)
-        up_btn.setFixedHeight(height)
-        up_btn.clicked.connect(self.go_up)
-
-        down_btn = QPushButton(icon=ima.icon('folding.arrow_down_on'))
-        down_btn.setFlat(True)
-        down_btn.setFixedHeight(height)
-        down_btn.clicked.connect(self.go_down)
-
-        return up_btn, down_btn
-
     def set_figureviewer(self, figure_viewer):
         """Set the bamespace for the FigureViewer."""
         self.figure_viewer = figure_viewer
@@ -666,8 +712,17 @@ def set_figureviewer(self, 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.
+        their width fit that of the scrollarea and to remap some key press
+        events to mimick navigational behaviour of a Qt widget list.
         """
+        if event.type() == QEvent.KeyPress:
+            key = event.key()
+            if key == Qt.Key_Up:
+                self.go_previous_thumbnail()
+                return True
+            elif key == Qt.Key_Down:
+                self.go_next_thumbnail()
+                return True
         if event.type() == QEvent.Resize:
             self._update_thumbnail_size()
         return super(ThumbnailScrollBar, self).eventFilter(widget, event)
@@ -723,10 +778,9 @@ def save_figure_as(self, fig, fmt):
             save_figure_tofile(fig, fmt, fname)
 
     # ---- Thumbails Handlers
-    def _calculate_figure_canvas_width(self, thumbnail):
+    def _calculate_figure_canvas_width(self):
         """
-        Calculate the width the thumbnail's figure canvas need to have for the
-        thumbnail to fit the scrollarea.
+        Calculate the width the thumbnails need to have to fit the scrollarea.
         """
         extra_padding = 10 if sys.platform == 'darwin' else 0
         figure_canvas_width = (
@@ -734,8 +788,8 @@ def _calculate_figure_canvas_width(self, thumbnail):
             2 * self.lineWidth() -
             self.scrollarea.viewportMargins().left() -
             self.scrollarea.viewportMargins().right() -
-            thumbnail.savefig_btn.width() -
-            thumbnail.layout().spacing() - extra_padding
+            extra_padding -
+            self.scrollarea.verticalScrollBar().sizeHint().width()
             )
         if is_dark_interface():
             # This is required to take into account some hard-coded padding
@@ -748,7 +802,7 @@ 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)
+        max_canvas_size = self._calculate_figure_canvas_width()
         thumbnail.scale_canvas_size(max_canvas_size)
 
     def _update_thumbnail_size(self):
@@ -840,7 +894,7 @@ def set_current_thumbnail(self, thumbnail):
         """Set the currently selected thumbnail."""
         self.current_thumbnail = thumbnail
         self.figure_viewer.load_figure(
-                thumbnail.canvas.fig, thumbnail.canvas.fmt)
+            thumbnail.canvas.fig, thumbnail.canvas.fmt)
         for thumbnail in self._thumbnails:
             thumbnail.highlight_canvas(thumbnail == self.current_thumbnail)
 
@@ -914,6 +968,8 @@ def __init__(self, parent=None, background_color=None):
         super(FigureThumbnail, self).__init__(parent)
         self.canvas = FigureCanvas(self, background_color=background_color)
         self.canvas.installEventFilter(self)
+        self.canvas.sig_clear_fig_requested.connect(self.emit_remove_figure)
+        self.canvas.sig_save_fig_requested.connect(self.emit_save_figure)
         self.setup_gui()
 
     def setup_gui(self):
@@ -921,44 +977,24 @@ def setup_gui(self):
         layout = QGridLayout(self)
         layout.setContentsMargins(0, 0, 0, 0)
         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."""
-        self.savefig_btn = create_toolbutton(
-            self, icon=ima.icon('filesave'),
-            tip=_("Save Image As..."),
-            triggered=self.emit_save_figure)
-        self.delfig_btn = create_toolbutton(
-            self, icon=ima.icon('editclear'),
-            tip=_("Delete image"),
-            triggered=self.emit_remove_figure)
-
-        toolbar = QVBoxLayout()
-        toolbar.setContentsMargins(0, 0, 0, 0)
-        toolbar.setSpacing(1)
-        toolbar.addWidget(self.savefig_btn)
-        toolbar.addWidget(self.delfig_btn)
-        toolbar.addStretch(2)
-
-        return toolbar
-
     def highlight_canvas(self, highlight):
         """
         Set a colored frame around the FigureCanvas if highlight is True.
         """
-        colorname = self.canvas.palette().highlight().color().name()
         if highlight:
             # Highlighted figure is not clear in dark mode with blue color.
             # See spyder-ide/spyder#10255.
             if is_dark_interface():
                 self.canvas.setStyleSheet(
-                        "FigureCanvas{border: 2px solid %s;}" % "#148CD2")
+                    "FigureCanvas{border: 2px solid %s;}" %
+                    qdarkstyle.palette.DarkPalette.COLOR_SELECTION_LIGHT
+                    )
             else:
                 self.canvas.setStyleSheet(
-                        "FigureCanvas{border: 2px solid %s;}" % colorname)
+                    "FigureCanvas{border: 2px solid %s;}" %
+                    self.canvas.palette().highlight().color().name())
         else:
             self.canvas.setStyleSheet("FigureCanvas{}")
 
@@ -1005,6 +1041,8 @@ class FigureCanvas(QFrame):
     """
     A basic widget on which can be painted a custom png, jpg, or svg image.
     """
+    sig_clear_fig_requested = Signal()
+    sig_save_fig_requested = Signal()
 
     def __init__(self, parent=None, background_color=None):
         super(FigureCanvas, self).__init__(parent)
@@ -1027,11 +1065,21 @@ def context_menu_requested(self, event):
         if self.fig:
             pos = QPoint(event.x(), event.y())
             context_menu = QMenu(self)
+            context_menu.addAction(
+                ima.icon('filesave'),
+                _("Save plot as..."),
+                lambda: self.sig_save_fig_requested.emit(),
+                QKeySequence(CONF.get_shortcut('plots', 'save')))
             context_menu.addAction(
                 ima.icon('editcopy'),
-                "Copy Image",
+                _("Copy Image"),
                 self.copy_figure,
                 QKeySequence(CONF.get_shortcut('plots', 'copy')))
+            context_menu.addAction(
+                ima.icon('editclear'),
+                _("Remove plot"),
+                lambda: self.sig_clear_fig_requested.emit(),
+                QKeySequence(CONF.get_shortcut('plots', 'close')))
             context_menu.popup(self.mapToGlobal(pos))
 
     @Slot()


Test patch:
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
@@ -328,12 +328,12 @@ def test_save_thumbnails(figbrowser, tmpdir, qtbot, mocker, fmt):
     figs = add_figures_to_browser(figbrowser, 3, tmpdir, fmt)
     fext = '.svg' if fmt == 'image/svg+xml' else '.png'
 
-    # Save the second thumbnail of the scrollbar.
+    # Select and save the second thumbnail of the scrollbar.
     figname = osp.join(to_text_string(tmpdir), 'figname' + fext)
     mocker.patch('spyder.plugins.plots.widgets.figurebrowser.getsavefilename',
                  return_value=(figname, fext))
-    qtbot.mouseClick(
-        figbrowser.thumbnails_sb._thumbnails[1].savefig_btn, Qt.LeftButton)
+    figbrowser.thumbnails_sb.set_current_index(1)
+    qtbot.mouseClick(figbrowser.savefig_btn, Qt.LeftButton)
 
     expected_qpix = QPixmap()
     expected_qpix.loadFromData(figs[1], fmt.upper())
@@ -351,9 +351,9 @@ def test_close_thumbnails(figbrowser, tmpdir, qtbot, mocker, fmt):
     """
     figs = add_figures_to_browser(figbrowser, 3, tmpdir, fmt)
 
-    # Close the second thumbnail of the scrollbar.
-    qtbot.mouseClick(
-        figbrowser.thumbnails_sb._thumbnails[1].delfig_btn, Qt.LeftButton)
+    # Select and close the second thumbnail of the scrollbar.
+    figbrowser.thumbnails_sb.set_current_index(1)
+    qtbot.mouseClick(figbrowser.closefig_btn, Qt.LeftButton)
     del figs[1]
 
     assert len(figbrowser.thumbnails_sb._thumbnails) == len(figs)