# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2017 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""This module provides a set of QAction to use with :class:`.PlotWidget`.
The following QAction are available:
- :class:`ColormapAction`
- :class:`CopyAction`
- :class:`CrosshairAction`
- :class:`CurveStyleAction`
- :class:`FitAction`
- :class:`GridAction`
- :class:`KeepAspectRatioAction`
- :class:`PanWithArrowKeysAction`
- :class:`PrintAction`
- :class:`ResetZoomAction`
- :class:`SaveAction`
- :class:`XAxisLogarithmicAction`
- :class:`XAxisAutoScaleAction`
- :class:`YAxisInvertedAction`
- :class:`YAxisLogarithmicAction`
- :class:`YAxisAutoScaleAction`
- :class:`ZoomInAction`
- :class:`ZoomOutAction`
"""
from __future__ import division
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
__license__ = "MIT"
__date__ = "26/01/2017"
from collections import OrderedDict
import logging
import sys
import traceback
import weakref
if sys.version_info[0] == 3:
    from io import BytesIO
else:
    import cStringIO as _StringIO
    BytesIO = _StringIO.StringIO
import numpy
from .. import icons
from .. import qt
from .._utils import convertArrayToQImage
from . import Colors
from .ColormapDialog import ColormapDialog
from ._utils import applyZoomToPlot as _applyZoomToPlot
from silx.third_party.EdfFile import EdfFile
from silx.third_party.TiffIO import TiffIO
from silx.math.histogram import Histogramnd
from silx.io.utils import save1D, savespec
_logger = logging.getLogger(__name__)
[docs]class PlotAction(qt.QAction):
    """Base class for QAction that operates on a PlotWidget.
    :param plot: :class:`.PlotWidget` instance on which to operate.
    :param icon: QIcon or str name of icon to use
    :param str text: The name of this action to be used for menu label
    :param str tooltip: The text of the tooltip
    :param triggered: The callback to connect to the action's triggered
                      signal or None for no callback.
    :param bool checkable: True for checkable action, False otherwise (default)
    :param parent: See :class:`QAction`.
    """
    def __init__(self, plot, icon, text, tooltip=None,
                 triggered=None, checkable=False, parent=None):
        assert plot is not None
        self._plotRef = weakref.ref(plot)
        if not isinstance(icon, qt.QIcon):
            # Try with icon as a string and load corresponding icon
            icon = icons.getQIcon(icon)
        super(PlotAction, self).__init__(icon, text, None)
        if tooltip is not None:
            self.setToolTip(tooltip)
        self.setCheckable(checkable)
        if triggered is not None:
            self.triggered[bool].connect(triggered)
    @property
[docs]    def plot(self):
        """The :class:`.PlotWidget` this action group is controlling."""
        return self._plotRef()
  
[docs]class ResetZoomAction(PlotAction):
    """QAction controlling reset zoom on a :class:`.PlotWidget`.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        super(ResetZoomAction, self).__init__(
            plot, icon='zoom-original', text='Reset Zoom',
            tooltip='Auto-scale the graph',
            triggered=self._actionTriggered,
            checkable=False, parent=parent)
        self._autoscaleChanged(True)
        plot.sigSetXAxisAutoScale.connect(self._autoscaleChanged)
        plot.sigSetYAxisAutoScale.connect(self._autoscaleChanged)
    def _autoscaleChanged(self, enabled):
        self.setEnabled(
            self.plot.isXAxisAutoScale() or self.plot.isYAxisAutoScale())
        if self.plot.isXAxisAutoScale() and self.plot.isYAxisAutoScale():
            tooltip = 'Auto-scale the graph'
        elif self.plot.isXAxisAutoScale():  # And not Y axis
            tooltip = 'Auto-scale the x-axis of the graph only'
        elif self.plot.isYAxisAutoScale():  # And not X axis
            tooltip = 'Auto-scale the y-axis of the graph only'
        else:  # no axis in autoscale
            tooltip = 'Auto-scale the graph'
        self.setToolTip(tooltip)
    def _actionTriggered(self, checked=False):
        self.plot.resetZoom()
 
[docs]class ZoomInAction(PlotAction):
    """QAction performing a zoom-in on a :class:`.PlotWidget`.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        super(ZoomInAction, self).__init__(
            plot, icon='zoom-in', text='Zoom In',
            tooltip='Zoom in the plot',
            triggered=self._actionTriggered,
            checkable=False, parent=parent)
        self.setShortcut(qt.QKeySequence.ZoomIn)
        self.setShortcutContext(qt.Qt.WidgetShortcut)
    def _actionTriggered(self, checked=False):
        _applyZoomToPlot(self.plot, 1.1)
 
[docs]class ZoomOutAction(PlotAction):
    """QAction performing a zoom-out on a :class:`.PlotWidget`.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        super(ZoomOutAction, self).__init__(
            plot, icon='zoom-out', text='Zoom Out',
            tooltip='Zoom out the plot',
            triggered=self._actionTriggered,
            checkable=False, parent=parent)
        self.setShortcut(qt.QKeySequence.ZoomOut)
        self.setShortcutContext(qt.Qt.WidgetShortcut)
    def _actionTriggered(self, checked=False):
        _applyZoomToPlot(self.plot, 1. / 1.1)
 
