Skip to main content
`Ctrl`+`K`

# 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__)

    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()

    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()

    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.0 / 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.getXAxis().isAutoScale())
            plot.getXAxis().sigAutoScaleChanged.connect(self.setChecked)

        def _actionTriggered(self, checked=False):
            self.plot.getXAxis().setAutoScale(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.getYAxis().isAutoScale())
            plot.getYAxis().sigAutoScaleChanged.connect(self.setChecked)

        def _actionTriggered(self, checked=False):
            self.plot.getYAxis().setAutoScale(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.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)

    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)

    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())

            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])

    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)

        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)

    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)

    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.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())

    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)

    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)

    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()

    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")