# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2019 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.
#
# ###########################################################################*/
"""
:mod:`silx.gui.plot.actions.control` provides a set of QAction relative to control
of a :class:`.PlotWidget`.
The following QAction are available:
- :class:`ColormapAction`
- :class:`CrosshairAction`
- :class:`CurveStyleAction`
- :class:`GridAction`
- :class:`KeepAspectRatioAction`
- :class:`PanWithArrowKeysAction`
- :class:`ResetZoomAction`
- :class:`ShowAxisAction`
- :class:`XAxisLogarithmicAction`
- :class:`XAxisAutoScaleAction`
- :class:`YAxisInvertedAction`
- :class:`YAxisLogarithmicAction`
- :class:`YAxisAutoScaleAction`
- :class:`ZoomBackAction`
- :class:`ZoomInAction`
- :class:`ZoomOutAction`
"""
from __future__ import division
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
__license__ = "MIT"
__date__ = "27/11/2020"
from . import PlotAction
import logging
from silx.gui.plot import items
from silx.gui.plot._utils import applyZoomToPlot as _applyZoomToPlot
from silx.gui import qt
from silx.gui import icons
_logger = logging.getLogger(__name__)
[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.getXAxis().sigAutoScaleChanged.connect(self._autoscaleChanged)
plot.getYAxis().sigAutoScaleChanged.connect(self._autoscaleChanged)
def _autoscaleChanged(self, enabled):
xAxis = self.plot.getXAxis()
yAxis = self.plot.getYAxis()
self.setEnabled(xAxis.isAutoScale() or yAxis.isAutoScale())
if xAxis.isAutoScale() and yAxis.isAutoScale():
tooltip = 'Auto-scale the graph'
elif xAxis.isAutoScale(): # And not Y axis
tooltip = 'Auto-scale the x-axis of the graph only'
elif yAxis.isAutoScale(): # 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 ZoomBackAction(PlotAction):
"""QAction performing a zoom-back in :class:`.PlotWidget` limits history.
:param plot: :class:`.PlotWidget` instance on which to operate
:param parent: See :class:`QAction`
"""
def __init__(self, plot, parent=None):
super(ZoomBackAction, self).__init__(
plot, icon='zoom-back', text='Zoom Back',
tooltip='Zoom back the plot',
triggered=self._actionTriggered,
checkable=False, parent=parent)
self.setShortcutContext(qt.Qt.WidgetShortcut)
def _actionTriggered(self, checked=False):
self.plot.getLimitsHistory().pop()
[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.getXAxis().isAutoScale())
plot.getXAxis().sigAutoScaleChanged.connect(self.setChecked)
def _actionTriggered(self, checked=False):
self.plot.getXAxis().setAutoScale(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.getYAxis().isAutoScale())
plot.getYAxis().sigAutoScaleChanged.connect(self.setChecked)
def _actionTriggered(self, checked=False):
self.plot.getYAxis().setAutoScale(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.axis = plot.getXAxis()
self.setChecked(self.axis.getScale() == self.axis.LOGARITHMIC)
self.axis.sigScaleChanged.connect(self._setCheckedIfLogScale)
def _setCheckedIfLogScale(self, scale):
self.setChecked(scale == self.axis.LOGARITHMIC)
def _actionTriggered(self, checked=False):
scale = self.axis.LOGARITHMIC if checked else self.axis.LINEAR
self.axis.setScale(scale)
[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.axis = plot.getYAxis()
self.setChecked(self.axis.getScale() == self.axis.LOGARITHMIC)
self.axis.sigScaleChanged.connect(self._setCheckedIfLogScale)
def _setCheckedIfLogScale(self, scale):
self.setChecked(scale == self.axis.LOGARITHMIC)
def _actionTriggered(self, checked=False):
scale = self.axis.LOGARITHMIC if checked else self.axis.LINEAR
self.axis.setScale(scale)
[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())
if currentState == (False, False):
newState = True, False
else:
# 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=True, parent=parent)
self.plot.sigActiveImageChanged.connect(self._updateColormap)
self.plot.sigActiveScatterChanged.connect(self._updateColormap)
[docs] def setColorDialog(self, colorDialog):
"""Set a specific color dialog instead of using the default dialog."""
assert(colorDialog is not None)
assert(self._dialog is None)
self._dialog = colorDialog
self._dialog.visibleChanged.connect(self._dialogVisibleChanged)
self.setChecked(self._dialog.isVisible())
@staticmethod
def _createDialog(parent):
"""Create the dialog if not already existing
:parent QWidget parent: Parent of the new colormap
:rtype: ColormapDialog
"""
from silx.gui.dialog.ColormapDialog import ColormapDialog
dialog = ColormapDialog(parent=parent)
dialog.setModal(False)
return dialog
def _actionTriggered(self, checked=False):
"""Create a cmap dialog and update active image and default cmap."""
if self._dialog is None:
self._dialog = self._createDialog(self.plot)
self._dialog.visibleChanged.connect(self._dialogVisibleChanged)
# Run the dialog listening to colormap change
if checked is True:
self._updateColormap()
self._dialog.show()
else:
self._dialog.hide()
def _dialogVisibleChanged(self, isVisible):
self.setChecked(isVisible)
def _updateColormap(self):
if self._dialog is None:
return
image = self.plot.getActiveImage()
if isinstance(image, items.ImageComplexData):
# Specific init for complex images
colormap = image.getColormap()
mode = image.getComplexMode()
if mode in (items.ImageComplexData.ComplexMode.AMPLITUDE_PHASE,
items.ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE):
data = image.getData(
copy=False, mode=items.ImageComplexData.ComplexMode.PHASE)
else:
data = image.getData(copy=False)
# Set histogram and range if any
self._dialog.setData(data)
elif isinstance(image, items.ColormapMixIn):
# Set dialog from active image
colormap = image.getColormap()
# Set histogram and range if any
self._dialog.setItem(image)
else:
# No active image or active image is RGBA,
# Check for active scatter plot
scatter = self.plot._getActiveItem(kind='scatter')
if scatter is not None:
colormap = scatter.getColormap()
self._dialog.setItem(scatter)
else:
# No active data image nor scatter,
# set dialog from default info
colormap = self.plot.getDefaultColormap()
# Reset histogram and range if any
self._dialog.setData(None)
self._dialog.setColormap(colormap)
[docs]class ColorBarAction(PlotAction):
"""QAction opening the ColorBarWidget of the specified plot.
: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 ColorBar
super(ColorBarAction, self).__init__(
plot, icon='colorbar', text='Colorbar',
tooltip="Show/Hide the colorbar",
triggered=self._actionTriggered,
checkable=True, parent=parent)
colorBarWidget = self.plot.getColorBarWidget()
old = self.blockSignals(True)
self.setChecked(colorBarWidget.isVisibleTo(self.plot))
self.blockSignals(old)
colorBarWidget.sigVisibleChanged.connect(self._widgetVisibleChanged)
def _widgetVisibleChanged(self, isVisible):
"""Callback when the colorbar `visible` property change."""
if self.isChecked() == isVisible:
return
self.setChecked(isVisible)
def _actionTriggered(self, checked=False):
"""Create a cmap dialog and update active image and default cmap."""
colorBarWidget = self.plot.getColorBarWidget()
if not colorBarWidget.isHidden() == checked:
return
self.plot.getColorBarWidget().setVisible(checked)
[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.getYAxis().isInverted()]
super(YAxisInvertedAction, self).__init__(
plot,
icon=icon,
text='Invert Y Axis',
tooltip=tooltip,
triggered=self._actionTriggered,
checkable=False,
parent=parent)
plot.getYAxis().sigInvertedChanged.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
yAxis = self.plot.getYAxis()
yAxis.setInverted(not yAxis.isInverted())
[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)
[docs]class ShowAxisAction(PlotAction):
"""QAction controlling axis visibility on a :class:`.PlotWidget`.
:param plot: :class:`.PlotWidget` instance on which to operate
:param parent: See :class:`QAction`
"""
def __init__(self, plot, parent=None):
tooltip = 'Show plot axis when checked, otherwise hide them'
PlotAction.__init__(self,
plot,
icon='axis',
text='show axis',
tooltip=tooltip,
triggered=self._actionTriggered,
checkable=True,
parent=parent)
self.setChecked(self.plot.isAxesDisplayed())
plot._sigAxesVisibilityChanged.connect(self.setChecked)
def _actionTriggered(self, checked=False):
self.plot.setAxesDisplayed(checked)
[docs]class ClosePolygonInteractionAction(PlotAction):
"""QAction controlling closure of a polygon in draw interaction mode
if the :class:`.PlotWidget`.
:param plot: :class:`.PlotWidget` instance on which to operate
:param parent: See :class:`QAction`
"""
def __init__(self, plot, parent=None):
tooltip = 'Close the current polygon drawn'
PlotAction.__init__(self,
plot,
icon='add-shape-polygon',
text='Close the polygon',
tooltip=tooltip,
triggered=self._actionTriggered,
checkable=True,
parent=parent)
self.plot.sigInteractiveModeChanged.connect(self._modeChanged)
self._modeChanged(None)
def _modeChanged(self, source):
mode = self.plot.getInteractiveMode()
enabled = "shape" in mode and mode["shape"] == "polygon"
self.setEnabled(enabled)
def _actionTriggered(self, checked=False):
self.plot._eventHandler.validate()
[docs]class OpenGLAction(PlotAction):
"""QAction controlling rendering of a :class:`.PlotWidget`.
For now it can enable or not the OpenGL backend.
: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 = {
"opengl": (icons.getQIcon('backend-opengl'),
"OpenGL rendering (fast)\nClick to disable OpenGL"),
"matplotlib": (icons.getQIcon('backend-opengl'),
"Matplotlib rendering (safe)\nClick to enable OpenGL"),
"unknown": (icons.getQIcon('backend-opengl'),
"Custom rendering")
}
name = self._getBackendName(plot)
self.__state = name
icon, tooltip = self._states[name]
super(OpenGLAction, self).__init__(
plot,
icon=icon,
text='Enable/disable OpenGL rendering',
tooltip=tooltip,
triggered=self._actionTriggered,
checkable=True,
parent=parent)
def _backendUpdated(self):
name = self._getBackendName(self.plot)
self.__state = name
icon, tooltip = self._states[name]
self.setIcon(icon)
self.setToolTip(tooltip)
self.setChecked(name == "opengl")
def _getBackendName(self, plot):
backend = plot.getBackend()
name = type(backend).__name__.lower()
if "opengl" in name:
return "opengl"
elif "matplotlib" in name:
return "matplotlib"
else:
return "unknown"
def _actionTriggered(self, checked=False):
plot = self.plot
name = self._getBackendName(self.plot)
if self.__state != name:
# THere is no event to know the backend was updated
# So here we check if there is a mismatch between the displayed state
# and the real state of the widget
self._backendUpdated()
return
if name != "opengl":
from silx.gui.utils import glutils
result = glutils.isOpenGLAvailable()
if not result:
qt.QMessageBox.critical(plot, "OpenGL rendering not available", result.error)
# Uncheck if needed
self._backendUpdated()
return
plot.setBackend("opengl")
else:
plot.setBackend("matplotlib")
self._backendUpdated()