[docs]class XAxisAutoScaleAction(PlotAction):
    """QAction controlling X axis autoscale on a :class:`.PlotWidget`.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        super(XAxisAutoScaleAction, self).__init__(
            plot, icon='plot-xauto', text='X Autoscale',
            tooltip='Enable x-axis auto-scale when checked.\n'
                    'If unchecked, x-axis does not change when reseting zoom.',
            triggered=self._actionTriggered,
            checkable=True, parent=parent)
        self.setChecked(plot.isXAxisAutoScale())
        plot.sigSetXAxisAutoScale.connect(self.setChecked)
    def _actionTriggered(self, checked=False):
        self.plot.setXAxisAutoScale(checked)
        if checked:
            self.plot.resetZoom()
 
[docs]class YAxisAutoScaleAction(PlotAction):
    """QAction controlling Y axis autoscale on a :class:`.PlotWidget`.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        super(YAxisAutoScaleAction, self).__init__(
            plot, icon='plot-yauto', text='Y Autoscale',
            tooltip='Enable y-axis auto-scale when checked.\n'
                    'If unchecked, y-axis does not change when reseting zoom.',
            triggered=self._actionTriggered,
            checkable=True, parent=parent)
        self.setChecked(plot.isXAxisAutoScale())
        plot.sigSetYAxisAutoScale.connect(self.setChecked)
    def _actionTriggered(self, checked=False):
        self.plot.setYAxisAutoScale(checked)
        if checked:
            self.plot.resetZoom()
 
[docs]class XAxisLogarithmicAction(PlotAction):
    """QAction controlling X axis log scale on a :class:`.PlotWidget`.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        super(XAxisLogarithmicAction, self).__init__(
            plot, icon='plot-xlog', text='X Log. scale',
            tooltip='Logarithmic x-axis when checked',
            triggered=self._actionTriggered,
            checkable=True, parent=parent)
        self.setChecked(plot.isXAxisLogarithmic())
        plot.sigSetXAxisLogarithmic.connect(self.setChecked)
    def _actionTriggered(self, checked=False):
        self.plot.setXAxisLogarithmic(checked)
 
[docs]class YAxisLogarithmicAction(PlotAction):
    """QAction controlling Y axis log scale on a :class:`.PlotWidget`.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        super(YAxisLogarithmicAction, self).__init__(
            plot, icon='plot-ylog', text='Y Log. scale',
            tooltip='Logarithmic y-axis when checked',
            triggered=self._actionTriggered,
            checkable=True, parent=parent)
        self.setChecked(plot.isYAxisLogarithmic())
        plot.sigSetYAxisLogarithmic.connect(self.setChecked)
    def _actionTriggered(self, checked=False):
        self.plot.setYAxisLogarithmic(checked)
 
[docs]class GridAction(PlotAction):
    """QAction controlling grid mode on a :class:`.PlotWidget`.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param str gridMode: The grid mode to use in 'both', 'major'.
                         See :meth:`.PlotWidget.setGraphGrid`
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, gridMode='both', parent=None):
        assert gridMode in ('both', 'major')
        self._gridMode = gridMode
        super(GridAction, self).__init__(
            plot, icon='plot-grid', text='Grid',
            tooltip='Toggle grid (on/off)',
            triggered=self._actionTriggered,
            checkable=True, parent=parent)
        self.setChecked(plot.getGraphGrid() is not None)
        plot.sigSetGraphGrid.connect(self._gridChanged)
    def _gridChanged(self, which):
        """Slot listening for PlotWidget grid mode change."""
        self.setChecked(which != 'None')
    def _actionTriggered(self, checked=False):
        self.plot.setGraphGrid(self._gridMode if checked else None)
 
[docs]class CurveStyleAction(PlotAction):
    """QAction controlling curve style on a :class:`.PlotWidget`.
    It changes the default line and markers style which updates all
    curves on the plot.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        super(CurveStyleAction, self).__init__(
            plot, icon='plot-toggle-points', text='Curve style',
            tooltip='Change curve line and markers style',
            triggered=self._actionTriggered,
            checkable=False, parent=parent)
    def _actionTriggered(self, checked=False):
        currentState = (self.plot.isDefaultPlotLines(),
                        self.plot.isDefaultPlotPoints())
        # line only, line and symbol, symbol only
        states = (True, False), (True, True), (False, True)
        newState = states[(states.index(currentState) + 1) % 3]
        self.plot.setDefaultPlotLines(newState[0])
        self.plot.setDefaultPlotPoints(newState[1])
 
