Repository: spyder-ide/spyder

Issue:
Feature Request: copy figure of plot widget to clipboard
## Problem Description
I often paste figures into my document. Currently Plot Widget only supports save function, so I have to go through several steps to put figure into document. I would be convenient if the copy function is added.

I think that there are three ways to add the copy function to the plot widget.
- option 1
![fig_copy_context](https://user-images.githubusercontent.com/22970578/50296409-c9a6e980-04bd-11e9-8a1a-00c54ff91f58.gif)
  
- option 2
![fig_copy_thumnail](https://user-images.githubusercontent.com/22970578/50296425-d297bb00-04bd-11e9-9c65-bf6cc3b6a323.gif)
  
- option 3
![fig_copy_toolbar](https://user-images.githubusercontent.com/22970578/50296451-de837d00-04bd-11e9-96b0-d25a3a8940b0.gif)

Please add a "Copy figure to Clipboard" feature to Plot Widget.


Discussion:
Thoughts @jnsebgosselin ?
Thanks for your suggestion. This is definitively a feature that needs to be added.

- I like option 1 and I think it should also be implemented for the thumbnails.
- Option 3 is definitively something that needs to be added.
- I like Option 2, but it makes the thumbnails look a little bit crowded with 3 icons.
- Also, I think that being able to use a keyboard shortcut (like `Ctrl+C`) to copy the figure of the selected thumbnail to the clipboard would also be quite useful.

Would you be interested in proposing a PR to implement it? If not, I'll try to do it during the holidays.
@jnsebgosselin My code for "copy to clipboard" is working in my platform. but I am not experienced programmer. I am a radar engineer. So my code might have a problem. Can I pull request my code?
> Can I pull request my code?

@ok97465 Yeah sure, we will review it. and help you if needed.

@goanpeca or @ccordoba12 What do you think, do you have anything to say about this feature request before a PR is opened for this?
@jnsebgosselin, I also think this would be really nice to have and we're welcome to receive a PR for it.

Resolving patch:
diff --git a/spyder/config/main.py b/spyder/config/main.py
--- a/spyder/config/main.py
+++ b/spyder/config/main.py
@@ -454,6 +454,8 @@
               'array_builder/enter array table': "Ctrl+M",
               # ---- In widgets/variableexplorer/aarayeditor.py ----
               'variable_explorer/copy': 'Ctrl+C',
+              # ---- In widgets/plots/figurebrowser.py ----
+              'plots/copy': 'Ctrl+C',
               }),
             ('appearance',
              {
diff --git a/spyder/plugins/plots/plugin.py b/spyder/plugins/plots/plugin.py
--- a/spyder/plugins/plots/plugin.py
+++ b/spyder/plugins/plots/plugin.py
@@ -103,6 +103,7 @@ def add_shellwidget(self, shellwidget):
                 self.sig_option_changed.emit)
             fig_browser.thumbnails_sb.redirect_stdio.connect(
                 self.main.redirect_internalshell_stdio)
+            self.register_widget_shortcuts(fig_browser)
             self.add_widget(fig_browser)
             self.shellwidgets[shellwidget_id] = fig_browser
             self.set_shellwidget_from_id(shellwidget_id)
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
@@ -15,10 +15,10 @@
 import os.path as osp
 
 # ---- Third library imports
-from qtconsole.svg import svg_to_image
+from qtconsole.svg import svg_to_image, svg_to_clipboard
 from qtpy.compat import getsavefilename, getexistingdirectory
-from qtpy.QtCore import Qt, Signal, QRect, QEvent
-from qtpy.QtGui import QPixmap, QPainter
+from qtpy.QtCore import Qt, Signal, QRect, QEvent, QPoint, QTimer, Slot
+from qtpy.QtGui import QPixmap, QPainter, QKeySequence
 from qtpy.QtWidgets import (QApplication, QHBoxLayout, QMenu,
                             QVBoxLayout, QWidget, QGridLayout, QFrame,
                             QScrollArea, QPushButton, QScrollBar, QSizePolicy,
@@ -32,6 +32,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
 
 
 def save_figure_tofile(fig, fmt, fname):
@@ -91,6 +92,7 @@ def __init__(self, parent, options_button=None, plugin_actions=[],
 
         self.options_button = options_button
         self.plugin_actions = plugin_actions
+        self.shortcuts = self.create_shortcuts(parent)
 
     def setup(self, mute_inline_plotting=None, show_plot_outline=None):
         """Setup the figure browser with provided settings."""
@@ -145,6 +147,12 @@ def setup_toolbar(self):
                 tip=_("Save All Images..."),
                 triggered=self.thumbnails_sb.save_all_figures_as)
 
+        copyfig_btn = create_toolbutton(
+            self, icon=ima.icon('editcopy'),
+            tip=_("Copy plot to clipboard as image (%s)" %
+                  get_shortcut('plots', 'copy')),
+            triggered=self.copy_figure)
+
         closefig_btn = create_toolbutton(
                 self, icon=ima.icon('editclear'),
                 tip=_("Remove image"),
@@ -198,8 +206,8 @@ def setup_toolbar(self):
         layout.addWidget(zoom_in_btn)
         layout.addWidget(self.zoom_disp)
 
-        return [savefig_btn, saveall_btn, closefig_btn, closeall_btn, vsep1,
-                goback_btn, gonext_btn, vsep2, zoom_pan]
+        return [savefig_btn, saveall_btn, copyfig_btn, closefig_btn,
+                closeall_btn, vsep1, goback_btn, gonext_btn, vsep2, zoom_pan]
 
     def setup_option_actions(self, mute_inline_plotting, show_plot_outline):
         """Setup the actions to show in the cog menu."""
@@ -242,6 +250,24 @@ def setup_options_button(self):
         else:
             self.tools_layout.addWidget(self.options_button)
 
+    def create_shortcuts(self, parent):
+        """Create shortcuts for this widget."""
+        # Configurable
+        copyfig = config_shortcut(self.copy_figure, context='plots',
+                                  name='copy', parent=parent)
+
+        return [copyfig]
+
+    def get_shortcut_data(self):
+        """
+        Return shortcut data, a list of tuples (shortcut, text, default).
+
+        shortcut (QShortcut or QAction instance)
+        text (string): action/shortcut description
+        default (string): default key sequence
+        """
+        return [sc.data for sc in self.shortcuts]
+
     def option_changed(self, option, value):
         """Handle when the value of an option has changed"""
         setattr(self, to_text_string(option), value)
@@ -306,6 +332,11 @@ def close_all_figures(self):
         """Close all the figures in the thumbnail scrollbar."""
         self.thumbnails_sb.remove_all_thumbnails()
 
+    def copy_figure(self):
+        """Copy figure from figviewer to clipboard."""
+        if self.figviewer and self.figviewer.figcanvas.fig:
+            self.figviewer.figcanvas.copy_figure()
+
 
 class FigureViewer(QScrollArea):
     """
@@ -764,6 +795,44 @@ def __init__(self, parent=None, background_color=None):
         self.fmt = None
         self.fwidth, self.fheight = 200, 200
 
+        self.setContextMenuPolicy(Qt.CustomContextMenu)
+        self.customContextMenuRequested.connect(self.context_menu_requested)
+
+    def context_menu_requested(self, event):
+        """Popup context menu."""
+        if self.fig:
+            pos = QPoint(event.x(), event.y())
+            context_menu = QMenu(self)
+            context_menu.addAction(ima.icon('editcopy'), "Copy Image",
+                                   self.copy_figure,
+                                   QKeySequence(
+                                       get_shortcut('plots', 'copy')))
+            context_menu.popup(self.mapToGlobal(pos))
+
+    @Slot()
+    def copy_figure(self):
+        """Copy figure to clipboard."""
+        if self.fmt in ['image/png', 'image/jpeg']:
+            qpixmap = QPixmap()
+            qpixmap.loadFromData(self.fig, self.fmt.upper())
+            QApplication.clipboard().setImage(qpixmap.toImage())
+        elif self.fmt == 'image/svg+xml':
+            svg_to_clipboard(self.fig)
+        else:
+            return
+
+        self.blink_figure()
+
+    def blink_figure(self):
+        """Blink figure once."""
+        if self.fig:
+            timer = QTimer()
+            frame_rect = self.frameSize()
+            self.setFixedSize(0, 0)
+            timer.singleShot(40,
+                             lambda: self.setFixedSize(frame_rect.width(),
+                                                       frame_rect.height()))
+
     def clear_canvas(self):
         """Clear the figure that was painted on the widget."""
         self.fig = None


Test patch: