Source code for silx.gui.plot.actions.control

# /*##########################################################################
#
# Copyright (c) 2004-2023 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`
"""

__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
from silx.utils.deprecation import deprecated

_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.0 / 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 setColormapDialog(self, dialog): """Set a specific colormap dialog instead of using the default one.""" assert dialog is not None if self._dialog is not None: self._dialog.visibleChanged.disconnect(self._dialogVisibleChanged) self._dialog = dialog self._dialog.visibleChanged.connect( self._dialogVisibleChanged, qt.Qt.UniqueConnection ) self.setChecked(self._dialog.isVisible())
@deprecated(replacement="setColormapDialog", since_version="2.0") def setColorDialog(self, colorDialog): self.setColormapDialog(colorDialog) def getColormapDialog(self): if self._dialog is None: self._dialog = self._createDialog(self.plot) self._dialog.visibleChanged.connect(self._dialogVisibleChanged) return self._dialog @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.""" dialog = self.getColormapDialog() # Run the dialog listening to colormap change if checked is True: self._updateColormap() dialog.show() else: 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.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.getActiveScatter() 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.interaction()._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) 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, ) plot.sigBackendChanged.connect(self._backendUpdated) def _backendUpdated(self): name = self._getBackendName(self.plot) 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 name != "opengl": from silx.gui.utils import glutils result = glutils.isOpenGLAvailable() if not result: qt.QMessageBox.critical( plot, "OpenGL rendering is not available", result.error ) return plot.setBackend("opengl") else: plot.setBackend("matplotlib")