[docs]class ColormapAction(PlotAction):
    """QAction opening a ColormapDialog to update the colormap.
    Both the active image colormap and the default colormap are updated.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        self._dialog = None  # To store an instance of ColormapDialog
        super(ColormapAction, self).__init__(
            plot, icon='colormap', text='Colormap',
            tooltip="Change colormap",
            triggered=self._actionTriggered,
            checkable=False, parent=parent)
    def _actionTriggered(self, checked=False):
        """Create a cmap dialog and update active image and default cmap."""
        # Create the dialog if not already existing
        if self._dialog is None:
            self._dialog = ColormapDialog()
        image = self.plot.getActiveImage()
        if image is None:
            # No active image, set dialog from default info
            colormap = self.plot.getDefaultColormap()
            self._dialog.setHistogram()  # Reset histogram and range if any
        else:
            # Set dialog from active image
            colormap = image[4]['colormap']
            data = image[0]
            goodData = data[numpy.isfinite(data)]
            if goodData.size > 0:
                dataMin = goodData.min()
                dataMax = goodData.max()
            else:
                qt.QMessageBox.warning(
                    self, "No Data",
                    "Image data does not contain any real value")
                dataMin, dataMax = 1., 10.
            self._dialog.setHistogram()  # Reset histogram if any
            self._dialog.setDataRange(dataMin, dataMax)
            # The histogram should be done in a worker thread
            # hist, bin_edges = numpy.histogram(goodData, bins=256)
            # self._dialog.setHistogram(hist, bin_edges)
        self._dialog.setColormap(**colormap)
        # Run the dialog listening to colormap change
        self._dialog.sigColormapChanged.connect(self._colormapChanged)
        result = self._dialog.exec_()
        self._dialog.sigColormapChanged.disconnect(self._colormapChanged)
        if not result:  # Restore the previous colormap
            self._colormapChanged(colormap)
    def _colormapChanged(self, colormap):
        # Update default colormap
        self.plot.setDefaultColormap(colormap)
        # Update active image
        image = self.plot.getActiveImage()
        if image is not None:
            # Update image: This do not preserve pixmap
            params = image[4].copy()
            params['colormap'] = colormap
            self.plot.addImage(image[0],
                               legend=image[1],
                               replace=False,
                               resetzoom=False,
                               **params)
 
[docs]class KeepAspectRatioAction(PlotAction):
    """QAction controlling aspect ratio on a :class:`.PlotWidget`.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        # Uses two images for checked/unchecked states
        self._states = {
            False: (icons.getQIcon('shape-circle-solid'),
                    "Keep data aspect ratio"),
            True: (icons.getQIcon('shape-ellipse-solid'),
                   "Do no keep data aspect ratio")
        }
        icon, tooltip = self._states[plot.isKeepDataAspectRatio()]
        super(KeepAspectRatioAction, self).__init__(
            plot,
            icon=icon,
            text='Toggle keep aspect ratio',
            tooltip=tooltip,
            triggered=self._actionTriggered,
            checkable=False,
            parent=parent)
        plot.sigSetKeepDataAspectRatio.connect(
            self._keepDataAspectRatioChanged)
    def _keepDataAspectRatioChanged(self, aspectRatio):
        """Handle Plot set keep aspect ratio signal"""
        icon, tooltip = self._states[aspectRatio]
        self.setIcon(icon)
        self.setToolTip(tooltip)
    def _actionTriggered(self, checked=False):
        # This will trigger _keepDataAspectRatioChanged
        self.plot.setKeepDataAspectRatio(not self.plot.isKeepDataAspectRatio())
 
