# Source code for guiqwt.tools

    # -*- coding: utf-8 -*-
    #
    # Copyright © 2009-2010 CEA
    # Pierre Raybaut
    # Licensed under the terms of the CECILL License
    # (see guiqwt/__init__.py for details)

    # pylint: disable=C0103

    """
    guiqwt.tools
    ------------

    The `tools` module provides a collection of `plot tools` :
        * :py:class:`guiqwt.tools.RectZoomTool`
        * :py:class:`guiqwt.tools.SelectTool`
        * :py:class:`guiqwt.tools.SelectPointTool`
        * :py:class:`guiqwt.tools.MultiLineTool`
        * :py:class:`guiqwt.tools.FreeFormTool`
        * :py:class:`guiqwt.tools.LabelTool`
        * :py:class:`guiqwt.tools.RectangleTool`
        * :py:class:`guiqwt.tools.PointTool`
        * :py:class:`guiqwt.tools.SegmentTool`
        * :py:class:`guiqwt.tools.CircleTool`
        * :py:class:`guiqwt.tools.EllipseTool`
        * :py:class:`guiqwt.tools.PlaceAxesTool`
        * :py:class:`guiqwt.tools.AnnotatedRectangleTool`
        * :py:class:`guiqwt.tools.AnnotatedCircleTool`
        * :py:class:`guiqwt.tools.AnnotatedEllipseTool`
        * :py:class:`guiqwt.tools.AnnotatedPointTool`
        * :py:class:`guiqwt.tools.AnnotatedSegmentTool`
        * :py:class:`guiqwt.tools.HRangeTool`
        * :py:class:`guiqwt.tools.DummySeparatorTool`
        * :py:class:`guiqwt.tools.AntiAliasingTool`
        * :py:class:`guiqwt.tools.DisplayCoordsTool`
        * :py:class:`guiqwt.tools.ReverseYAxisTool`
        * :py:class:`guiqwt.tools.AspectRatioTool`
        * :py:class:`guiqwt.tools.PanelTool`
        * :py:class:`guiqwt.tools.ItemListPanelTool`
        * :py:class:`guiqwt.tools.ContrastPanelTool`
        * :py:class:`guiqwt.tools.ColormapTool`
        * :py:class:`guiqwt.tools.XCSPanelTool`
        * :py:class:`guiqwt.tools.YCSPanelTool`
        * :py:class:`guiqwt.tools.CrossSectionTool`
        * :py:class:`guiqwt.tools.AverageCrossSectionTool`
        * :py:class:`guiqwt.tools.SaveAsTool`
        * :py:class:`guiqwt.tools.CopyToClipboardTool`
        * :py:class:`guiqwt.tools.OpenFileTool`
        * :py:class:`guiqwt.tools.OpenImageTool`
        * :py:class:`guiqwt.tools.SnapshotTool`
        * :py:class:`guiqwt.tools.PrintTool`
        * :py:class:`guiqwt.tools.SaveItemsTool`
        * :py:class:`guiqwt.tools.LoadItemsTool`
        * :py:class:`guiqwt.tools.AxisScaleTool`
        * :py:class:`guiqwt.tools.HelpTool`
        * :py:class:`guiqwt.tools.ExportItemDataTool`
        * :py:class:`guiqwt.tools.EditItemDataTool`
        * :py:class:`guiqwt.tools.ItemCenterTool`
        * :py:class:`guiqwt.tools.DeleteItemTool`

    A `plot tool` is an object providing various features to a plotting widget
    (:py:class:`guiqwt.curve.CurvePlot` or :py:class:`guiqwt.image.ImagePlot`):
    buttons, menus, selection tools, image I/O tools, etc.
    To make it work, a tool has to be registered to the plotting widget's manager,
    i.e. an instance of the :py:class:`guiqwt.plot.PlotManager` class (see
    the :py:mod:`guiqwt.plot` module for more details on the procedure).

    The `CurvePlot` and `ImagePlot` widgets do not provide any `PlotManager`: the
    manager has to be created separately. On the contrary, the ready-to-use widgets
    :py:class:`guiqwt.plot.CurveWidget` and :py:class:`guiqwt.plot.ImageWidget`
    are higher-level plotting widgets with integrated manager, tools and panels.

    .. seealso::

        Module :py:mod:`guiqwt.plot`
            Module providing ready-to-use curve and image plotting widgets and
            dialog boxes

        Module :py:mod:`guiqwt.curve`
            Module providing curve-related plot items and plotting widgets

        Module :py:mod:`guiqwt.image`
            Module providing image-related plot items and plotting widgets

    Example
    ~~~~~~~

    The following example add all the existing tools to an `ImageWidget` object
    for testing purpose:

    .. literalinclude:: /../guiqwt/tests/image_plot_tools.py
       :start-after: guitest: show
       :end-before: Workaround for Sphinx v0.6 bug: empty 'end-before' directive

    .. image:: /images/screenshots/image_plot_tools.png

    Reference
    ~~~~~~~~~

    .. autoclass:: RectZoomTool
       :members:
       :inherited-members:
    .. autoclass:: SelectTool
       :members:
       :inherited-members:
    .. autoclass:: SelectPointTool
       :members:
       :inherited-members:
    .. autoclass:: MultiLineTool
       :members:
       :inherited-members:
    .. autoclass:: FreeFormTool
       :members:
       :inherited-members:
    .. autoclass:: LabelTool
       :members:
       :inherited-members:
    .. autoclass:: RectangleTool
       :members:
       :inherited-members:
    .. autoclass:: PointTool
       :members:
       :inherited-members:
    .. autoclass:: SegmentTool
       :members:
       :inherited-members:
    .. autoclass:: CircleTool
       :members:
       :inherited-members:
    .. autoclass:: EllipseTool
       :members:
       :inherited-members:
    .. autoclass:: PlaceAxesTool
       :members:
       :inherited-members:
    .. autoclass:: AnnotatedRectangleTool
       :members:
       :inherited-members:
    .. autoclass:: AnnotatedCircleTool
       :members:
       :inherited-members:
    .. autoclass:: AnnotatedEllipseTool
       :members:
       :inherited-members:
    .. autoclass:: AnnotatedPointTool
       :members:
       :inherited-members:
    .. autoclass:: AnnotatedSegmentTool
       :members:
       :inherited-members:
    .. autoclass:: HRangeTool
       :members:
       :inherited-members:
    .. autoclass:: DummySeparatorTool
       :members:
       :inherited-members:
    .. autoclass:: AntiAliasingTool
       :members:
       :inherited-members:
    .. autoclass:: DisplayCoordsTool
       :members:
       :inherited-members:
    .. autoclass:: ReverseYAxisTool
       :members:
       :inherited-members:
    .. autoclass:: AspectRatioTool
       :members:
       :inherited-members:
    .. autoclass:: PanelTool
       :members:
       :inherited-members:
    .. autoclass:: ItemListPanelTool
       :members:
       :inherited-members:
    .. autoclass:: ContrastPanelTool
       :members:
       :inherited-members:
    .. autoclass:: ColormapTool
       :members:
       :inherited-members:
    .. autoclass:: XCSPanelTool
       :members:
       :inherited-members:
    .. autoclass:: YCSPanelTool
       :members:
       :inherited-members:
    .. autoclass:: CrossSectionTool
       :members:
       :inherited-members:
    .. autoclass:: AverageCrossSectionTool
       :members:
       :inherited-members:
    .. autoclass:: SaveAsTool
       :members:
       :inherited-members:
    .. autoclass:: CopyToClipboardTool
       :members:
       :inherited-members:
    .. autoclass:: OpenFileTool
       :members:
       :inherited-members:
    .. autoclass:: OpenImageTool
       :members:
       :inherited-members:
    .. autoclass:: SnapshotTool
       :members:
       :inherited-members:
    .. autoclass:: PrintTool
       :members:
       :inherited-members:
    .. autoclass:: SaveItemsTool
       :members:
       :inherited-members:
    .. autoclass:: LoadItemsTool
       :members:
       :inherited-members:
    .. autoclass:: AxisScaleTool
       :members:
       :inherited-members:
    .. autoclass:: HelpTool
       :members:
       :inherited-members:
    .. autoclass:: ExportItemDataTool
       :members:
       :inherited-members:
    .. autoclass:: EditItemDataTool
       :members:
       :inherited-members:
    .. autoclass:: ItemCenterTool
       :members:
       :inherited-members:
    .. autoclass:: DeleteItemTool
       :members:
       :inherited-members:
    """

    # TODO: z(long-terme) à partir d'une sélection rectangulaire sur une image
    #      afficher un ArrayEditor montrant les valeurs de la zone sélectionnée

    import sys
    import numpy as np
    import weakref
    import os.path as osp

    from qtpy.QtCore import Qt, QObject, QPointF, Signal
    from qtpy.QtGui import QKeySequence
    from qtpy.QtPrintSupport import QPrinter, QPrintDialog
    from qtpy.QtWidgets import (
        QMenu,
        QActionGroup,
        QMessageBox,
        QAction,
        QToolButton,
    )
    from qtpy.compat import getsavefilename, getopenfilename

    from guidata.qthelpers import get_std_icon, add_actions, add_separator
    from guidata.widgets.arrayeditor import ArrayEditor
    from guidata.configtools import get_icon
    from guidata.dataset.datatypes import DataSet
    from guidata.dataset.dataitems import BoolItem, FloatItem

    # Local imports
    from guiqwt.config import _
    from guiqwt.events import (
        setup_standard_tool_filter,
        ObjectHandler,
        KeyEventMatch,
        StandardKeyMatch,
        QtDragHandler,
        ZoomRectHandler,
        RectangularSelectionHandler,
        PointSelectionHandler,
        ClickHandler,
    )
    from guiqwt.shapes import (
        Axes,
        RectangleShape,
        Marker,
        PolygonShape,
        EllipseShape,
        SegmentShape,
        PointShape,
        ObliqueRectangleShape,
    )
    from guiqwt.annotations import (
        AnnotatedRectangle,
        AnnotatedCircle,
        AnnotatedEllipse,
        AnnotatedSegment,
        AnnotatedPoint,
        AnnotatedObliqueRectangle,
    )
    from guiqwt.colormap import get_colormap_list, build_icon_from_cmap_name
    from guiqwt.interfaces import (
        IColormapImageItemType,
        IPlotManager,
        IVoiImageItemType,
        IStatsImageItemType,
        ICurveItemType,
    )
    from guiqwt.panels import ID_XCS, ID_YCS, ID_OCS, ID_ITEMLIST, ID_CONTRAST
    from guiqwt.interfaces import IImageItemType

    class DefaultToolbarID:
        pass

    class GuiTool(QObject):
        """Base class for interactive tool applying on a plot"""

        def __init__(self, manager, toolbar_id=DefaultToolbarID):
            """Constructor"""
            super(GuiTool, self).__init__()
            assert IPlotManager in manager.__implements__
            self.manager = manager
            self.parent_tool = None
            self.plots = set()
            self.action = self.create_action(manager)
            self.menu = self.create_action_menu(manager)
            if toolbar_id is DefaultToolbarID:
                toolbar = manager.get_default_toolbar()
            else:
                toolbar = manager.get_toolbar(toolbar_id)
            if toolbar is not None:
                self.setup_toolbar(toolbar)

        def create_action(self, manager):
            """Create and return tool's action"""
            pass

        def setup_toolbar(self, toolbar):
            """Setup tool's toolbar"""
            toolbar.addAction(self.action)
            if self.menu is not None:
                widget = toolbar.widgetForAction(self.action)
                widget.setMenu(self.menu)
                widget.setPopupMode(QToolButton.InstantPopup)

        def create_action_menu(self, manager):
            """Create and return menu for the tool's action"""
            pass

        def set_parent_tool(self, tool):
            """Used to organize tools automatically in menu items"""
            self.parent_tool = tool

        def register_plot(self, baseplot):
            """Every BasePlot using this tool should call register_plot
            to notify the tool about this widget using it
            """
            self.plots.add(baseplot)

        def get_active_plot(self):
            for plot in self.plots:
                canvas = plot.canvas()
                if canvas.hasFocus():
                    return plot
            if len(self.plots) == 1:
                return list(self.plots)[0]
            return None

        def update_status(self, plot):
            """called by to allow derived
            classes to update the states of actions based on the currently
            active BasePlot

            can also be called after an action modifying the BasePlot
            (e.g. in order to update action states when an item is deselected)
            """
            pass

        def setup_context_menu(self, menu, plot):
            """If the tool supports it, this method should install an action
            in the context menu"""
            pass

    class InteractiveTool(GuiTool):
        """Interactive tool base class"""

        TITLE = None
        ICON = None
        TIP = None
        CURSOR = Qt.CrossCursor
        SWITCH_TO_DEFAULT_TOOL = False  # switch to default tool when finished

        #: Signal emitted by InteractiveTool when validating tool action
        SIG_VALIDATE_TOOL = Signal(object)

        #: Signal emitted by InteractiveTool when tool job is finished
        SIG_TOOL_JOB_FINISHED = Signal()

        def __init__(
            self,
            manager,
            toolbar_id=DefaultToolbarID,
            title=None,
            icon=None,
            tip=None,
            switch_to_default_tool=None,
        ):
            if title is not None:
                self.TITLE = title
            if icon is not None:
                self.ICON = icon
            if tip is not None:
                self.TIP = tip
            super(InteractiveTool, self).__init__(manager, toolbar_id)
            # Starting state for every plotwidget we can act upon
            self.start_state = {}

            if switch_to_default_tool is None:
                switch_to_default_tool = self.SWITCH_TO_DEFAULT_TOOL
            if switch_to_default_tool:
                self.SIG_TOOL_JOB_FINISHED.connect(self.manager.activate_default_tool)

        def create_action(self, manager):
            """Create and return tool's action"""
            action = manager.create_action(
                self.TITLE, icon=get_icon(self.ICON), tip=self.TIP, triggered=self.activate
            )
            action.setCheckable(True)
            group = self.manager.get_tool_group("interactive")
            group.addAction(action)
            group.triggered.connect(self.interactive_triggered)
            return action

        def cursor(self):
            """Return tool mouse cursor"""
            return self.CURSOR

        def register_plot(self, baseplot):
            # TODO: with the introduction of PlotManager it should
            # be possible to remove the per tool dictionary start_state
            # since all plots from a manager share the same set of tools
            # the State Machine generated by the calls to tool.setup_filter
            # should be the same for all plots. Thus it should be done only once
            # and not once per plot managed by the plot manager
            super(InteractiveTool, self).register_plot(baseplot)
            filter = baseplot.filter
            start_state = self.setup_filter(baseplot)
            self.start_state[baseplot] = start_state
            curs = self.cursor()
            if curs is not None:
                filter.set_cursor(curs, start_state)

        def interactive_triggered(self, action):
            if action is self.action:
                self.activate()
            else:
                self.deactivate()

        def activate(self):
            """Activate tool"""
            for baseplot, start_state in list(self.start_state.items()):
                baseplot.filter.set_state(start_state, None)
            self.action.setChecked(True)
            self.manager.set_active_tool(self)

        def deactivate(self):
            """Deactivate tool"""
            self.action.setChecked(False)

        def validate(self, filter, event):
            self.SIG_VALIDATE_TOOL.emit(filter)
            self.SIG_TOOL_JOB_FINISHED.emit()

    class SelectTool(InteractiveTool):
        """
        Graphical Object Selection Tool
        """

        TITLE = _("Selection")
        ICON = "selection.png"
        CURSOR = Qt.ArrowCursor

        def setup_filter(self, baseplot):
            filter = baseplot.filter
            # Initialisation du filtre
            start_state = filter.new_state()
            # Bouton gauche :
            ObjectHandler(filter, Qt.LeftButton, start_state=start_state)
            ObjectHandler(
                filter,
                Qt.LeftButton,
                mods=Qt.ControlModifier,
                start_state=start_state,
                multiselection=True,
            )
            filter.add_event(
                start_state,
                KeyEventMatch((Qt.Key_Enter, Qt.Key_Return, Qt.Key_Space)),
                self.validate,
                start_state,
            )
            filter.add_event(
                start_state,
                StandardKeyMatch(QKeySequence.SelectAll),
                self.select_all_items,
                start_state,
            )
            return setup_standard_tool_filter(filter, start_state)

        def select_all_items(self, filter, event):
            filter.plot.select_all()
            filter.plot.replot()

    class SelectPointTool(InteractiveTool):
        TITLE = _("Point selection")
        ICON = "point_selection.png"
        MARKER_STYLE_SECT = "plot"
        MARKER_STYLE_KEY = "marker/curve"
        CURSOR = Qt.PointingHandCursor

        def __init__(
            self,
            manager,
            mode="reuse",
            on_active_item=False,
            title=None,
            icon=None,
            tip=None,
            end_callback=None,
            toolbar_id=DefaultToolbarID,
            marker_style=None,
            switch_to_default_tool=None,
        ):
            super(SelectPointTool, self).__init__(
                manager,
                toolbar_id,
                title=title,
                icon=icon,
                tip=tip,
                switch_to_default_tool=switch_to_default_tool,
            )
            assert mode in ("reuse", "create")
            self.mode = mode
            self.end_callback = end_callback
            self.marker = None
            self.last_pos = None
            self.on_active_item = on_active_item
            if marker_style is not None:
                self.marker_style_sect = marker_style[0]
                self.marker_style_key = marker_style[1]
            else:
                self.marker_style_sect = self.MARKER_STYLE_SECT
                self.marker_style_key = self.MARKER_STYLE_KEY

        def set_marker_style(self, marker):
            marker.set_style(self.marker_style_sect, self.marker_style_key)

        def setup_filter(self, baseplot):
            filter = baseplot.filter
            # Initialisation du filtre
            start_state = filter.new_state()
            # Bouton gauche :
            handler = QtDragHandler(filter, Qt.LeftButton, start_state=start_state)
            handler.SIG_START_TRACKING.connect(self.start)
            handler.SIG_MOVE.connect(self.move)
            handler.SIG_STOP_NOT_MOVING.connect(self.stop)
            handler.SIG_STOP_MOVING.connect(self.stop)
            return setup_standard_tool_filter(filter, start_state)

        def start(self, filter, event):
            if self.marker is None:
                title = ""
                if self.TITLE:
                    title = "<b>%s</b><br>" % self.TITLE
                if self.on_active_item:
                    constraint_cb = filter.plot.on_active_curve
                    label_cb = lambda x, y: title + filter.plot.get_coordinates_str(x, y)
                else:
                    constraint_cb = None
                    label_cb = lambda x, y: "%sx = %g<br>y = %g" % (title, x, y)
                self.marker = Marker(label_cb=label_cb, constraint_cb=constraint_cb)
                self.set_marker_style(self.marker)
            self.marker.attach(filter.plot)
            self.marker.setZ(filter.plot.get_max_z() + 1)
            self.marker.setVisible(True)

        def stop(self, filter, event):
            self.move(filter, event)
            if self.mode != "reuse":
                self.marker.detach()
                self.marker = None
            if self.end_callback:
                self.end_callback(self)

        def move(self, filter, event):
            if self.marker is None:
                return  # something is wrong ...
            self.marker.move_local_point_to(0, event.pos())
            filter.plot.replot()
            self.last_pos = self.marker.xValue(), self.marker.yValue()

        def get_coordinates(self):
            return self.last_pos

    SHAPE_Z_OFFSET = 1000

    class MultiLineTool(InteractiveTool):
        TITLE = _("Polyline")
        ICON = "polyline.png"
        CURSOR = Qt.ArrowCursor

        def __init__(
            self,
            manager,
            handle_final_shape_cb=None,
            shape_style=None,
            toolbar_id=DefaultToolbarID,
            title=None,
            icon=None,
            tip=None,
            switch_to_default_tool=None,
        ):
            super(MultiLineTool, self).__init__(
                manager,
                toolbar_id,
                title=title,
                icon=icon,
                tip=tip,
                switch_to_default_tool=switch_to_default_tool,
            )
            self.handle_final_shape_cb = handle_final_shape_cb
            self.shape = None
            self.current_handle = None
            self.init_pos = None
            if shape_style is not None:
                self.shape_style_sect = shape_style[0]
                self.shape_style_key = shape_style[1]
            else:
                self.shape_style_sect = "plot"
                self.shape_style_key = "shape/drag"

        def reset(self):
            self.shape = None
            self.current_handle = None

        def create_shape(self, filter, pt):
            self.shape = PolygonShape(closed=False)
            filter.plot.add_item_with_z_offset(self.shape, SHAPE_Z_OFFSET)
            self.shape.setVisible(True)
            self.shape.set_style(self.shape_style_sect, self.shape_style_key)
            self.shape.add_local_point(pt)
            return self.shape.add_local_point(pt)

        def setup_filter(self, baseplot):
            filter = baseplot.filter
            # Initialisation du filtre
            start_state = filter.new_state()
            # Bouton gauche :
            handler = QtDragHandler(filter, Qt.LeftButton, start_state=start_state)
            filter.add_event(
                start_state,
                KeyEventMatch((Qt.Key_Enter, Qt.Key_Return, Qt.Key_Space)),
                self.validate,
                start_state,
            )
            filter.add_event(
                start_state,
                KeyEventMatch(
                    (
                        Qt.Key_Backspace,
                        Qt.Key_Escape,
                    )
                ),
                self.cancel_point,
                start_state,
            )
            handler.SIG_START_TRACKING.connect(self.mouse_press)
            handler.SIG_MOVE.connect(self.move)
            handler.SIG_STOP_NOT_MOVING.connect(self.mouse_release)
            handler.SIG_STOP_MOVING.connect(self.mouse_release)
            return setup_standard_tool_filter(filter, start_state)

        def validate(self, filter, event):
            super(MultiLineTool, self).validate(filter, event)
            if self.handle_final_shape_cb is not None:
                self.handle_final_shape_cb(self.shape)
            self.reset()

        def cancel_point(self, filter, event):
            if self.shape is None:
                return
            points = self.shape.get_points()
            if points is None:
                return
            elif len(points) <= 2:
                filter.plot.del_item(self.shape)
                self.reset()
            else:
                if self.current_handle:
                    newh = self.shape.del_point(self.current_handle)
                else:
                    newh = self.shape.del_point(-1)
                self.current_handle = newh
            filter.plot.replot()

        def mouse_press(self, filter, event):
            """We create a new shape if it's the first point
            otherwise we add a new point
            """
            if self.shape is None:
                self.init_pos = event.pos()
                self.current_handle = self.create_shape(filter, event.pos())
                filter.plot.replot()
            else:
                self.current_handle = self.shape.add_local_point(event.pos())

        def move(self, filter, event):
            """moving while holding the button down lets the user
            position the last created point
            """
            if self.shape is None or self.current_handle is None:
                # Error ??
                return
            self.shape.move_local_point_to(self.current_handle, event.pos())
            filter.plot.replot()

        def mouse_release(self, filter, event):
            """Releasing the mouse button validate the last point position"""
            if self.current_handle is None:
                return
            if self.init_pos is not None and self.init_pos == event.pos():
                self.shape.del_point(-1)
            else:
                self.shape.move_local_point_to(self.current_handle, event.pos())
            self.init_pos = None
            self.current_handle = None
            filter.plot.replot()

    class FreeFormTool(MultiLineTool):
        TITLE = _("Free form")
        ICON = "freeform.png"

        def cancel_point(self, filter, event):
            """Reimplement base class method"""
            super(FreeFormTool, self).cancel_point(filter, event)
            self.shape.closed = len(self.shape.points) > 2

        def mouse_press(self, filter, event):
            """Reimplement base class method"""
            super(FreeFormTool, self).mouse_press(filter, event)
            self.shape.closed = len(self.shape.points) > 2

    class LabelTool(InteractiveTool):
        TITLE = _("Label")
        ICON = "label.png"
        LABEL_STYLE_SECT = "plot"
        LABEL_STYLE_KEY = "label"
        SWITCH_TO_DEFAULT_TOOL = True

        def __init__(
            self,
            manager,
            handle_label_cb=None,
            label_style=None,
            toolbar_id=DefaultToolbarID,
            title=None,
            icon=None,
            tip=None,
            switch_to_default_tool=None,
        ):
            self.handle_label_cb = handle_label_cb
            super(LabelTool, self).__init__(
                manager,
                toolbar_id,
                title=title,
                icon=icon,
                tip=tip,
                switch_to_default_tool=switch_to_default_tool,
            )
            if label_style is not None:
                self.label_style_sect = label_style[0]
                self.label_style_key = label_style[1]
            else:
                self.label_style_sect = self.LABEL_STYLE_SECT
                self.label_style_key = self.LABEL_STYLE_KEY

        def set_label_style(self, label):
            label.set_style(self.label_style_sect, self.label_style_key)

        def setup_filter(self, baseplot):
            filter = baseplot.filter
            start_state = filter.new_state()
            handler = ClickHandler(filter, Qt.LeftButton, start_state=start_state)
            handler.SIG_CLICK_EVENT.connect(self.add_label_to_plot)
            return setup_standard_tool_filter(filter, start_state)

        def add_label_to_plot(self, filter, event):
            plot = filter.plot
            import guidata.dataset as ds

            class TextParam(ds.datatypes.DataSet):
                text = ds.dataitems.TextItem("", _("Label"))

            textparam = TextParam(_("Label text"))
            if textparam.edit(plot):
                text = textparam.text.replace("\n", "<br>")
                from guiqwt.builder import make

                label = make.label(text, (0, 0), (10, 10), "TL")
                self.set_label_style(label)
                label.setTitle(self.TITLE)
                x = plot.invTransform(label.xAxis(), event.pos().x())
                y = plot.invTransform(label.yAxis(), event.pos().y())
                label.set_pos(x, y)
                plot.add_item_with_z_offset(label, SHAPE_Z_OFFSET)
                if self.handle_label_cb is not None:
                    self.handle_label_cb(label)
                plot.replot()
                self.SIG_TOOL_JOB_FINISHED.emit()

    class RectangularActionTool(InteractiveTool):
        SHAPE_STYLE_SECT = "plot"
        SHAPE_STYLE_KEY = "shape/drag"
        AVOID_NULL_SHAPE = False

        def __init__(
            self,
            manager,
            func,
            shape_style=None,
            toolbar_id=DefaultToolbarID,
            title=None,
            icon=None,
            tip=None,
            fix_orientation=False,
            switch_to_default_tool=None,
        ):
            self.action_func = func
            self.fix_orientation = fix_orientation
            super(RectangularActionTool, self).__init__(
                manager,
                toolbar_id,
                title=title,
                icon=icon,
                tip=tip,
                switch_to_default_tool=switch_to_default_tool,
            )
            if shape_style is not None:
                self.shape_style_sect = shape_style[0]
                self.shape_style_key = shape_style[1]
            else:
                self.shape_style_sect = self.SHAPE_STYLE_SECT
                self.shape_style_key = self.SHAPE_STYLE_KEY
            self.last_final_shape = None
            self.switch_to_default_tool = switch_to_default_tool

        def get_last_final_shape(self):
            if self.last_final_shape is not None:
                return self.last_final_shape()

        def set_shape_style(self, shape):
            shape.set_style(self.shape_style_sect, self.shape_style_key)

        def create_shape(self):
            shape = RectangleShape(0, 0, 1, 1)
            self.set_shape_style(shape)
            return shape, 0, 2

        def setup_shape(self, shape):
            pass

        def get_shape(self):
            """Reimplemented RectangularActionTool method"""
            shape, h0, h1 = self.create_shape()
            self.setup_shape(shape)
            return shape, h0, h1

        def get_final_shape(self, plot, p0, p1):
            shape, h0, h1 = self.create_shape()
            self.setup_shape(shape)
            plot.add_item_with_z_offset(shape, SHAPE_Z_OFFSET)
            shape.move_local_point_to(h0, p0)
            shape.move_local_point_to(h1, p1)
            self.last_final_shape = weakref.ref(shape)
            return shape

        def get_selection_handler(self, filter, start_state):
            return RectangularSelectionHandler(
                filter, Qt.LeftButton, start_state=start_state
            )

        def setup_filter(self, baseplot):
            filter = baseplot.filter
            start_state = filter.new_state()
            handler = self.get_selection_handler(filter, start_state)
            shape, h0, h1 = self.get_shape()
            handler.set_shape(
                shape, h0, h1, self.setup_shape, avoid_null_shape=self.AVOID_NULL_SHAPE
            )
            handler.SIG_END_RECT.connect(self.end_rect)
            return setup_standard_tool_filter(filter, start_state)

        def end_rect(self, filter, p0, p1):
            plot = filter.plot
            if self.fix_orientation:
                left, right = min(p0.x(), p1.x()), max(p0.x(), p1.x())
                top, bottom = min(p0.y(), p1.y()), max(p0.y(), p1.y())
                self.action_func(plot, QPointF(left, top), QPointF(right, bottom))
            else:
                self.action_func(plot, p0, p1)
            self.SIG_TOOL_JOB_FINISHED.emit()
            if self.switch_to_default_tool:
                shape = self.get_last_final_shape()
                plot.set_active_item(shape)

    class RectangularShapeTool(RectangularActionTool):
        TITLE = None
        ICON = None

        def __init__(
            self,
            manager,
            setup_shape_cb=None,
            handle_final_shape_cb=None,
            shape_style=None,
            toolbar_id=DefaultToolbarID,
            title=None,
            icon=None,
            tip=None,
            switch_to_default_tool=None,
        ):
            super(RectangularShapeTool, self).__init__(
                manager,
                self.add_shape_to_plot,
                shape_style,
                toolbar_id=toolbar_id,
                title=title,
                icon=icon,
                tip=tip,
                switch_to_default_tool=switch_to_default_tool,
            )
            self.setup_shape_cb = setup_shape_cb
            self.handle_final_shape_cb = handle_final_shape_cb

        def add_shape_to_plot(self, plot, p0, p1):
            """
            Method called when shape's rectangular area
            has just been drawn on screen.
            Adding the final shape to plot and returning it.
            """
            shape = self.get_final_shape(plot, p0, p1)
            self.handle_final_shape(shape)
            plot.replot()

        def setup_shape(self, shape):
            """To be reimplemented"""
            shape.setTitle(self.TITLE)
            if self.setup_shape_cb is not None:
                self.setup_shape_cb(shape)

        def handle_final_shape(self, shape):
            """To be reimplemented"""
            if self.handle_final_shape_cb is not None:
                self.handle_final_shape_cb(shape)

    class RectangleTool(RectangularShapeTool):
        TITLE = _("Rectangle")
        ICON = "rectangle.png"

    class ObliqueRectangleTool(RectangularShapeTool):
        TITLE = _("Oblique rectangle")
        ICON = "oblique_rectangle.png"
        AVOID_NULL_SHAPE = True

        def create_shape(self):
            shape = ObliqueRectangleShape(1, 1, 2, 1, 2, 2, 1, 2)
            self.set_shape_style(shape)
            return shape, 0, 2

    class PointTool(RectangularShapeTool):
        TITLE = _("Point")
        ICON = "point_shape.png"
        SHAPE_STYLE_KEY = "shape/point"

        def create_shape(self):
            shape = PointShape(0, 0)
            self.set_shape_style(shape)
            return shape, 0, 0

        def get_selection_handler(self, filter, start_state):
            return PointSelectionHandler(filter, Qt.LeftButton, start_state=start_state)

    class SegmentTool(RectangularShapeTool):
        TITLE = _("Segment")
        ICON = "segment.png"
        SHAPE_STYLE_KEY = "shape/segment"

        def create_shape(self):
            shape = SegmentShape(0, 0, 1, 1)
            self.set_shape_style(shape)
            return shape, 0, 1

    class CircleTool(RectangularShapeTool):
        TITLE = _("Circle")
        ICON = "circle.png"

        def create_shape(self):
            shape = EllipseShape(0, 0, 1, 1)
            self.set_shape_style(shape)
            return shape, 0, 1

    class EllipseTool(RectangularShapeTool):
        TITLE = _("Ellipse")
        ICON = "ellipse_shape.png"

        def create_shape(self):
            shape = EllipseShape(0, 0, 1, 1)
            self.set_shape_style(shape)
            return shape, 0, 1

        def handle_final_shape(self, shape):
            shape.switch_to_ellipse()
            super(EllipseTool, self).handle_final_shape(shape)

    class PlaceAxesTool(RectangularShapeTool):
        TITLE = _("Axes")
        ICON = "gtaxes.png"
        SHAPE_STYLE_KEY = "shape/axes"

        def create_shape(self):
            shape = Axes((0, 1), (1, 1), (0, 0))
            self.set_shape_style(shape)
            return shape, 0, 2

    class AnnotatedRectangleTool(RectangleTool):
        def create_shape(self):
            annotation = AnnotatedRectangle(0, 0, 1, 1)
            self.set_shape_style(annotation)
            return annotation, 0, 2

    class AnnotatedObliqueRectangleTool(ObliqueRectangleTool):
        AVOID_NULL_SHAPE = True

        def create_shape(self):
            annotation = AnnotatedObliqueRectangle(0, 0, 1, 0, 1, 1, 0, 1)
            self.set_shape_style(annotation)
            return annotation, 0, 2

    class AnnotatedCircleTool(CircleTool):
        def create_shape(self):
            annotation = AnnotatedCircle(0, 0, 1, 1)
            self.set_shape_style(annotation)
            return annotation, 0, 1

    class AnnotatedEllipseTool(EllipseTool):
        def create_shape(self):
            annotation = AnnotatedEllipse(0, 0, 1, 1)
            annotation.shape.switch_to_circle()
            self.set_shape_style(annotation)
            return annotation, 0, 1

        def handle_final_shape(self, shape):
            shape.shape.switch_to_ellipse()
            super(EllipseTool, self).handle_final_shape(shape)

    class AnnotatedPointTool(PointTool):
        def create_shape(self):
            annotation = AnnotatedPoint(0, 0)
            self.set_shape_style(annotation)
            return annotation, 0, 0

    class AnnotatedSegmentTool(SegmentTool):
        def create_shape(self):
            annotation = AnnotatedSegment(0, 0, 1, 1)
            self.set_shape_style(annotation)
            return annotation, 0, 1

    class ImageStatsRectangle(AnnotatedRectangle):
        def __init__(self, x1=0, y1=0, x2=0, y2=0, annotationparam=None):
            super(ImageStatsRectangle, self).__init__(x1, y1, x2, y2, annotationparam)
            self.image_item = None

        def set_image_item(self, image_item):
            self.image_item = image_item

        # ----AnnotatedShape API-----------------------------------------------------
        def get_infos(self):
            """Return formatted string with informations on current shape"""
            if self.image_item is not None:
                return self.image_item.get_stats(*self.get_rect())

    def update_image_tool_status(tool, plot):
        from guiqwt.image import ImagePlot

        enabled = isinstance(plot, ImagePlot)
        tool.action.setEnabled(enabled)
        return enabled

    class ImageStatsTool(RectangularShapeTool):
        SWITCH_TO_DEFAULT_TOOL = True
        TITLE = _("Image statistics")
        ICON = "imagestats.png"
        SHAPE_STYLE_KEY = "shape/image_stats"

        def __init__(
            self,
            manager,
            setup_shape_cb=None,
            handle_final_shape_cb=None,
            shape_style=None,
            toolbar_id=DefaultToolbarID,
            title=None,
            icon=None,
            tip=None,
        ):
            super(ImageStatsTool, self).__init__(
                manager,
                setup_shape_cb,
                handle_final_shape_cb,
                shape_style,
                toolbar_id,
                title,
                icon,
                tip,
            )
            self._last_item = None

        def get_last_item(self):
            if self._last_item is not None:
                return self._last_item()

        def create_shape(self):
            return ImageStatsRectangle(0, 0, 1, 1), 0, 2

        def setup_shape(self, shape):
            super(ImageStatsTool, self).setup_shape(shape)
            shape.setTitle("")
            self.set_shape_style(shape)
            self.register_shape(shape, final=False)

        def register_shape(self, shape, final=False):
            plot = shape.plot()
            if plot is not None:
                plot.unselect_all()
                plot.set_active_item(shape)
                shape.set_image_item(self.get_last_item())

        def handle_final_shape(self, shape):
            super(ImageStatsTool, self).handle_final_shape(shape)
            self.register_shape(shape, final=True)

        def get_associated_item(self, plot):
            items = plot.get_selected_items(item_type=IStatsImageItemType)
            if len(items) == 1:
                self._last_item = weakref.ref(items[0])
            return self.get_last_item()

        def update_status(self, plot):
            if update_image_tool_status(self, plot):
                item = self.get_associated_item(plot)
                self.action.setEnabled(item is not None)

    class CrossSectionTool(RectangularShapeTool):
        SWITCH_TO_DEFAULT_TOOL = True
        TITLE = _("Cross section")
        ICON = "csection.png"
        SHAPE_STYLE_KEY = "shape/cross_section"
        SHAPE_TITLE = TITLE
        PANEL_IDS = (ID_XCS, ID_YCS)

        def create_shape(self):
            return AnnotatedPoint(0, 0), 0, 0

        def setup_shape(self, shape):
            self.setup_shape_appearance(shape)
            super(CrossSectionTool, self).setup_shape(shape)
            self.register_shape(shape, final=False)

        def setup_shape_appearance(self, shape):
            self.set_shape_style(shape)
            param = shape.annotationparam
            param.title = self.SHAPE_TITLE
            #        param.show_computations = False
            param.update_annotation(shape)

        def register_shape(self, shape, final=False):
            plot = shape.plot()
            if plot is not None:
                plot.unselect_all()
                plot.set_active_item(shape)
            for panel_id in self.PANEL_IDS:
                panel = self.manager.get_panel(panel_id)
                if panel is not None:
                    panel.register_shape(shape, final=final)

        def activate(self):
            """Activate tool"""
            super(CrossSectionTool, self).activate()
            for panel_id in self.PANEL_IDS:
                panel = self.manager.get_panel(panel_id)
                panel.setVisible(True)
                shape = self.get_last_final_shape()
                if shape is not None:
                    panel.update_plot(shape)

        def handle_final_shape(self, shape):
            super(CrossSectionTool, self).handle_final_shape(shape)
            self.register_shape(shape, final=True)

        def update_status(self, plot):
            if update_image_tool_status(self, plot):
                item = plot.get_selected_items(item_type=IImageItemType)
                self.action.setEnabled(len(item) > 0)

    class AverageCrossSectionTool(CrossSectionTool):
        SWITCH_TO_DEFAULT_TOOL = True
        TITLE = _("Average cross section")
        ICON = "csection_a.png"
        SHAPE_STYLE_KEY = "shape/average_cross_section"
        SHAPE_TITLE = TITLE

        def create_shape(self):
            return AnnotatedRectangle(0, 0, 1, 1), 0, 2

    class ObliqueCrossSectionTool(CrossSectionTool):
        SWITCH_TO_DEFAULT_TOOL = True
        TITLE = _("Oblique averaged cross section")
        ICON = "csection_oblique.png"
        SHAPE_STYLE_KEY = "shape/average_cross_section"
        SHAPE_TITLE = TITLE
        PANEL_IDS = (ID_OCS,)

        def create_shape(self):
            annotation = AnnotatedObliqueRectangle(0, 0, 1, 0, 1, 1, 0, 1)
            self.set_shape_style(annotation)
            return annotation, 0, 2

    class RectZoomTool(InteractiveTool):
        TITLE = _("Rectangle zoom")
        ICON = "magnifier.png"

        def setup_filter(self, baseplot):
            filter = baseplot.filter
            start_state = filter.new_state()
            handler = ZoomRectHandler(filter, Qt.LeftButton, start_state=start_state)
            shape, h0, h1 = self.get_shape()
            handler.set_shape(shape, h0, h1)
            return setup_standard_tool_filter(filter, start_state)

        def get_shape(self):
            shape = RectangleShape(0, 0, 1, 1)
            shape.set_style("plot", "shape/rectzoom")
            return shape, 0, 2

    class BaseCursorTool(InteractiveTool):
        TITLE = None
        ICON = None

        def __init__(
            self,
            manager,
            toolbar_id=DefaultToolbarID,
            title=None,
            icon=None,
            tip=None,
            switch_to_default_tool=None,
        ):
            super(BaseCursorTool, self).__init__(
                manager,
                toolbar_id,
                title=title,
                icon=icon,
                tip=tip,
                switch_to_default_tool=switch_to_default_tool,
            )
            self.shape = None

        def create_shape(self):
            """Create and return the cursor/range shape"""
            raise NotImplementedError

        def setup_filter(self, baseplot):
            filter = baseplot.filter
            # Initialisation du filtre
            start_state = filter.new_state()
            # Bouton gauche :
            self.handler = QtDragHandler(filter, Qt.LeftButton, start_state=start_state)
            self.handler.SIG_MOVE.connect(self.move)
            self.handler.SIG_STOP_NOT_MOVING.connect(self.end_move)
            self.handler.SIG_STOP_MOVING.connect(self.end_move)
            return setup_standard_tool_filter(filter, start_state)

        def move(self, filter, event):
            plot = filter.plot
            if not self.shape:
                self.shape = self.create_shape()
                self.shape.attach(plot)
                self.shape.setZ(plot.get_max_z() + 1)
                self.shape.move_local_point_to(0, event.pos())
                self.shape.setVisible(True)
            self.shape.move_local_point_to(1, event.pos())
            plot.replot()

        def end_move(self, filter, event):
            if self.shape is not None:
                assert self.shape.plot() == filter.plot
                filter.plot.add_item_with_z_offset(self.shape, SHAPE_Z_OFFSET)
                self.shape = None
                self.SIG_TOOL_JOB_FINISHED.emit()

    class HRangeTool(BaseCursorTool):
        TITLE = _("Horizontal selection")
        ICON = "xrange.png"

        def create_shape(self):
            from guiqwt.shapes import XRangeSelection

            return XRangeSelection(0, 0)

    class VCursorTool(BaseCursorTool):
        TITLE = _("Vertical cursor")
        ICON = "vcursor.png"

        def create_shape(self):
            from guiqwt.shapes import Marker

            marker = Marker()
            marker.set_markerstyle("|")
            return marker

    class HCursorTool(BaseCursorTool):
        TITLE = _("Horizontal cursor")
        ICON = "hcursor.png"

        def create_shape(self):
            from guiqwt.shapes import Marker

            marker = Marker()
            marker.set_markerstyle("-")
            return marker

    class XCursorTool(BaseCursorTool):
        TITLE = _("Cross cursor")
        ICON = "xcursor.png"

        def create_shape(self):
            from guiqwt.shapes import Marker

            marker = Marker()
            marker.set_markerstyle("+")
            return marker

    class SignalStatsTool(BaseCursorTool):
        TITLE = _("Signal statistics")
        ICON = "xrange.png"
        SWITCH_TO_DEFAULT_TOOL = True

        def __init__(
            self, manager, toolbar_id=DefaultToolbarID, title=None, icon=None, tip=None
        ):
            super(SignalStatsTool, self).__init__(
                manager, toolbar_id, title=title, icon=icon, tip=tip
            )
            self._last_item = None
            self.label = None

        def get_last_item(self):
            if self._last_item is not None:
                return self._last_item()

        def create_shape(self):
            from guiqwt.shapes import XRangeSelection

            return XRangeSelection(0, 0)

        def move(self, filter, event):
            super(SignalStatsTool, self).move(filter, event)
            if self.label is None:
                plot = filter.plot
                curve = self.get_associated_item(plot)
                from guiqwt.builder import make

                self.label = make.computations(
                    self.shape,
                    "TL",
                    [
                        (
                            curve,
                            "%g &lt; x &lt; %g",
                            lambda *args: (args[0].min(), args[0].max()),
                        ),
                        (
                            curve,
                            "%g &lt; y &lt; %g",
                            lambda *args: (args[1].min(), args[1].max()),
                        ),
                        (curve, "&lt;y&gt;=%g", lambda *args: args[1].mean()),
                        (curve, "σ(y)=%g", lambda *args: args[1].std()),
                        (curve, "∑(y)=%g", lambda *args: np.trapz(args[1])),
                        (curve, "∫ydx=%g", lambda *args: np.trapz(args[1], args[0])),
                    ],
                )
                self.label.attach(plot)
                self.label.setZ(plot.get_max_z() + 1)
                self.label.setVisible(True)

        def end_move(self, filter, event):
            super(SignalStatsTool, self).end_move(filter, event)
            if self.label is not None:
                filter.plot.add_item_with_z_offset(self.label, SHAPE_Z_OFFSET)
                self.label = None

        def get_associated_item(self, plot):
            items = plot.get_selected_items(item_type=ICurveItemType)
            if len(items) == 1:
                self._last_item = weakref.ref(items[0])
            return self.get_last_item()

        def update_status(self, plot):
            item = self.get_associated_item(plot)
            self.action.setEnabled(item is not None)

    class DummySeparatorTool(GuiTool):
        def __init__(self, manager, toolbar_id=DefaultToolbarID):
            super(DummySeparatorTool, self).__init__(manager, toolbar_id)

        def setup_toolbar(self, toolbar):
            """Setup tool's toolbar"""
            add_separator(toolbar)

    class CommandTool(GuiTool):
        """Base class for command tools: action, context menu entry"""

        CHECKABLE = False

        def __init__(
            self, manager, title, icon=None, tip=None, toolbar_id=DefaultToolbarID
        ):
            self.title = title
            if icon and isinstance(icon, str):
                self.icon = get_icon(icon)
            else:
                self.icon = icon
            self.tip = tip
            super(CommandTool, self).__init__(manager, toolbar_id)

        def create_action(self, manager):
            """Create and return tool's action"""
            return manager.create_action(
                self.title,
                icon=self.icon,
                tip=self.tip,
                triggered=self.activate,
                checkable=self.CHECKABLE,
            )

        def setup_context_menu(self, menu, plot):
            if self.menu is not None:
                self.menu.setTitle(self.action.text())
                menu.addMenu(self.menu)
            else:
                menu.addAction(self.action)

        def activate(self, checked=True):
            plot = self.get_active_plot()
            if plot is not None:
                self.activate_command(plot, checked)

        def set_status_active_item(self, plot):
            item = plot.get_active_item()
            if item:
                self.action.setEnabled(True)
            else:
                self.action.setEnabled(False)

    class ToggleTool(CommandTool):
        CHECKABLE = True

        def __init__(self, manager, title, icon=None, tip=None, toolbar_id=None):
            super(ToggleTool, self).__init__(manager, title, icon, tip, toolbar_id)

    class BasePlotMenuTool(CommandTool):
        """
        A tool that gather parameter panels from the BasePlot
        and proposes to edit them and set them back
        """

        def __init__(
            self, manager, key, title=None, icon=None, tip=None, toolbar_id=DefaultToolbarID
        ):
            from guiqwt.baseplot import PARAMETERS_TITLE_ICON

            default_title, default_icon = PARAMETERS_TITLE_ICON[key]
            if title is None:
                title = default_title
            if icon is None:
                icon = default_icon
            super(BasePlotMenuTool, self).__init__(manager, title, icon, tip, toolbar_id)
            # Warning: icon (str) --(Base class constructor)--> self.icon (QIcon)
            self.key = key

        def activate_command(self, plot, checked):
            """Activate tool"""
            plot.edit_plot_parameters(self.key)

        def update_status(self, plot):
            status = plot.get_plot_parameters_status(self.key)
            self.action.setEnabled(status)

    class AntiAliasingTool(ToggleTool):
        def __init__(self, manager):
            super(AntiAliasingTool, self).__init__(manager, _("Antialiasing (curves)"))

        def activate_command(self, plot, checked):
            """Activate tool"""
            plot.set_antialiasing(checked)
            plot.replot()

        def update_status(self, plot):
            self.action.setChecked(plot.antialiased)

    class DisplayCoordsTool(CommandTool):
        def __init__(self, manager):
            super(DisplayCoordsTool, self).__init__(
                manager,
                _("Markers"),
                icon=get_icon("on_curve.png"),
                tip=None,
                toolbar_id=None,
            )
            self.action.setEnabled(True)

        def activate_canvas_pointer(self, enable):
            plot = self.get_active_plot()
            if plot is not None:
                plot.set_pointer("canvas" if enable else None)

        def activate_curve_pointer(self, enable):
            plot = self.get_active_plot()
            if plot is not None:
                plot.set_pointer("curve" if enable else None)

        def update_status(self, plot):
            self.canvas_act.setChecked(plot.canvas_pointer)
            self.curve_act.setChecked(plot.curve_pointer)

    class ReverseYAxisTool(ToggleTool):
        def __init__(self, manager):
            super(ReverseYAxisTool, self).__init__(manager, _("Reverse Y axis"))

        def activate_command(self, plot, checked):
            """Activate tool"""
            plot.set_axis_direction("left", checked)
            plot.replot()

        def update_status(self, plot):
            if update_image_tool_status(self, plot):
                self.action.setChecked(plot.get_axis_direction("left"))

    class AspectRatioParam(DataSet):
        lock = BoolItem(_("Lock aspect ratio"))
        current = FloatItem(_("Current value")).set_prop("display", active=False)
        ratio = FloatItem(_("Lock value"), min=1e-3)

    class AspectRatioTool(CommandTool):
        def __init__(self, manager):
            super(AspectRatioTool, self).__init__(
                manager, _("Aspect ratio"), tip=None, toolbar_id=None
            )
            self.action.setEnabled(True)

        def set_aspect_ratio_1_1(self):
            plot = self.get_active_plot()
            if plot is not None:
                plot.set_aspect_ratio(ratio=1)
                plot.replot()

        def activate_command(self, plot, checked):
            """Activate tool"""
            pass

        def __update_actions(self, checked):
            self.ar_param.lock = checked
            #        self.lock_action.blockSignals(True)
            self.lock_action.setChecked(checked)
            #        self.lock_action.blockSignals(False)
            plot = self.get_active_plot()
            if plot is not None:
                ratio = plot.get_aspect_ratio()
                self.ratio1_action.setEnabled(checked and ratio != 1.0)

        def lock_aspect_ratio(self, checked):
            """Lock aspect ratio"""
            plot = self.get_active_plot()
            if plot is not None:
                plot.set_aspect_ratio(lock=checked)
                self.__update_actions(checked)
                plot.replot()

        def edit_aspect_ratio(self):
            plot = self.get_active_plot()
            if plot is not None:
                self.ar_param.lock = plot.lock_aspect_ratio
                self.ar_param.ratio = plot.get_aspect_ratio()
                self.ar_param.current = plot.get_current_aspect_ratio()
                if self.ar_param.edit(parent=plot):
                    lock, ratio = self.ar_param.lock, self.ar_param.ratio
                    plot.set_aspect_ratio(ratio=ratio, lock=lock)
                    self.__update_actions(lock)
                    plot.replot()

        def update_status(self, plot):
            if update_image_tool_status(self, plot):
                ratio = plot.get_aspect_ratio()
                lock = plot.lock_aspect_ratio
                self.ar_param.ratio, self.ar_param.lock = ratio, lock
                self.__update_actions(lock)

    class PanelTool(ToggleTool):
        panel_id = None
        panel_name = None

        def __init__(self, manager):
            super(PanelTool, self).__init__(manager, self.panel_name)
            manager.get_panel(self.panel_id).SIG_VISIBILITY_CHANGED.connect(
                self.action.setChecked
            )

        def activate_command(self, plot, checked):
            """Activate tool"""
            panel = self.manager.get_panel(self.panel_id)
            panel.setVisible(checked)

        def update_status(self, plot):
            panel = self.manager.get_panel(self.panel_id)
            self.action.setChecked(panel.isVisible())

    class ContrastPanelTool(PanelTool):
        panel_name = _("Contrast adjustment")
        panel_id = ID_CONTRAST

        def update_status(self, plot):
            super(ContrastPanelTool, self).update_status(plot)
            update_image_tool_status(self, plot)
            item = plot.get_last_active_item(IVoiImageItemType)
            panel = self.manager.get_panel(self.panel_id)
            for action in panel.toolbar.actions():
                if isinstance(action, QAction):
                    action.setEnabled(item is not None)

    class XCSPanelTool(PanelTool):
        panel_name = _("X-axis cross section")
        panel_id = ID_XCS

    class YCSPanelTool(PanelTool):
        panel_name = _("Y-axis cross section")
        panel_id = ID_YCS

    class OCSPanelTool(PanelTool):
        panel_name = _("Oblique averaged cross section")
        panel_id = ID_OCS

    class ItemListPanelTool(PanelTool):
        panel_name = _("Item list")
        panel_id = ID_ITEMLIST

    class SaveAsTool(CommandTool):
        def __init__(self, manager, toolbar_id=DefaultToolbarID):
            super(SaveAsTool, self).__init__(
                manager,
                _("Save as..."),
                get_std_icon("DialogSaveButton", 16),
                toolbar_id=toolbar_id,
            )

        def activate_command(self, plot, checked):
            """Activate tool"""
            # FIXME: Qt's PDF printer is unable to print plots including images
            # --> until this bug is fixed internally, disabling PDF output format
            #     when plot has image items.
            formats = "%s (*.png)" % _("PNG image")

            for item in plot.get_items():
                if IImageItemType in item.types():
                    break
            else:
                formats += "\n%s (*.pdf)" % _("PDF document")
            fname, _f = getsavefilename(plot, _("Save as"), _("untitled"), formats)
            if fname:
                plot.save_widget(fname)

    class CopyToClipboardTool(CommandTool):
        def __init__(self, manager, toolbar_id=DefaultToolbarID):
            super(CopyToClipboardTool, self).__init__(
                manager,
                _("Copy to clipboard"),
                get_icon("copytoclipboard.png"),
                toolbar_id=toolbar_id,
            )

        def activate_command(self, plot, checked):
            """Activate tool"""
            plot.copy_to_clipboard()

    def save_snapshot(plot, p0, p1, new_size=None):
        """
        Save rectangular plot area
        p0, p1: resp. top left and bottom right points (`QPointF` objects)
        new_size: destination image size (tuple: (width, height))
        """
        from guiqwt.image import (
            get_image_from_plot,
            get_plot_qrect,
            get_items_in_rectangle,
            compute_trimageitems_original_size,
        )
        from guiqwt import io

        items = get_items_in_rectangle(plot, p0, p1)
        if not items:
            QMessageBox.critical(
                plot,
                _("Rectangle snapshot"),
                _("There is no supported image item in current selection."),
            )
            return
        src_x, src_y, src_w, src_h = get_plot_qrect(plot, p0, p1).getRect()
        original_size = compute_trimageitems_original_size(items, src_w, src_h)

        if new_size is None:
            new_size = (int(p1.x() - p0.x() + 1), int(p1.y() - p0.y() + 1))  # Screen size

        from guiqwt.widgets.resizedialog import ResizeDialog

        dlg = ResizeDialog(
            plot, new_size=new_size, old_size=original_size, text=_("Destination size:")
        )
        if not dlg.exec_():
            return

        from guidata.dataset.datatypes import DataSet, BeginGroup, EndGroup
        from guidata.dataset.dataitems import BoolItem, ChoiceItem

        class SnapshotParam(DataSet):
            _levels = BeginGroup(_("Image levels adjustments"))
            apply_contrast = BoolItem(_("Apply contrast settings"), default=False)
            apply_interpolation = BoolItem(_("Apply interpolation algorithm"), default=True)
            norm_range = BoolItem(_("Scale levels to maximum range"), default=False)
            _end_levels = EndGroup(_("Image levels adjustments"))
            _multiple = BeginGroup(_("Superimposed images"))
            add_images = ChoiceItem(
                _("If image B is behind image A, " "replace intersection by"),
                [(False, "A"), (True, "A+B")],
                default=None,
            )
            _end_multiple = EndGroup(_("Superimposed images"))

        param = SnapshotParam(_("Rectangle snapshot"))
        if not param.edit(parent=plot):
            return

        if dlg.keep_original_size:
            destw, desth = original_size
        else:
            destw, desth = dlg.width, dlg.height

        try:
            data = get_image_from_plot(
                plot,
                p0,
                p1,
                destw=destw,
                desth=desth,
                add_images=param.add_images,
                apply_lut=param.apply_contrast,
                apply_interpolation=param.apply_interpolation,
                original_resolution=dlg.keep_original_size,
            )

            dtype = None
            for item in items:
                if dtype is None or item.data.dtype.itemsize > dtype.itemsize:
                    dtype = item.data.dtype
            if param.norm_range:
                data = io.scale_data_to_dtype(data, dtype=dtype)
            else:
                data = np.array(data, dtype=dtype)
        except MemoryError:
            mbytes = int(destw * desth * 32.0 / (8 * 1024**2))
            QMessageBox.critical(
                plot,
                _("Memory error"),
                _(
                    "There is not enough memory left to process "
                    "this %d x %d image (%d MB would be required)."
                )
                % (destw, desth, mbytes),
            )
            return
        for model_item in items:
            model_fname = model_item.get_filename()
            if model_fname is not None and model_fname.lower().endswith(".dcm"):
                break
        else:
            model_fname = None
        fname, _f = getsavefilename(
            plot,
            _("Save as"),
            _("untitled"),
            io.iohandler.get_filters("save", data.dtype, template=True),
        )
        _base, ext = osp.splitext(fname)
        options = {}
        if not fname:
            return
        elif ext.lower() == ".png":
            options.update(dict(dtype=np.uint8, max_range=True))
        elif ext.lower() == ".dcm":
            from pydicom import dicomio

            model_dcm = dicomio.read_file(model_fname)
            try:
                ps_attr = "ImagerPixelSpacing"
                ps_x, ps_y = getattr(model_dcm, ps_attr)
            except AttributeError:
                ps_attr = "PixelSpacing"
                ps_x, ps_y = getattr(model_dcm, ps_attr)
            model_dcm.Rows, model_dcm.Columns = data.shape

            dest_height, dest_width = data.shape
            (
                _x,
                _y,
                _angle,
                model_dx,
                model_dy,
                _hflip,
                _vflip,
            ) = model_item.get_transform()
            new_ps_x = ps_x * src_w / (model_dx * dest_width)
            new_ps_y = ps_y * src_h / (model_dy * dest_height)
            setattr(model_dcm, ps_attr, [new_ps_x, new_ps_y])
            options.update(dict(template=model_dcm))
        io.imwrite(fname, data, **options)

    class SnapshotTool(RectangularActionTool):
        SWITCH_TO_DEFAULT_TOOL = True
        TITLE = _("Rectangle snapshot")
        ICON = "snapshot.png"

        def __init__(self, manager, toolbar_id=DefaultToolbarID):
            super(SnapshotTool, self).__init__(
                manager, save_snapshot, toolbar_id=toolbar_id, fix_orientation=True
            )

    class RotateCropTool(CommandTool):
        """Rotate & Crop tool

        See :py:class:`guiqwt.rotatecrop.RotateCropDialog` dialog."""

        def __init__(self, manager, toolbar_id=DefaultToolbarID, options=None):
            super(RotateCropTool, self).__init__(
                manager,
                title=_("Rotate and crop"),
                icon=get_icon("rotate.png"),
                toolbar_id=toolbar_id,
            )
            self.options = options

        def activate_command(self, plot, checked):
            """Activate tool"""
            from guiqwt.image import TrImageItem
            from guiqwt.widgets.rotatecrop import RotateCropDialog

            for item in plot.get_selected_items():
                if isinstance(item, TrImageItem):
                    z = item.z()
                    plot.del_item(item)
                    dlg = RotateCropDialog(plot.parent(), options=self.options)
                    dlg.set_item(item)
                    ok = dlg.exec_()
                    plot.add_item(item, z=z)
                    if not ok:
                        break

        def update_status(self, plot):
            from guiqwt.image import TrImageItem

            status = any(
                [isinstance(item, TrImageItem) for item in plot.get_selected_items()]
            )
            self.action.setEnabled(status)

    class PrintTool(CommandTool):
        def __init__(self, manager, toolbar_id=DefaultToolbarID):
            super(PrintTool, self).__init__(
                manager, _("Print..."), get_icon("print.png"), toolbar_id=toolbar_id
            )

        def activate_command(self, plot, checked):
            """Activate tool"""
            printer = QPrinter()
            dialog = QPrintDialog(printer, plot)
            saved_in, saved_out, saved_err = sys.stdin, sys.stdout, sys.stderr
            sys.stdout = None
            ok = dialog.exec_()
            sys.stdin, sys.stdout, sys.stderr = saved_in, saved_out, saved_err
            if ok:
                plot.print_(printer)

    class OpenFileTool(CommandTool):

        #: Signal emitted by OpenFileTool when a file was opened (arg: filename)
        SIG_OPEN_FILE = Signal(str)

        def __init__(
            self, manager, title=_("Open..."), formats="*.*", toolbar_id=DefaultToolbarID
        ):
            super(OpenFileTool, self).__init__(
                manager, title, get_std_icon("DialogOpenButton", 16), toolbar_id=toolbar_id
            )
            self.formats = formats
            self.directory = ""

        def get_filename(self, plot):
            saved_in, saved_out, saved_err = sys.stdin, sys.stdout, sys.stderr
            sys.stdout = None
            filename, _f = getopenfilename(plot, _("Open"), self.directory, self.formats)
            sys.stdin, sys.stdout, sys.stderr = saved_in, saved_out, saved_err
            filename = str(filename)
            if filename:
                self.directory = osp.dirname(filename)
            return filename

        def activate_command(self, plot, checked):
            """Activate tool"""
            filename = self.get_filename(plot)
            if filename:
                self.SIG_OPEN_FILE.emit(filename)

    class SaveItemsTool(CommandTool):
        def __init__(self, manager, toolbar_id=DefaultToolbarID):
            super(SaveItemsTool, self).__init__(
                manager,
                _("Save items"),
                get_std_icon("DialogSaveButton", 16),
                toolbar_id=toolbar_id,
            )

        def activate_command(self, plot, checked):
            """Activate tool"""
            fname, _f = getsavefilename(
                plot, _("Save items as"), _("untitled"), "%s (*.gui)" % _("guiqwt items")
            )
            if not fname:
                return
            itemfile = open(fname, "wb")
            plot.save_items(itemfile, selected=True)

    class LoadItemsTool(OpenFileTool):
        def __init__(self, manager, toolbar_id=DefaultToolbarID):
            super(LoadItemsTool, self).__init__(
                manager, title=_("Load items"), formats="*.gui", toolbar_id=toolbar_id
            )

        def activate_command(self, plot, checked):
            """Activate tool"""
            filename = self.get_filename(plot)
            if not filename:
                return
            itemfile = open(filename, "rb")
            plot.restore_items(itemfile)
            plot.replot()

    class OpenImageTool(OpenFileTool):
        def __init__(self, manager, toolbar_id=DefaultToolbarID):
            from guiqwt import io

            super(OpenImageTool, self).__init__(
                manager,
                title=_("Open image"),
                formats=io.iohandler.get_filters("load"),
                toolbar_id=toolbar_id,
            )

    class AxisScaleTool(CommandTool):
        def __init__(self, manager):
            super(AxisScaleTool, self).__init__(
                manager, _("Scale"), icon=get_icon("log_log.png"), tip=None, toolbar_id=None
            )
            self.action.setEnabled(True)

        def update_status(self, plot):
            item = plot.get_active_item()
            active_scale = ("lin", "lin")
            if item is not None:
                xscale = plot.get_axis_scale(item.xAxis())
                yscale = plot.get_axis_scale(item.yAxis())
                active_scale = xscale, yscale
            for scale_type, scale_action in list(self.scale_menu.items()):
                if item is None:
                    scale_action.setEnabled(False)
                else:
                    scale_action.setEnabled(True)
                    if active_scale == scale_type:
                        scale_action.setChecked(True)
                    else:
                        scale_action.setChecked(False)

        def set_scale(self, checked, xscale, yscale):
            if not checked:
                return
            plot = self.get_active_plot()
            if plot is not None:
                cur_xscale, cur_yscale = plot.get_scales()
                if cur_xscale != xscale or cur_yscale != yscale:
                    plot.set_scales(xscale, yscale)

    class HelpTool(CommandTool):
        def __init__(self, manager, toolbar_id=DefaultToolbarID):
            super(HelpTool, self).__init__(
                manager,
                _("Help"),
                get_std_icon("DialogHelpButton", 16),
                toolbar_id=toolbar_id,
            )

        def activate_command(self, plot, checked):
            """Activate tool"""
            QMessageBox.information(
                plot,
                _("Help"),
                _(
                    """<b>Keyboard/mouse shortcuts:</b><br><br>
      - <u>single left-click</u>: item (curve, image, ...) selection<br>
      - <u>single right-click</u>: context-menu relative to selected item<br>
      - <u>shift</u>: on-active-curve (or image) cursor (+ <u>control</u> to maintain cursor visible)<br>
      - <u>shift + control</u>: on-active-curve cursor (+ <u>control</u> to maintain cursor visible)<br>
      - <u>alt</u>: free cursor<br>
      - <u>left-click + mouse move</u>: move item (when available)<br>
      - <u>middle-click + mouse move</u>: pan<br>
      - <u>right-click + mouse move</u>: zoom"""
                ),
            )

    class AboutTool(CommandTool):
        def __init__(self, manager, toolbar_id=DefaultToolbarID):
            super(AboutTool, self).__init__(
                manager, _("About") + " guiqwt", get_icon("guiqwt.svg"), toolbar_id=None
            )

        def activate_command(self, plot, checked):
            """Activate tool"""
            from guiqwt import about

            QMessageBox.about(plot, _("About") + " guiqwt", about(html=True))

    class ItemManipulationBaseTool(CommandTool):
        TITLE = None
        ICON = None
        TIP = None

        def __init__(self, manager, toolbar_id, curve_func, image_func):
            super(ItemManipulationBaseTool, self).__init__(
                manager, self.TITLE, icon=self.ICON, tip=self.TIP, toolbar_id=toolbar_id
            )
            self.curve_func = curve_func
            self.image_func = image_func

        def get_supported_items(self, plot):
            all_items = [
                item
                for item in plot.get_items(item_type=ICurveItemType)
                if not item.is_empty()
            ]
            from guiqwt.image import ImageItem

            all_items += [
                item
                for item in plot.get_items()
                if isinstance(item, ImageItem) and not item.is_empty()
            ]
            if len(all_items) == 1:
                return all_items
            else:
                return [item for item in all_items if item in plot.get_selected_items()]

        def update_status(self, plot):
            self.action.setEnabled(len(self.get_supported_items(plot)) > 0)

        def activate_command(self, plot, checked):
            """Activate tool"""
            for item in self.get_supported_items(plot):
                if ICurveItemType in item.types():
                    self.curve_func(item)
                else:
                    self.image_func(item)
            plot.replot()

    def export_curve_data(item):
        """Export curve item data to text file"""
        item_data = item.get_data()
        if len(item_data) > 2:
            x, y, dx, dy = item_data
            array_list = [x, y]
            if dx is not None:
                array_list.append(dx)
            if dy is not None:
                array_list.append(dy)
            data = np.array(array_list).T
        else:
            x, y = item_data
            data = np.array([x, y]).T
        plot = item.plot()
        title = _("Export")
        if item.curveparam.label:
            title += " (%s)" % item.curveparam.label
        fname, _f = getsavefilename(plot, title, "", _("Text file") + " (*.txt)")
        if fname:
            try:
                np.savetxt(str(fname), data, delimiter=",")
            except RuntimeError as error:
                QMessageBox.critical(
                    plot,
                    _("Export"),
                    _("Unable to export item data.")
                    + "<br><br>"
                    + _("Error message:")
                    + "<br>"
                    + str(error),
                )

    def export_image_data(item):
        """Export image item data to file"""
        from guiqwt.qthelpers import exec_image_save_dialog

        exec_image_save_dialog(item.plot(), item.data)

    class ExportItemDataTool(ItemManipulationBaseTool):
        TITLE = _("Export data...")
        ICON = "export.png"

        def __init__(self, manager, toolbar_id=None):
            super(ExportItemDataTool, self).__init__(
                manager,
                toolbar_id,
                curve_func=export_curve_data,
                image_func=export_image_data,
            )

    def edit_curve_data(item):
        """Edit curve item data in array editor"""
        item_data = item.get_data()
        if len(item_data) > 2:
            x, y, dx, dy = item_data
            array_list = [x, y]
            if dx is not None:
                array_list.append(dx)
            if dy is not None:
                array_list.append(dy)
            data = np.array(array_list).T
        else:
            x, y = item_data
            data = np.array([x, y]).T

        dialog = ArrayEditor(item.plot())
        dialog.setup_and_check(data)
        if dialog.exec_():
            if data.shape[1] > 2:
                if data.shape[1] == 3:
                    x, y, tmp = data.T
                    if dx is not None:
                        dx = tmp
                    else:
                        dy = tmp
                else:
                    x, y, dx, dy = data.T
                item.set_data(x, y, dx, dy)
            else:
                x, y = data.T
                item.set_data(x, y)

    def edit_image_data(item):
        """Edit image item data in array editor"""
        dialog = ArrayEditor(item.plot())
        dialog.setup_and_check(item.data)
        dialog.exec_()

    class EditItemDataTool(ItemManipulationBaseTool):
        """Edit item data"""

        TITLE = _("Edit data...")
        ICON = "arredit.png"

        def __init__(self, manager, toolbar_id=None):
            super(EditItemDataTool, self).__init__(
                manager,
                toolbar_id,
                curve_func=edit_curve_data,
                image_func=edit_image_data,
            )

    class ItemCenterTool(CommandTool):
        def __init__(self, manager, toolbar_id=None):
            super(ItemCenterTool, self).__init__(
                manager, _("Center items"), "center.png", toolbar_id=toolbar_id
            )

        def get_supported_items(self, plot):
            from guiqwt.shapes import RectangleShape, EllipseShape, ObliqueRectangleShape
            from guiqwt.annotations import (
                AnnotatedRectangle,
                AnnotatedEllipse,
                AnnotatedObliqueRectangle,
            )

            item_types = (
                RectangleShape,
                EllipseShape,
                ObliqueRectangleShape,
                AnnotatedRectangle,
                AnnotatedEllipse,
                AnnotatedObliqueRectangle,
            )
            return [
                item
                for item in plot.get_selected_items(z_sorted=True)
                if isinstance(item, item_types)
            ]

        def update_status(self, plot):
            self.action.setEnabled(len(self.get_supported_items(plot)) > 1)

        def activate_command(self, plot, checked):
            """Activate tool"""
            items = self.get_supported_items(plot)
            xc0, yc0 = items.pop(-1).get_center()
            for item in items:
                xc, yc = item.get_center()
                item.move_with_selection(xc0 - xc, yc0 - yc)
            plot.replot()

    class DeleteItemTool(CommandTool):
        def __init__(self, manager, toolbar_id=None):
            super(DeleteItemTool, self).__init__(
                manager, _("Remove"), "trash.png", toolbar_id=toolbar_id
            )

        def get_removable_items(self, plot):
            return [item for item in plot.get_selected_items() if not item.is_readonly()]

        def update_status(self, plot):
            self.action.setEnabled(len(self.get_removable_items(plot)) > 0)

        def activate_command(self, plot, checked):
            """Activate tool"""
            items = self.get_removable_items(plot)
            if len(items) == 1:
                message = _("Do you really want to remove this item?")
            else:
                message = _("Do you really want to remove selected items?")
            answer = QMessageBox.warning(
                plot, _("Remove"), message, QMessageBox.Yes | QMessageBox.No
            )
            if answer == QMessageBox.Yes:
                plot.del_items(items)
                plot.replot()

    class FilterTool(CommandTool):
        def __init__(self, manager, filter, toolbar_id=None):
            super(FilterTool, self).__init__(
                manager, str(filter.name), toolbar_id=toolbar_id
            )
            self.filter = filter

        def update_status(self, plot):
            self.set_status_active_item()

        def activate_command(self, plot, checked):
            """Activate tool"""
            plot.apply_filter(self.filter)

    class ColormapTool(CommandTool):
        def __init__(self, manager, toolbar_id=DefaultToolbarID):
            super(ColormapTool, self).__init__(
                manager,
                _("Colormap"),
                tip=_("Select colormap for active " "image"),
                toolbar_id=toolbar_id,
            )
            self.action.setEnabled(False)
            self.action.setIconText("")
            self.default_icon = build_icon_from_cmap_name("jet")
            self.action.setIcon(self.default_icon)

        def activate_command(self, plot, checked):
            """Activate tool"""
            pass

        def get_selected_images(self, plot):
            items = [it for it in plot.get_selected_items(item_type=IColormapImageItemType)]
            if not items:
                active_image = plot.get_last_active_item(IColormapImageItemType)
                if active_image:
                    items = [active_image]
            return items

        def activate_cmap(self, action):
            plot = self.get_active_plot()
            if plot is not None:
                items = self.get_selected_images(plot)
                cmap_name = str(action.text())
                for item in items:
                    item.imageparam.colormap = cmap_name
                    item.imageparam.update_image(item)
                self.action.setText(cmap_name)
                plot.invalidate()
                self.update_status(plot)

        def update_status(self, plot):
            if update_image_tool_status(self, plot):
                item = plot.get_last_active_item(IColormapImageItemType)
                icon = self.default_icon
                if item:
                    self.action.setEnabled(True)
                    cmap_name = item.get_color_map_name()
                    if cmap_name:
                        icon = build_icon_from_cmap_name(cmap_name)
                else:
                    self.action.setEnabled(False)
                self.action.setIcon(icon)

    class ImageMaskTool(CommandTool):

        #: Signal emitted by ImageMaskTool when mask was applied
        SIG_APPLIED_MASK_TOOL = Signal()

        def __init__(self, manager, toolbar_id=DefaultToolbarID):
            self._mask_shapes = {}
            self._mask_already_restored = {}
            super(ImageMaskTool, self).__init__(
                manager,
                _("Mask"),
                icon="mask_tool.png",
                tip=_("Manage image masking areas"),
                toolbar_id=toolbar_id,
            )
            self.masked_image = None  # associated masked image item

        def create_action_menu(self, manager):
            """Create and return menu for the tool's action"""
            rect_tool = manager.add_tool(
                RectangleTool,
                toolbar_id=None,
                handle_final_shape_cb=lambda shape: self.handle_shape(shape, inside=True),
                title=_("Mask rectangular area (inside)"),
                icon="mask_rectangle.png",
            )
            rect_out_tool = manager.add_tool(
                RectangleTool,
                toolbar_id=None,
                handle_final_shape_cb=lambda shape: self.handle_shape(shape, inside=False),
                title=_("Mask rectangular area (outside)"),
                icon="mask_rectangle_outside.png",
            )
            ellipse_tool = manager.add_tool(
                CircleTool,
                toolbar_id=None,
                handle_final_shape_cb=lambda shape: self.handle_shape(shape, inside=True),
                title=_("Mask circular area (inside)"),
                icon="mask_circle.png",
            )
            ellipse_out_tool = manager.add_tool(
                CircleTool,
                toolbar_id=None,
                handle_final_shape_cb=lambda shape: self.handle_shape(shape, inside=False),
                title=_("Mask circular area (outside)"),
                icon="mask_circle_outside.png",
            )

            menu = QMenu()
            self.showmask_action = manager.create_action(
                _("Show image mask"), toggled=self.show_mask
            )
            showshapes_action = manager.create_action(
                _("Show masking shapes"), toggled=self.show_shapes
            )
            showshapes_action.setChecked(True)
            applymask_a = manager.create_action(
                _("Apply mask"), icon=get_icon("apply.png"), triggered=self.apply_mask
            )
            clearmask_a = manager.create_action(
                _("Clear mask"), icon=get_icon("delete.png"), triggered=self.clear_mask
            )
            removeshapes_a = manager.create_action(
                _("Remove all masking shapes"),
                icon=get_icon("delete.png"),
                triggered=self.remove_all_shapes,
            )
            add_actions(
                menu,
                (
                    self.showmask_action,
                    None,
                    showshapes_action,
                    rect_tool.action,
                    ellipse_tool.action,
                    rect_out_tool.action,
                    ellipse_out_tool.action,
                    applymask_a,
                    None,
                    clearmask_a,
                    removeshapes_a,
                ),
            )
            return menu

        def update_status(self, plot):
            self.action.setEnabled(self.masked_image is not None)

        def register_plot(self, baseplot):
            super(ImageMaskTool, self).register_plot(baseplot)
            self._mask_shapes.setdefault(baseplot, [])
            baseplot.SIG_ITEMS_CHANGED.connect(self.items_changed)
            baseplot.SIG_ITEM_SELECTION_CHANGED.connect(self.item_selection_changed)

        def show_mask(self, state):
            if self.masked_image is not None:
                self.masked_image.set_mask_visible(state)

        def apply_mask(self):
            mask = self.masked_image.get_mask()
            plot = self.get_active_plot()
            for shape, inside in self._mask_shapes[plot]:
                if isinstance(shape, RectangleShape):
                    self.masked_image.align_rectangular_shape(shape)
                    x0, y0, x1, y1 = shape.get_rect()
                    self.masked_image.mask_rectangular_area(x0, y0, x1, y1, inside=inside)
                else:
                    x0, y0, x1, y1 = shape.get_rect()
                    self.masked_image.mask_circular_area(x0, y0, x1, y1, inside=inside)
            self.masked_image.set_mask(mask)
            plot.replot()
            self.SIG_APPLIED_MASK_TOOL.emit()

        def remove_all_shapes(self):
            message = _("Do you really want to remove all masking shapes?")
            plot = self.get_active_plot()
            answer = QMessageBox.warning(
                plot,
                _("Remove all masking shapes"),
                message,
                QMessageBox.Yes | QMessageBox.No,
            )
            if answer == QMessageBox.Yes:
                self.remove_shapes()

        def remove_shapes(self):
            plot = self.get_active_plot()
            plot.del_items(
                [shape for shape, _inside in self._mask_shapes[plot]]
            )  # remove shapes
            self._mask_shapes[plot] = []
            plot.replot()

        def show_shapes(self, state):
            plot = self.get_active_plot()
            if plot is not None:
                for shape, _inside in self._mask_shapes[plot]:
                    shape.setVisible(state)
                plot.replot()

        def handle_shape(self, shape, inside):
            shape.set_style("plot", "shape/mask")
            shape.set_private(True)
            plot = self.get_active_plot()
            plot.set_active_item(shape)
            self._mask_shapes[plot] += [(shape, inside)]

        def find_masked_image(self, plot):
            item = plot.get_active_item()
            from guiqwt.image import MaskedImageItem

            if isinstance(item, MaskedImageItem):
                return item
            else:
                items = [
                    item for item in plot.get_items() if isinstance(item, MaskedImageItem)
                ]
                if items:
                    return items[-1]

        def create_shapes_from_masked_areas(self):
            plot = self.get_active_plot()
            self._mask_shapes[plot] = []
            for area in self.masked_image.get_masked_areas():
                if area.geometry == "rectangular":
                    shape = RectangleShape(area.x0, area.y0, area.x1, area.y1)
                    self.masked_image.align_rectangular_shape(shape)
                else:
                    shape = EllipseShape(
                        area.x0,
                        0.5 * (area.y0 + area.y1),
                        area.x1,
                        0.5 * (area.y0 + area.y1),
                    )
                shape.set_style("plot", "shape/mask")
                shape.set_private(True)
                self._mask_shapes[plot] += [(shape, area.inside)]
                plot.blockSignals(True)
                plot.add_item(shape)
                plot.blockSignals(False)

        def set_masked_image(self, plot):
            self.masked_image = item = self.find_masked_image(plot)
            if self.masked_image is not None and not self._mask_already_restored:
                self.create_shapes_from_masked_areas()
                self._mask_already_restored = True
            enable = False if item is None else item.is_mask_visible()
            self.showmask_action.setChecked(enable)

        def items_changed(self, plot):
            self.set_masked_image(plot)
            self._mask_shapes[plot] = [
                (shape, inside)
                for shape, inside in self._mask_shapes[plot]
                if shape.plot() is plot
            ]
            self.update_status(plot)

        def item_selection_changed(self, plot):
            self.set_masked_image(plot)
            self.update_status(plot)

        def clear_mask(self):
            message = _("Do you really want to clear the mask?")
            plot = self.get_active_plot()
            answer = QMessageBox.warning(
                plot, _("Clear mask"), message, QMessageBox.Yes | QMessageBox.No
            )
            if answer == QMessageBox.Yes:
                self.masked_image.unmask_all()
                plot.replot()

        def activate_command(self, plot, checked):
            """Activate tool"""
            pass