diff options
Diffstat (limited to 'silx/gui/plot/PlotActions.py')
-rw-r--r-- | silx/gui/plot/PlotActions.py | 1399 |
1 files changed, 40 insertions, 1359 deletions
diff --git a/silx/gui/plot/PlotActions.py b/silx/gui/plot/PlotActions.py index aad27d2..dd16221 100644 --- a/silx/gui/plot/PlotActions.py +++ b/silx/gui/plot/PlotActions.py @@ -22,1365 +22,46 @@ # THE SOFTWARE. # # ###########################################################################*/ -"""This module provides a set of QAction to use with :class:`.PlotWidget`. +"""Depracted module linking old PlotAction with the actions.xxx""" -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"] +__author__ = ["V.A. Sole", "T. Vincent"] __license__ = "MIT" -__date__ = "20/04/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, items -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.math.medianfilter import medfilt2d -from silx.gui.widgets.MedianFilterDialog import MedianFilterDialog - -from silx.io.utils import save1D, savespec - - -_logger = logging.getLogger(__name__) - - -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, parent) - - if tooltip is not None: - self.setToolTip(tooltip) - - self.setCheckable(checkable) - - if triggered is not None: - self.triggered[bool].connect(triggered) - - @property - def plot(self): - """The :class:`.PlotWidget` this action group is controlling.""" - return self._plotRef() - - -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() - - -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) - - -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) - - -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() - - -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() - - -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) - - -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) - - -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) - - -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]) - - -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 not isinstance(image, items.ColormapMixIn): - # No active image or active image is RGBA, - # 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.getColormap() - - data = image.getData(copy=False) - - 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 colormap - activeImage = self.plot.getActiveImage() - if isinstance(activeImage, items.ColormapMixIn): - activeImage.setColormap(colormap) - - -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()) - - -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()) - - -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.getXLabel() - if xlabel is None: - xlabel = self.plot.getGraphXLabel() - ylabel = curve.getYLabel() - if ylabel is None: - ylabel = self.plot.getGraphYLabel() - - try: - save1D(filename, - curve.getXData(copy=False), - curve.getYData(copy=False), - 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.getXData(copy=False), - curve.getYData(copy=False), - curve.getXLabel(), - curve.getYLabel(), - 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.getXData(copy=False), - curve.getYData(copy=False), - curve.getXLabel(), - curve.getYLabel(), - 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.getData(copy=False) - - # 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): - # Get displayed image - rgbaImage = image.getRbgaImageData(copy=False) - # 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 - - -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 - 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 - - def printPlotAsWidget(self): - """Open the print dialog and print the plot. - - Use :meth:`QWidget.render` to print the plot - - :return: True if successful - """ - dialog = qt.QPrintDialog(self.printer, self.plot) - dialog.setWindowTitle('Print Plot') - if not dialog.exec_(): - return False - - # Print a snapshot of the plot widget at the top of the page - widget = self.plot.centralWidget() - - painter = qt.QPainter() - if not painter.begin(self.printer): - return False - - pageRect = self.printer.pageRect() - xScale = pageRect.width() / widget.width() - yScale = pageRect.height() / widget.height() - scale = min(xScale, yScale) - - painter.translate(pageRect.width() / 2., 0.) - painter.scale(scale, scale) - painter.translate(-widget.width() / 2., 0.) - widget.render(painter) - painter.end() - - return True - - 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 - - -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) - - 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) - - -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) - - -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'.") - - -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 = curve.getXData(copy=False) - self.y = curve.getYData(copy=False) - self.legend = curve.getLegend() - 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] - fit_legend = "Fit <%s>" % self.legend - fit_curve = self.plot.getCurve(fit_legend) - - if ddict["event"] == "FitFinished": - y_fit = self.fit_widget.fitmanager.gendata() - if fit_curve is None: - self.plot.addCurve(x_fit, y_fit, - fit_legend, - xlabel=self.xlabel, ylabel=self.ylabel, - resetzoom=False) - else: - fit_curve.setData(x_fit, y_fit) - fit_curve.setVisible(True) - - if ddict["event"] in ["FitStarted", "FitFailed"]: - if fit_curve is not None: - fit_curve.setVisible(False) - - -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() - - def computeIntensityDistribution(self): - """Get the active image and compute the image intensity distribution - """ - activeImage = self.plot.getActiveImage() - - if activeImage is not None: - image = activeImage.getData(copy=False) - 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): - if nbins > xmax - xmin: - nbins = xmax - xmin - - nbins = max(2, nbins) - - data = image.ravel().astype(numpy.float32) - histogram = Histogramnd(data, n_bins=nbins, histo_range=data_range) - assert len(histogram.edges) == 1 - self._histo = histogram.histo - edges = histogram.edges[0] - plot = self.getHistogramPlotWidget() - plot.addHistogram(histogram=self._histo, - edges=edges, - legend='pixel intensity', - fill=True, - color='red') - plot.resetZoom() - - 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) - - def getHistogramPlotWidget(self): - """Create the plot histogram if needed, otherwise create it - - :return: the PlotWidget showing the histogram of the pixel intensities - """ - from silx.gui.plot.PlotWindow import Plot1D - if self._plotHistogram is None: - self._plotHistogram = Plot1D(parent=self.plot) - self._plotHistogram.setWindowFlags(qt.Qt.Window) - self._plotHistogram.setWindowTitle('Image Intensity Histogram') - self._plotHistogram.installEventFilter(self) - self._plotHistogram.setGraphXLabel("Value") - self._plotHistogram.setGraphYLabel("Count") - - return self._plotHistogram - - def getHistogram(self): - """Return the last computed histogram - - :return: the histogram displayed in the HistogramPlotWiget - """ - return self._histo - - -class MedianFilterAction(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='median-filter', - text='median filter', - tooltip='Apply a median filter on the image', - triggered=self._triggered, - parent=parent) - self._originalImage = None - self._legend = None - self._filteredImage = None - self._popup = MedianFilterDialog(parent=None) - self._popup.sigFilterOptChanged.connect(self._updateFilter) - self.plot.sigActiveImageChanged.connect( self._updateActiveImage) - self._updateActiveImage() - - def _triggered(self, checked): - """Update the plot of the histogram visibility status - - :param bool checked: status of the action button - """ - self._popup.show() - - def _updateActiveImage(self): - """Set _activeImageLegend and _originalImage from the active image""" - self._activeImageLegend = self.plot.getActiveImage(just_legend=True) - if self._activeImageLegend is None: - self._originalImage = None - self._legend = None - else: - self._originalImage = self.plot.getImage(self._activeImageLegend).getData(copy=False) - self._legend = self.plot.getImage(self._activeImageLegend).getLegend() - - def _updateFilter(self, kernelWidth, conditional=False): - if self._originalImage is None: - return - - self.plot.sigActiveImageChanged.disconnect(self._updateActiveImage) - filteredImage = self._computeFilteredImage(kernelWidth, conditional) - self.plot.addImage(data=filteredImage, - legend=self._legend, - replace=True) - self.plot.sigActiveImageChanged.connect(self._updateActiveImage) - - def _computeFilteredImage(self, kernelWidth, conditional): - raise NotImplemented('MedianFilterAction is a an abstract class') - - def getFilteredImage(self): - """ - :return: the image with the median filter apply on""" - return self._filteredImage - - -class MedianFilter1DAction(MedianFilterAction): - """Define the MedianFilterAction for 1D - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - def __init__(self, plot, parent=None): - MedianFilterAction.__init__(self, - plot, - parent=parent) - - def _computeFilteredImage(self, kernelWidth, conditional): - assert(self.plot is not None) - return medfilt2d(self._originalImage, - (kernelWidth, 1), - conditional) - - -class MedianFilter2DAction(MedianFilterAction): - """Define the MedianFilterAction for 2D - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - def __init__(self, plot, parent=None): - MedianFilterAction.__init__(self, - plot, - parent=parent) - - def _computeFilteredImage(self, kernelWidth, conditional): - assert(self.plot is not None) - return medfilt2d(self._originalImage, - (kernelWidth, kernelWidth), - conditional) +__date__ = "01/06/2017" + +from silx.utils.deprecation import deprecated_warning + +deprecated_warning(type_='module', + name=__file__, + reason='PlotActions refactoring', + replacement='plot.actions', + since_version='0.6') + +from .actions import PlotAction + +from .actions.io import CopyAction +from .actions.io import PrintAction +from .actions.io import SaveAction + +from .actions.control import ColormapAction +from .actions.control import CrosshairAction +from .actions.control import CurveStyleAction +from .actions.control import GridAction +from .actions.control import KeepAspectRatioAction +from .actions.control import PanWithArrowKeysAction +from .actions.control import ResetZoomAction +from .actions.control import XAxisAutoScaleAction +from .actions.control import XAxisLogarithmicAction +from .actions.control import YAxisAutoScaleAction +from .actions.control import YAxisLogarithmicAction +from .actions.control import YAxisInvertedAction +from .actions.control import ZoomInAction +from .actions.control import ZoomOutAction + +from .actions.medfilt import MedianFilter1DAction +from .actions.medfilt import MedianFilter2DAction +from .actions.medfilt import MedianFilterAction + +from .actions.histogram import PixelIntensitiesHistoAction + +from .actions.fit import FitAction |