[docs]class YAxisInvertedAction(PlotAction):
    """QAction controlling Y orientation on a :class:`.PlotWidget`.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        # Uses two images for checked/unchecked states
        self._states = {
            False: (icons.getQIcon('plot-ydown'),
                    "Orient Y axis downward"),
            True: (icons.getQIcon('plot-yup'),
                   "Orient Y axis upward"),
        }
        icon, tooltip = self._states[plot.isYAxisInverted()]
        super(YAxisInvertedAction, self).__init__(
            plot,
            icon=icon,
            text='Invert Y Axis',
            tooltip=tooltip,
            triggered=self._actionTriggered,
            checkable=False,
            parent=parent)
        plot.sigSetYAxisInverted.connect(self._yAxisInvertedChanged)
    def _yAxisInvertedChanged(self, inverted):
        """Handle Plot set y axis inverted signal"""
        icon, tooltip = self._states[inverted]
        self.setIcon(icon)
        self.setToolTip(tooltip)
    def _actionTriggered(self, checked=False):
        # This will trigger _yAxisInvertedChanged
        self.plot.setYAxisInverted(not self.plot.isYAxisInverted())
 
[docs]class SaveAction(PlotAction):
    """QAction for saving Plot content.
    It opens a Save as... dialog.
    :param plot: :class:`.PlotWidget` instance on which to operate.
    :param parent: See :class:`QAction`.
    """
    # TODO find a way to make the filter list selectable and extensible
    SNAPSHOT_FILTER_SVG = 'Plot Snapshot as SVG (*.svg)'
    SNAPSHOT_FILTERS = ('Plot Snapshot as PNG (*.png)',
                        'Plot Snapshot as JPEG (*.jpg)',
                        SNAPSHOT_FILTER_SVG)
    # Dict of curve filters with CSV-like format
    # Using ordered dict to guarantee filters order
    # Note: '%.18e' is numpy.savetxt default format
    CURVE_FILTERS_TXT = OrderedDict((
        ('Curve as Raw ASCII (*.txt)',
         {'fmt': '%.18e', 'delimiter': ' ', 'header': False}),
        ('Curve as ";"-separated CSV (*.csv)',
         {'fmt': '%.18e', 'delimiter': ';', 'header': True}),
        ('Curve as ","-separated CSV (*.csv)',
         {'fmt': '%.18e', 'delimiter': ',', 'header': True}),
        ('Curve as tab-separated CSV (*.csv)',
         {'fmt': '%.18e', 'delimiter': '\t', 'header': True}),
        ('Curve as OMNIC CSV (*.csv)',
         {'fmt': '%.7E', 'delimiter': ',', 'header': False}),
        ('Curve as SpecFile (*.dat)',
         {'fmt': '%.7g', 'delimiter': '', 'header': False})
    ))
    CURVE_FILTER_NPY = 'Curve as NumPy binary file (*.npy)'
    CURVE_FILTERS = list(CURVE_FILTERS_TXT.keys()) + [CURVE_FILTER_NPY]
    ALL_CURVES_FILTERS = ("All curves as SpecFile (*.dat)", )
    IMAGE_FILTER_EDF = 'Image data as EDF (*.edf)'
    IMAGE_FILTER_TIFF = 'Image data as TIFF (*.tif)'
    IMAGE_FILTER_NUMPY = 'Image data as NumPy binary file (*.npy)'
    IMAGE_FILTER_ASCII = 'Image data as ASCII (*.dat)'
    IMAGE_FILTER_CSV_COMMA = 'Image data as ,-separated CSV (*.csv)'
    IMAGE_FILTER_CSV_SEMICOLON = 'Image data as ;-separated CSV (*.csv)'
    IMAGE_FILTER_CSV_TAB = 'Image data as tab-separated CSV (*.csv)'
    IMAGE_FILTER_RGB_PNG = 'Image as PNG (*.png)'
    IMAGE_FILTER_RGB_TIFF = 'Image as TIFF (*.tif)'
    IMAGE_FILTERS = (IMAGE_FILTER_EDF,
                     IMAGE_FILTER_TIFF,
                     IMAGE_FILTER_NUMPY,
                     IMAGE_FILTER_ASCII,
                     IMAGE_FILTER_CSV_COMMA,
                     IMAGE_FILTER_CSV_SEMICOLON,
                     IMAGE_FILTER_CSV_TAB,
                     IMAGE_FILTER_RGB_PNG,
                     IMAGE_FILTER_RGB_TIFF)
    def __init__(self, plot, parent=None):
        super(SaveAction, self).__init__(
            plot, icon='document-save', text='Save as...',
            tooltip='Save curve/image/plot snapshot dialog',
            triggered=self._actionTriggered,
            checkable=False, parent=parent)
        self.setShortcut(qt.QKeySequence.Save)
        self.setShortcutContext(qt.Qt.WidgetShortcut)
    def _errorMessage(self, informativeText=''):
        """Display an error message."""
        # TODO issue with QMessageBox size fixed and too small
        msg = qt.QMessageBox(self.plot)
        msg.setIcon(qt.QMessageBox.Critical)
        msg.setInformativeText(informativeText + ' ' + str(sys.exc_info()[1]))
        msg.setDetailedText(traceback.format_exc())
        msg.exec_()
    def _saveSnapshot(self, filename, nameFilter):
        """Save a snapshot of the :class:`PlotWindow` widget.
        :param str filename: The name of the file to write
        :param str nameFilter: The selected name filter
        :return: False if format is not supported or save failed,
                 True otherwise.
        """
        if nameFilter == self.SNAPSHOT_FILTER_SVG:
            self.plot.saveGraph(filename, fileFormat='svg')
        else:
            if hasattr(qt.QPixmap, "grabWidget"):
                # Qt 4
                pixmap = qt.QPixmap.grabWidget(self.plot.getWidgetHandle())
            else:
                # Qt 5
                pixmap = self.plot.getWidgetHandle().grab()
            if not pixmap.save(filename):
                self._errorMessage()
                return False
        return True
    def _saveCurve(self, filename, nameFilter):
        """Save a curve from the plot.
        :param str filename: The name of the file to write
        :param str nameFilter: The selected name filter
        :return: False if format is not supported or save failed,
                 True otherwise.
        """
        if nameFilter not in self.CURVE_FILTERS:
            return False
        # Check if a curve is to be saved
        curve = self.plot.getActiveCurve()
        # before calling _saveCurve, if there is no selected curve, we
        # make sure there is only one curve on the graph
        if curve is None:
            curves = self.plot.getAllCurves()
            if not curves:
                self._errorMessage("No curve to be saved")
                return False
            curve = curves[0]
        if nameFilter in self.CURVE_FILTERS_TXT:
            filter_ = self.CURVE_FILTERS_TXT[nameFilter]
            fmt = filter_['fmt']
            csvdelim = filter_['delimiter']
            autoheader = filter_['header']
        else:
            # .npy
            fmt, csvdelim, autoheader = ("", "", False)
        # If curve has no associated label, get the default from the plot
        xlabel = curve[4]['xlabel']
        if xlabel is None:
            xlabel = self.plot.getGraphXLabel()
        ylabel = curve[4]['ylabel']
        if ylabel is None:
            ylabel = self.plot.getGraphYLabel()
        try:
            save1D(filename, curve[0], curve[1],
                   xlabel, [ylabel],
                   fmt=fmt, csvdelim=csvdelim,
                   autoheader=autoheader)
        except IOError:
            self._errorMessage('Save failed\n')
            return False
        return True
    def _saveCurves(self, filename, nameFilter):
        """Save all curves from the plot.
        :param str filename: The name of the file to write
        :param str nameFilter: The selected name filter
        :return: False if format is not supported or save failed,
                 True otherwise.
        """
        if nameFilter not in self.ALL_CURVES_FILTERS:
            return False
        curves = self.plot.getAllCurves()
        if not curves:
            self._errorMessage("No curves to be saved")
            return False
        curve = curves[0]
        scanno = 1
        try:
            specfile = savespec(filename, curve[0], curve[1],
                                curve[4]['xlabel'], curve[4]['ylabel'],
                                fmt="%.7g", scan_number=1, mode="w",
                                write_file_header=True,
                                close_file=False)
        except IOError:
            self._errorMessage('Save failed\n')
            return False
        for curve in curves[1:]:
            try:
                scanno += 1
                specfile = savespec(specfile, curve[0], curve[1],
                                    curve[4]['xlabel'], curve[4]['ylabel'],
                                    fmt="%.7g", scan_number=scanno, mode="w",
                                    write_file_header=False,
                                    close_file=False)
            except IOError:
                self._errorMessage('Save failed\n')
                return False
        specfile.close()
        return True
    def _saveImage(self, filename, nameFilter):
        """Save an image from the plot.
        :param str filename: The name of the file to write
        :param str nameFilter: The selected name filter
        :return: False if format is not supported or save failed,
                 True otherwise.
        """
        if nameFilter not in self.IMAGE_FILTERS:
            return False
        image = self.plot.getActiveImage()
        if image is None:
            qt.QMessageBox.warning(
                self.plot, "No Data", "No image to be saved")
            return False
        data = image[0]
        # TODO Use silx.io for writing files
        if nameFilter == self.IMAGE_FILTER_EDF:
            edfFile = EdfFile(filename, access="w+")
            edfFile.WriteImage({}, data, Append=0)
            return True
        elif nameFilter == self.IMAGE_FILTER_TIFF:
            tiffFile = TiffIO(filename, mode='w')
            tiffFile.writeImage(data, software='silx')
            return True
        elif nameFilter == self.IMAGE_FILTER_NUMPY:
            try:
                numpy.save(filename, data)
            except IOError:
                self._errorMessage('Save failed\n')
                return False
            return True
        elif nameFilter in (self.IMAGE_FILTER_ASCII,
                            self.IMAGE_FILTER_CSV_COMMA,
                            self.IMAGE_FILTER_CSV_SEMICOLON,
                            self.IMAGE_FILTER_CSV_TAB):
            csvdelim, filetype = {
                self.IMAGE_FILTER_ASCII: (' ', 'txt'),
                self.IMAGE_FILTER_CSV_COMMA: (',', 'csv'),
                self.IMAGE_FILTER_CSV_SEMICOLON: (';', 'csv'),
                self.IMAGE_FILTER_CSV_TAB: ('\t', 'csv'),
                }[nameFilter]
            height, width = data.shape
            rows, cols = numpy.mgrid[0:height, 0:width]
            try:
                save1D(filename, rows.ravel(), (cols.ravel(), data.ravel()),
                       filetype=filetype,
                       xlabel='row',
                       ylabels=['column', 'value'],
                       csvdelim=csvdelim,
                       autoheader=True)
            except IOError:
                self._errorMessage('Save failed\n')
                return False
            return True
        elif nameFilter in (self.IMAGE_FILTER_RGB_PNG,
                            self.IMAGE_FILTER_RGB_TIFF):
            # Apply colormap to data
            colormap = image[4]['colormap']
            scalarMappable = Colors.getMPLScalarMappable(colormap, data)
            rgbaImage = scalarMappable.to_rgba(data, bytes=True)
            # Convert RGB QImage
            qimage = convertArrayToQImage(rgbaImage[:, :, :3])
            if nameFilter == self.IMAGE_FILTER_RGB_PNG:
                fileFormat = 'PNG'
            else:
                fileFormat = 'TIFF'
            if qimage.save(filename, fileFormat):
                return True
            else:
                _logger.error('Failed to save image as %s', filename)
                qt.QMessageBox.critical(
                    self.parent(),
                    'Save image as',
                    'Failed to save image')
        return False
    def _actionTriggered(self, checked=False):
        """Handle save action."""
        # Set-up filters
        filters = []
        # Add image filters if there is an active image
        if self.plot.getActiveImage() is not None:
            filters.extend(self.IMAGE_FILTERS)
        # Add curve filters if there is a curve to save
        if (self.plot.getActiveCurve() is not None or
                len(self.plot.getAllCurves()) == 1):
            filters.extend(self.CURVE_FILTERS)
        if len(self.plot.getAllCurves()) > 1:
            filters.extend(self.ALL_CURVES_FILTERS)
        filters.extend(self.SNAPSHOT_FILTERS)
        # Create and run File dialog
        dialog = qt.QFileDialog(self.plot)
        dialog.setWindowTitle("Output File Selection")
        dialog.setModal(1)
        dialog.setNameFilters(filters)
        dialog.setFileMode(dialog.AnyFile)
        dialog.setAcceptMode(dialog.AcceptSave)
        if not dialog.exec_():
            return False
        nameFilter = dialog.selectedNameFilter()
        filename = dialog.selectedFiles()[0]
        dialog.close()
        # Forces the filename extension to match the chosen filter
        extension = nameFilter.split()[-1][2:-1]
        if (len(filename) <= len(extension) or
                filename[-len(extension):].lower() != extension.lower()):
            filename += extension
        # Handle save
        if nameFilter in self.SNAPSHOT_FILTERS:
            return self._saveSnapshot(filename, nameFilter)
        elif nameFilter in self.CURVE_FILTERS:
            return self._saveCurve(filename, nameFilter)
        elif nameFilter in self.ALL_CURVES_FILTERS:
            return self._saveCurves(filename, nameFilter)
        elif nameFilter in self.IMAGE_FILTERS:
            return self._saveImage(filename, nameFilter)
        else:
            _logger.warning('Unsupported file filter: %s', nameFilter)
            return False
 
def _plotAsPNG(plot):
    """Save a :class:`Plot` as PNG and return the payload.
    :param plot: The :class:`Plot` to save
    """
    pngFile = BytesIO()
    plot.saveGraph(pngFile, fileFormat='png')
    pngFile.flush()
    pngFile.seek(0)
    data = pngFile.read()
    pngFile.close()
    return data
[docs]class PrintAction(PlotAction):
    """QAction for printing the plot.
    It opens a Print dialog.
    Current implementation print a bitmap of the plot area and not vector
    graphics, so printing quality is not great.
    :param plot: :class:`.PlotWidget` instance on which to operate.
    :param parent: See :class:`QAction`.
    """
    # Share QPrinter instance to propose latest used as default
    _printer = None
    def __init__(self, plot, parent=None):
        super(PrintAction, self).__init__(
            plot, icon='document-print', text='Print...',
            tooltip='Open print dialog',
            triggered=self.printPlot,
            checkable=False, parent=parent)
        self.setShortcut(qt.QKeySequence.Print)
        self.setShortcutContext(qt.Qt.WidgetShortcut)
    @property
[docs]    def printer(self):
        """The QPrinter instance used by the actions.
        This is shared accross all instances of PrintAct
        """
        if self._printer is None:
            PrintAction._printer = qt.QPrinter()
        return self._printer
 
[docs]    def printPlot(self):
        """Open the print dialog and print the plot.
        Use :meth:`Plot.saveGraph` to print the plot.
        :return: True if successful
        """
        # Init printer and start printer dialog
        dialog = qt.QPrintDialog(self.printer, self.plot)
        dialog.setWindowTitle('Print Plot')
        if not dialog.exec_():
            return False
        # Save Plot as PNG and make a pixmap from it with default dpi
        pngData = _plotAsPNG(self.plot)
        pixmap = qt.QPixmap()
        pixmap.loadFromData(pngData, 'png')
        xScale = self.printer.pageRect().width() / pixmap.width()
        yScale = self.printer.pageRect().height() / pixmap.height()
        scale = min(xScale, yScale)
        # Draw pixmap with painter
        painter = qt.QPainter()
        if not painter.begin(self.printer):
            return False
        painter.drawPixmap(0, 0,
                           pixmap.width() * scale,
                           pixmap.height() * scale,
                           pixmap)
        painter.end()
        return True
  
[docs]class CopyAction(PlotAction):
    """QAction to copy :class:`.PlotWidget` content to clipboard.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        super(CopyAction, self).__init__(
            plot, icon='edit-copy', text='Copy plot',
            tooltip='Copy a snapshot of the plot into the clipboard',
            triggered=self.copyPlot,
            checkable=False, parent=parent)
        self.setShortcut(qt.QKeySequence.Copy)
        self.setShortcutContext(qt.Qt.WidgetShortcut)
[docs]    def copyPlot(self):
        """Copy plot content to the clipboard as a bitmap."""
        # Save Plot as PNG and make a QImage from it with default dpi
        pngData = _plotAsPNG(self.plot)
        image = qt.QImage.fromData(pngData, 'png')
        qt.QApplication.clipboard().setImage(image)
  
[docs]class CrosshairAction(PlotAction):
    """QAction toggling crosshair cursor on a :class:`.PlotWidget`.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param str color: Color to use to draw the crosshair
    :param int linewidth: Width of the crosshair cursor
    :param str linestyle: Style of line. See :meth:`.Plot.setGraphCursor`
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, color='black', linewidth=1, linestyle='-',
                 parent=None):
        self.color = color
        """Color used to draw the crosshair (str)."""
        self.linewidth = linewidth
        """Width of the crosshair cursor (int)."""
        self.linestyle = linestyle
        """Style of line of the cursor (str)."""
        super(CrosshairAction, self).__init__(
            plot, icon='crosshair', text='Crosshair Cursor',
            tooltip='Enable crosshair cursor when checked',
            triggered=self._actionTriggered,
            checkable=True, parent=parent)
        self.setChecked(plot.getGraphCursor() is not None)
        plot.sigSetGraphCursor.connect(self.setChecked)
    def _actionTriggered(self, checked=False):
        self.plot.setGraphCursor(checked,
                                 color=self.color,
                                 linestyle=self.linestyle,
                                 linewidth=self.linewidth)
 
[docs]class PanWithArrowKeysAction(PlotAction):
    """QAction toggling pan with arrow keys on a :class:`.PlotWidget`.
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        super(PanWithArrowKeysAction, self).__init__(
            plot, icon='arrow-keys', text='Pan with arrow keys',
            tooltip='Enable pan with arrow keys when checked',
            triggered=self._actionTriggered,
            checkable=True, parent=parent)
        self.setChecked(plot.isPanWithArrowKeys())
        plot.sigSetPanWithArrowKeys.connect(self.setChecked)
    def _actionTriggered(self, checked=False):
        self.plot.setPanWithArrowKeys(checked)
 
def _warningMessage(informativeText='', detailedText='', parent=None):
    """Display a popup warning message."""
    msg = qt.QMessageBox(parent)
    msg.setIcon(qt.QMessageBox.Warning)
    msg.setInformativeText(informativeText)
    msg.setDetailedText(detailedText)
    msg.exec_()
def _getOneCurve(plt, mode="unique"):
    """Get a single curve from the plot.
    By default, get the active curve if any, else if a single curve is plotted
    get it, else return None and display a warning popup.
    This behavior can be adjusted by modifying the *mode* parameter: always
    return the active curve if any, but adjust the behavior in case no curve
    is active.
    :param plt: :class:`.PlotWidget` instance on which to operate
    :param mode: Parameter defining the behavior when no curve is active.
        Possible modes:
            - "none": return None (enforce curve activation)
            - "unique": return the unique curve or None if multiple curves
            - "first": return first curve
            - "last": return last curve (most recently added one)
    :return: return value of plt.getActiveCurve(), or plt.getAllCurves()[0],
        or plt.getAllCurves()[-1], or None
    """
    curve = plt.getActiveCurve()
    if curve is not None:
        return curve
    if mode is None or mode.lower() == "none":
        _warningMessage("You must activate a curve!",
                        parent=plt)
        return None
    curves = plt.getAllCurves()
    if len(curves) == 0:
        _warningMessage("No curve on this plot.",
                        parent=plt)
        return None
    if len(curves) == 1:
        return curves[0]
    if len(curves) > 1:
        if mode == "unique":
            _warningMessage("Multiple curves are plotted. " +
                            "Please activate the one you want to use.",
                            parent=plt)
            return None
        if mode.lower() == "first":
            return curves[0]
        if mode.lower() == "last":
            return curves[-1]
    raise ValueError("Illegal value for parameter 'mode'." +
                     " Allowed values: 'none', 'unique', 'first', 'last'.")
[docs]class FitAction(PlotAction):
    """QAction to open a :class:`FitWidget` and set its data to the
    active curve if any, or to the first curve..
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        super(FitAction, self).__init__(
            plot, icon='math-fit', text='Fit curve',
            tooltip='Open a fit dialog',
            triggered=self._getFitWindow,
            checkable=False, parent=parent)
        self.fit_window = None
    def _getFitWindow(self):
        curve = _getOneCurve(self.plot)
        if curve is None:
            return
        self.xlabel = self.plot.getGraphXLabel()
        self.ylabel = self.plot.getGraphYLabel()
        self.x, self.y, self.legend = curve[0:3]
        self.xmin, self.xmax = self.plot.getGraphXLimits()
        # open a window with a FitWidget
        if self.fit_window is None:
            self.fit_window = qt.QMainWindow()
            # import done here rather than at module level to avoid circular import
            # FitWidget -> BackgroundWidget -> PlotWindow -> PlotActions -> FitWidget
            from ..fit.FitWidget import FitWidget
            self.fit_widget = FitWidget(parent=self.fit_window)
            self.fit_window.setCentralWidget(
                    self.fit_widget)
            self.fit_widget.guibuttons.DismissButton.clicked.connect(
                    self.fit_window.close)
            self.fit_widget.sigFitWidgetSignal.connect(
                    self.handle_signal)
            self.fit_window.show()
        else:
            if self.fit_window.isHidden():
                self.fit_window.show()
                self.fit_widget.show()
            self.fit_window.raise_()
        self.fit_widget.setData(self.x, self.y,
                                xmin=self.xmin, xmax=self.xmax)
        self.fit_window.setWindowTitle(
                "Fitting " + self.legend +
                " on x range %f-%f" % (self.xmin, self.xmax))
    def handle_signal(self, ddict):
        x_fit = self.x[self.xmin <= self.x]
        x_fit = x_fit[x_fit <= self.xmax]
        if ddict["event"] == "EstimateFinished":
            self.plot.removeCurve("Fit <%s>" % self.legend)
            y_fit = self.fit_widget.fitmanager.gendata(estimated=True)
            self.plot.addCurve(x_fit, y_fit,
                               "Estimated <%s>" % self.legend,
                               xlabel=self.xlabel, ylabel=self.ylabel,
                               resetzoom=False)
        if ddict["event"] == "FitFinished":
            self.plot.removeCurve("Estimated <%s>" % self.legend)
            y_fit = self.fit_widget.fitmanager.gendata()
            self.plot.addCurve(x_fit, y_fit,
                               "Fit <%s>" % self.legend,
                               xlabel=self.xlabel, ylabel=self.ylabel,
                               resetzoom=False)
        if ddict["event"] == "EstimateFailed":
            self.plot.removeCurve("Estimated <%s>" % self.legend)
        if ddict["event"] == "FitFailed":
            self.plot.removeCurve("Fit <%s>" % self.legend)
 
[docs]class PixelIntensitiesHistoAction(PlotAction):
    """QAction to plot the pixels intensities diagram
    :param plot: :class:`.PlotWidget` instance on which to operate
    :param parent: See :class:`QAction`
    """
    def __init__(self, plot, parent=None):
        PlotAction.__init__(self,
                            plot,
                            icon='pixel-intensities',
                            text='pixels intensity',
                            tooltip='Compute image intensity distribution',
                            triggered=self._triggered,
                            parent=parent,
                            checkable=True)
        self._plotHistogram = None
        self._connectedToActiveImage = False
        self._histo = None
    def _triggered(self, checked):
        """Update the plot of the histogram visibility status
        :param bool checked: status  of the action button
        """
        if checked:
            if not self._connectedToActiveImage:
                self.plot.sigActiveImageChanged.connect(
                    self._activeImageChanged)
                self._connectedToActiveImage = True
                self.computeIntensityDistribution()
            self.getHistogramPlotWidget().show()
        else:
            if self._connectedToActiveImage:
                self.plot.sigActiveImageChanged.disconnect(
                    self._activeImageChanged)
                self._connectedToActiveImage = False
            self.getHistogramPlotWidget().hide()
    def _activeImageChanged(self, previous, legend):
        """Handle active image change: toggle enabled toolbar, update curve"""
        if self.isChecked():
            self.computeIntensityDistribution()
[docs]    def computeIntensityDistribution(self):
        """Get the active image and compute the image intensity distribution
        """
        activeImage = self.plot.getActiveImage()
        if activeImage is not None:
            image = activeImage[0]
            if image.ndim == 3:  # RGB(A) images
                _logger.info('Converting current image from RGB(A) to grayscale\
                    in order to compute the intensity distribution')
                image = image[:,:,0]*0.299 + image[:,:,1]*0.587 + \
                        
image[:,:,2]*0.114
            xmin = numpy.nanmin(image)
            xmax = numpy.nanmax(image)
            nbins = min(1024, int(numpy.sqrt(image.size)))
            data_range = xmin, xmax
            
            # bad hack: get 256 bins in the case we have a B&W
            if numpy.issubdtype(image.dtype, numpy.integer) and nbins>(xmax-xmin):
                nbins = xmax-xmin
            nbins = max(2, nbins)
            data = image.ravel().astype(numpy.float32)
            self._histo, w_histo, edges = Histogramnd(data,
                                                n_bins=nbins,
                                                histo_range=data_range)
            assert(len(edges) == 1)
            x=numpy.arange(nbins)*(xmax-xmin)/nbins + xmin
            y=self._histo
            self.getHistogramPlotWidget().addCurve(
                x=x,
                y=y,
                legend='pixel intensity',
                fill=True,
                color='red',
                histogram='center')
            self.getHistogramPlotWidget().setActiveCurve(None)
 
[docs]    def eventFilter(self, qobject, event):
        """Observe when the close event is emitted then 
        simply uncheck the action button
        :param qobject: the object observe
        :param event: the event received by qobject
        """
        if event.type() == qt.QEvent.Close:
            if self._plotHistogram is not None:
                self._plotHistogram.hide()
            self.setChecked(False)
        return PlotAction.eventFilter(self, qobject, event)
 
[docs]    def getHistogram(self):
        """Return the histogram displayed in the HistogramPlotWiget
        """
        return self._histo