# -*- 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.styles
-------------

The `styles` module provides set of parameters (DataSet classes) to 
configure `plot items` and `plot tools`.

.. 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
        
    Module :py:mod:`guiqwt.tools`
        Module providing the `plot tools`
        
Reference
~~~~~~~~~

.. autoclass:: CurveParam
   :members:
   :inherited-members:
.. autoclass:: ErrorBarParam
   :members:
   :inherited-members:
.. autoclass:: GridParam
   :members:
   :inherited-members:
.. autoclass:: ImageParam
   :members:
   :inherited-members:
.. autoclass:: TrImageParam
   :members:
   :inherited-members:
.. autoclass:: ImageFilterParam
   :members:
   :inherited-members:
.. autoclass:: HistogramParam
   :members:
   :inherited-members:
.. autoclass:: Histogram2DParam
   :members:
   :inherited-members:
.. autoclass:: AxesParam
   :members:
   :inherited-members:
.. autoclass:: ImageAxesParam
   :members:
   :inherited-members:
.. autoclass:: LabelParam
   :members:
   :inherited-members:
.. autoclass:: LegendParam
   :members:
   :inherited-members:
.. autoclass:: ShapeParam
   :members:
   :inherited-members:
.. autoclass:: AnnotationParam
   :members:
   :inherited-members:
.. autoclass:: AxesShapeParam
   :members:
   :inherited-members:
.. autoclass:: RangeShapeParam
   :members:
   :inherited-members:
.. autoclass:: MarkerParam
   :members:
   :inherited-members:
.. autoclass:: FontParam
   :members:
   :inherited-members:
.. autoclass:: SymbolParam
   :members:
   :inherited-members:
.. autoclass:: LineStyleParam
   :members:
   :inherited-members:
.. autoclass:: BrushStyleParam
   :members:
   :inherited-members:
.. autoclass:: TextStyleParam
   :members:
   :inherited-members:
"""

import numpy as np

from qtpy.QtWidgets import QFontDialog
from qtpy.QtGui import QPen, QBrush, QColor, QFont, QTransform
from qtpy.QtCore import Qt, QSizeF, QPointF

from guidata.dataset.datatypes import (
    DataSet,
    ObjectItem,
    BeginGroup,
    EndGroup,
    Obj,
    DataSetGroup,
    BeginTabGroup,
    EndTabGroup,
    GetAttrProp,
    NotProp,
)
from guidata.dataset.dataitems import (
    ChoiceItem,
    BoolItem,
    FloatItem,
    IntItem,
    ImageChoiceItem,
    ColorItem,
    StringItem,
    ButtonItem,
    FloatArrayItem,
    TextItem,
)
from guidata.dataset.qtwidgets import DataSetEditLayout
from guidata.dataset.qtitemwidgets import DataSetWidget
from guidata.utils import update_dataset
from qtpy.py3compat import to_text_string

# Local imports
from guiqwt.transitional import QwtPlot, QwtPlotCurve, QwtSymbol, QwtPlotMarker
from guiqwt.config import _
from guiqwt.colormap import get_colormap_list, build_icon_from_cmap_name


class ItemParameters(object):
    """Class handling QwtPlotItem-like parameters"""

    MULTISEL_DATASETS = []
    # Customizing tab display order:
    ENDING_PARAMETERS = (
        "CurveParam",
        "ErrorBarParam",
        "ShapeParam",
        "LabelParam",
        "LegendParam",
        "GridParam",
        "AxesParam",
    )

    def __init__(self, multiselection=False):
        self.multiselection = multiselection
        self.paramdict = {}
        self.items = set()

    @classmethod
    def register_multiselection(cls, klass, klass_ms):
        """Register a DataSet couple: (DataSet, DataSet_for_MultiSelection)"""
        # Inserting element backwards because classes have to be registered
        # from children to parent (see 'add' method to fully understand why)
        cls.MULTISEL_DATASETS.insert(0, (klass, klass_ms))

    def __add(self, key, item, param):
        self.paramdict[key] = param
        self.items.add(item)

    def add(self, key, item, param):
        if self.multiselection:
            for klass, klass_ms in self.MULTISEL_DATASETS:
                if isinstance(param, klass):
                    title = param.get_title()
                    if key in self.paramdict and not title.endswith("s"):
                        title += "s"
                    param_ms = klass_ms(
                        title=title, comment=param.get_comment(), icon=param.get_icon()
                    )
                    update_dataset(param_ms, param)
                    self.__add(key, item, param_ms)
                    return
        self.__add(key, item, param)

    def get(self, key):
        from copy import deepcopy

        return deepcopy(self.paramdict.get(key))

    def update(self, plot):
        # XXX: removed the following workaround as the associated bug can't be
        # reproduced anymore with guiqwt 3. However, keeping the workaround
        # here (commented) as it could become useful eventually.
        # -----
        # FIXME: without the following workaround, ImagePlot object aspect ratio
        # is changed when pressing button "Apply"
        # (see also guiqwt.image.ImagePlot.edit_axis_parameters)
        #        from guiqwt.image import ImagePlot
        #        if isinstance(plot, ImagePlot):
        #            ratio = plot.get_current_aspect_ratio()
        # -----
        for item in self.items:
            item.set_item_parameters(self)
        plot.replot()
        # -----
        #        if isinstance(plot, ImagePlot):
        #            plot.set_aspect_ratio(ratio=ratio)
        #            plot.replot()
        # -----
        plot.SIG_ITEMS_CHANGED.emit(plot)

    def edit(self, plot, title, icon):
        paramdict = self.paramdict.copy()
        ending_parameters = []
        for key in self.ENDING_PARAMETERS:
            if key in paramdict:
                ending_parameters.append(paramdict.pop(key))
        parameters = list(paramdict.values()) + ending_parameters
        dset = DataSetGroup(parameters, title=title.rstrip("."), icon=icon)
        if dset.edit(parent=plot, apply=lambda dset: self.update(plot)):
            self.update(plot)


LINESTYLES = {
    "-": "SolidLine",
    "--": "DashLine",
    ":": "DotLine",
    "-.": "DashDotLine",
}
COLORS = {
    "r": "red",
    "g": "green",
    "b": "blue",
    "c": "cyan",
    "m": "magenta",
    "y": "yellow",
    "k": "black",
    "w": "white",
    "G": "gray",
}
MARKERS = {
    "+": QwtSymbol.Cross,
    "o": QwtSymbol.Ellipse,
    "*": QwtSymbol.Star1,
    ".": QwtSymbol(QwtSymbol.Ellipse, QBrush(Qt.black), QPen(Qt.black), QSizeF(3, 3)),
    "x": QwtSymbol.XCross,
    "s": QwtSymbol.Rect,
    "d": QwtSymbol.Diamond,
    "^": QwtSymbol.UTriangle,
    "v": QwtSymbol.DTriangle,
    ">": QwtSymbol.RTriangle,
    "<": QwtSymbol.LTriangle,
    "h": QwtSymbol.Star2,
}
MARKERSTYLES = {
    None: "NoLine",
    "-": "HLine",
    "|": "VLine",
    "+": "Cross",
}


def style_generator(color_keys="bgrcmykG"):
    """Cycling through curve styles"""
    while True:
        for linestyle in sorted(LINESTYLES.keys()):
            for color in color_keys:
                yield color + linestyle


def update_style_attr(style, param):
    """Parse a MATLAB-like style string and
    update the color, linestyle, marker attributes of the param
    object
    """
    for marker in list(MARKERS.keys()):
        if marker in style:
            param.symbol.update_param(MARKERS[marker])
            break
    else:
        param.symbol.update_param(QwtSymbol.NoSymbol)
    for linestyle in list(LINESTYLES.keys()):
        if linestyle in style:
            param.line.style = LINESTYLES[linestyle]
            break
    else:
        param.line.style = "NoPen"
    for color in list(COLORS.keys()):
        if color in style:
            param.line.color = COLORS[color]
            param.symbol.facecolor = COLORS[color]
            param.symbol.edgecolor = COLORS[color]
            break


def build_reverse_map(lst, obj):
    dict = {}
    for idx, _name, _icon in lst:
        val = getattr(obj, idx)
        dict[val] = idx
    return dict


LINESTYLE_CHOICES = [
    ("SolidLine", _("Solid line"), "solid.png"),
    ("DashLine", _("Dashed line"), "dash.png"),
    ("DotLine", _("Dotted line"), "dot.png"),
    ("DashDotLine", _("Dash-dot line"), "dashdot.png"),
    ("DashDotDotLine", _("Dash-dot-dot line"), "dashdotdot.png"),
    ("NoPen", _("No line"), "none.png"),
]
MARKER_CHOICES = [
    ("Cross", _("Cross"), "cross.png"),
    ("Ellipse", _("Ellipse"), "ellipse.png"),
    ("Star1", _("Star"), "star.png"),
    ("XCross", _("X-Cross"), "xcross.png"),
    ("Rect", _("Square"), "square.png"),
    ("Diamond", _("Diamond"), "diamond.png"),
    ("UTriangle", _("Triangle"), "triangle_u.png"),
    ("DTriangle", _("Triangle"), "triangle_d.png"),
    ("RTriangle", _("Triangle"), "triangle_r.png"),
    ("LTriangle", _("Triangle"), "triangle_l.png"),
    ("Star2", _("Hexagon"), "hexagon.png"),
    ("NoSymbol", _("No symbol"), "none.png"),
]
CURVESTYLE_CHOICES = [
    ("Lines", _("Lines"), "lines.png"),
    ("Sticks", _("Sticks"), "sticks.png"),
    ("Steps", _("Steps"), "steps.png"),
    ("Dots", _("Dots"), "dots.png"),
    ("NoCurve", _("No curve"), "none.png"),
]

BRUSHSTYLE_CHOICES = [
    ("NoBrush", _("No brush pattern"), "nobrush.png"),
    ("SolidPattern", _("Uniform color"), "solidpattern.png"),
    ("Dense1Pattern", _("Extremely dense brush pattern"), "dense1pattern.png"),
    ("Dense2Pattern", _("Very dense brush pattern"), "dense2pattern.png"),
    ("Dense3Pattern", _("Somewhat dense brush pattern"), "dense3pattern.png"),
    ("Dense4Pattern", _("Half dense brush pattern"), "dense4pattern.png"),
    ("Dense5Pattern", _("Somewhat sparse brush pattern"), "dense5pattern.png"),
    ("Dense6Pattern", _("Very sparse brush pattern"), "dense6pattern.png"),
    ("Dense7Pattern", _("Extremely sparse brush pattern"), "dense7pattern.png"),
    ("HorPattern", _("Horizontal lines"), "horpattern.png"),
    ("VerPattern", _("Vertical lines"), "verpattern.png"),
    ("CrossPattern", _("Crossing horizontal and vertical lines"), "crosspattern.png"),
    ("BDiagPattern", _("Backward diagonal lines"), "bdiagpattern.png"),
    ("FDiagPattern", _("Forward diagonal lines"), "fdiagpattern.png"),
    ("DiagCrossPattern", _("Crossing diagonal lines"), "diagcrosspattern.png"),
    #    ("LinearGradientPattern", _("Linear gradient (set using a dedicated QBrush constructor)"), "none.png"),
    #    ("ConicalGradientPattern", _("Conical gradient (set using a dedicated QBrush constructor)"), "none.png"),
    #    ("RadialGradientPattern", _("Radial gradient (set using a dedicated QBrush constructor)"), "none.png"),
    #    ("TexturePattern", _("Custom pattern (see QBrush::setTexture())"), "none.png"),
]

MARKERSTYLE_CHOICES = [
    ("NoLine", _("None"), "none.png"),
    ("HLine", _("Horizontal"), "horiz_marker.png"),
    ("VLine", _("Vertical"), "vert_marker.png"),
    ("Cross", _("Cross"), "cross_marker.png"),
]

MARKER_NAME = build_reverse_map(MARKER_CHOICES, QwtSymbol)
CURVESTYLE_NAME = build_reverse_map(CURVESTYLE_CHOICES, QwtPlotCurve)
LINESTYLE_NAME = build_reverse_map(LINESTYLE_CHOICES, Qt)
BRUSHSTYLE_NAME = build_reverse_map(BRUSHSTYLE_CHOICES, Qt)
MARKERSTYLE_NAME = build_reverse_map(MARKERSTYLE_CHOICES, QwtPlotMarker)


# ===================================================
# Common font parameters
# ===================================================
def _font_selection(param, item, value, parent):
    font = param.build_font()
    result, valid = QFontDialog.getFont(font, parent)
    if valid:
        param.update_param(result)


class FontParam(DataSet):
    family = StringItem(_("Family"), default="default")
    _choose = ButtonItem(_("Choose font"), _font_selection, default=None).set_pos(col=1)
    size = IntItem(_("Size in point"), default=12)
    bold = BoolItem(_("Bold"), default=False).set_pos(col=1)
    italic = BoolItem(_("Italic"), default=False).set_pos(col=2)

    def update_param(self, font):
        self.family = str(font.family())
        self.size = font.pointSize()
        self.bold = bool(font.bold())
        self.italic = bool(font.italic())

    def build_font(self):
        font = QFont(self.family)
        font.setPointSize(self.size)
        font.setBold(self.bold)
        font.setItalic(self.italic)
        return font


class FontItemWidget(DataSetWidget):
    klass = FontParam


class FontItem(ObjectItem):
    """Item holding a LineStyleParam"""

    klass = FontParam


DataSetEditLayout.register(FontItem, FontItemWidget)


# ===================================================
# Common Qwt symbol parameters
# ===================================================
class SymbolParam(DataSet):
    marker = ImageChoiceItem(_("Style"), MARKER_CHOICES, default="NoSymbol")
    size = IntItem(_("Size"), default=9)
    edgecolor = ColorItem(_("Border"), default="gray")
    facecolor = ColorItem(_("Background color"), default="yellow")
    alpha = FloatItem(_("Background alpha"), default=1.0, min=0, max=1)

    def update_param(self, symb):
        if not isinstance(symb, QwtSymbol):
            # check if this is still needed
            # raise RuntimeError
            assert isinstance(symb, QwtSymbol.Style)
            self.marker = MARKER_NAME[symb]
            return
        self.marker = MARKER_NAME[symb.style()]
        self.size = symb.size().width()
        self.edgecolor = str(symb.pen().color().name())
        self.facecolor = str(symb.brush().color().name())

    def build_symbol(self):
        marker_type = getattr(QwtSymbol, self.marker)
        color = QColor(self.facecolor)
        color.setAlphaF(self.alpha)
        marker = QwtSymbol(
            marker_type,
            QBrush(color),
            QPen(QColor(self.edgecolor)),
            QSizeF(self.size, self.size),
        )
        return marker

    def update_symbol(self, obj):
        obj.setSymbol(self.build_symbol())


class SymbolItemWidget(DataSetWidget):
    klass = SymbolParam


class SymbolItem(ObjectItem):
    """Item holding a SymbolParam"""

    klass = SymbolParam


DataSetEditLayout.register(SymbolItem, SymbolItemWidget)


# ===================================================
# Common line style parameters
# ===================================================
class LineStyleParam(DataSet):
    style = ImageChoiceItem(_("Style"), LINESTYLE_CHOICES, default="SolidLine")
    color = ColorItem(_("Color"), default="black")
    width = FloatItem(_("Width"), default=1.0, min=0)

    def update_param(self, pen):
        self.width = pen.widthF()
        self.color = str(pen.color().name())
        self.style = LINESTYLE_NAME[pen.style()]

    def build_pen(self):
        linecolor = QColor(self.color)
        style = getattr(Qt, self.style)
        pen = QPen(linecolor, self.width, style)
        return pen

    def set_style_from_matlab(self, linestyle):
        """Eventually convert MATLAB-like linestyle into Qt linestyle"""
        linestyle = LINESTYLES.get(linestyle, linestyle)  # MATLAB-style
        if linestyle == "":  # MATLAB-style
            linestyle = "NoPen"
        self.style = linestyle


class LineStyleItemWidget(DataSetWidget):
    klass = LineStyleParam


class LineStyleItem(ObjectItem):
    """Item holding a LineStyleParam"""

    klass = LineStyleParam


DataSetEditLayout.register(LineStyleItem, LineStyleItemWidget)

# ===================================================
# Common brush style parameters
# ===================================================
class BrushStyleParam(DataSet):
    style = ImageChoiceItem(_("Style"), BRUSHSTYLE_CHOICES, default="SolidPattern")
    color = ColorItem(_("Color"), default="black")
    alpha = FloatItem(_("Alpha"), default=1.0)
    angle = FloatItem(_("Angle"), default=0.0, min=0)
    sx = FloatItem(_("sx"), default=1.0, min=0)
    sy = FloatItem(_("sy"), default=1.0, min=0)

    def update_param(self, brush):
        from math import pi, sqrt, atan2

        tr = brush.transform()
        pt = tr.map(QPointF(1.0, 0.0))
        self.sx = sqrt(pt.x() ** 2 + pt.y() ** 2)
        self.angle = 180 * atan2(pt.y(), pt.x()) / pi
        pt = tr.map(QPointF(0.0, 1.0))
        self.sy = sqrt(pt.x() ** 2 + pt.y() ** 2)

        col = brush.color()
        self.color = str(col.name())
        self.alpha = col.alphaF()
        self.style = BRUSHSTYLE_NAME[brush.style()]

    def build_brush(self):
        color = QColor(self.color)
        color.setAlphaF(self.alpha)
        brush = QBrush(color, getattr(Qt, self.style))
        tr = QTransform()
        tr = tr.scale(self.sx, self.sy)
        tr = tr.rotate(self.angle)
        brush.setTransform(tr)
        return brush


class BrushStyleItemWidget(DataSetWidget):
    klass = BrushStyleParam


class BrushStyleItem(ObjectItem):
    """Item holding a LineStyleParam"""

    klass = BrushStyleParam


DataSetEditLayout.register(BrushStyleItem, BrushStyleItemWidget)


# ===================================================
# QwtText parameters
# ===================================================
class TextStyleParam(DataSet):
    font = FontItem(_("Font"))
    textcolor = ColorItem(_("Text color"), default="blue")
    background_color = ColorItem(_("Background color"), default="white")
    background_alpha = FloatItem(_("Background alpha"), default=0.5, min=0, max=1)

    def update_param(self, obj):
        """obj: QwtText instance"""
        self.font.update_param(obj.font())
        self.textcolor = obj.color().name()
        color = obj.backgroundBrush().color()
        self.background_color = color.name()
        self.background_alpha = color.alphaF()

    def update_text(self, obj):
        """obj: QwtText instance"""
        obj.setColor(QColor(self.textcolor))
        color = QColor(self.background_color)
        color.setAlphaF(self.background_alpha)
        obj.setBackgroundBrush(QBrush(color))
        font = self.font.build_font()
        obj.setFont(font)


class TextStyleItemWidget(DataSetWidget):
    klass = TextStyleParam


class TextStyleItem(ObjectItem):
    """Item holding a TextStyleParam"""

    klass = TextStyleParam


DataSetEditLayout.register(TextStyleItem, TextStyleItemWidget)


# ===================================================
# Grid parameters
# ===================================================
class GridParam(DataSet):
    background = ColorItem(_("Background color"), default="white")
    maj = BeginGroup(_("Major grid"))
    maj_xenabled = BoolItem(_("X Axis"), default=True)
    maj_yenabled = BoolItem(_("Y Axis"), default=True).set_pos(col=1)
    maj_line = LineStyleItem(_("Line"))
    _maj = EndGroup("end group")

    min = BeginGroup(_("Minor grid"))
    min_xenabled = BoolItem(_("X Axis"), default=False)
    min_yenabled = BoolItem(_("Y Axis"), default=False).set_pos(col=1)
    min_line = LineStyleItem(_("Line"))
    _min = EndGroup("fin groupe")

    def update_param(self, grid):
        plot = grid.plot()
        if plot is not None:
            self.background = str(plot.canvasBackground().color().name())
        self.maj_xenabled = grid.xEnabled()
        self.maj_yenabled = grid.yEnabled()
        self.maj_line.update_param(grid.majorPen())
        self.min_xenabled = grid.xMinEnabled()
        self.min_yenabled = grid.yMinEnabled()
        self.min_line.update_param(grid.minorPen())

    def update_grid(self, grid):
        plot = grid.plot()
        if plot is not None:
            plot.blockSignals(True)  # Avoid unwanted calls of update_param
            # triggered by the setter methods below
            plot.setCanvasBackground(QColor(self.background))
        grid.enableX(self.maj_xenabled)
        grid.enableY(self.maj_yenabled)
        grid.setPen(self.maj_line.build_pen())
        grid.enableXMin(self.min_xenabled)
        grid.enableYMin(self.min_yenabled)
        grid.setMinorPen(self.min_line.build_pen())
        grid.setTitle(self.get_title())
        if plot is not None:
            plot.blockSignals(False)


# ===================================================
# Axes style parameters
# ===================================================
class AxeStyleParam(DataSet):
    title = StringItem(_("Title"), default="")
    unit = StringItem(_("Unit"), default="")
    color = ColorItem(_("Color"), default="black").set_pos(col=1)
    title_font = FontItem(_("Title font"))
    ticks_font = FontItem(_("Values font"))


# ===================================================
# Axes parameters
# ===================================================
class AxisParam(DataSet):
    scale = ChoiceItem(
        _("Scale"), [("lin", _("linear")), ("log", _("logarithmic"))], default="lin"
    )
    vmin = FloatItem("Min", help=_("Lower axis limit"))
    vmax = FloatItem("Max", help=_("Upper axis limit"))

    def update_param(self, plot, axis_id):
        self.scale = plot.get_axis_scale(axis_id)
        axis = plot.axisScaleDiv(axis_id)
        self.vmin = axis.lowerBound()
        self.vmax = axis.upperBound()

    def update_axis(self, plot, axis_id):
        plot.enableAxis(axis_id, True)
        plot.set_axis_scale(axis_id, self.scale, autoscale=False)
        plot.setAxisScale(axis_id, self.vmin, self.vmax)
        plot.disable_unused_axes()


class AxisItemWidget(DataSetWidget):
    klass = AxisParam


class AxisItem(ObjectItem):
    klass = AxisParam


DataSetEditLayout.register(AxisItem, AxisItemWidget)


class AxesParam(DataSet):
    xaxis_id = ChoiceItem(
        _("X-axis position"),
        [(QwtPlot.xBottom, _("bottom")), (QwtPlot.xTop, _("top"))],
        default=QwtPlot.xBottom,
    )
    xaxis = AxisItem(_("X Axis"))
    yaxis_id = ChoiceItem(
        _("Y-axis position"),
        [(QwtPlot.yLeft, _("left")), (QwtPlot.yRight, _("right"))],
        default=QwtPlot.yLeft,
    )
    yaxis = AxisItem(_("Y Axis"))

    def update_param(self, item):
        plot = item.plot()
        self.xaxis_id = item.xAxis()
        self.xaxis.update_param(plot, self.xaxis_id)
        self.yaxis_id = item.yAxis()
        self.yaxis.update_param(plot, self.yaxis_id)

    def update_axes(self, item):
        plot = item.plot()
        plot.grid.setAxes(self.xaxis_id, self.yaxis_id)
        item.setXAxis(self.xaxis_id)
        self.xaxis.update_axis(plot, self.xaxis_id)
        item.setYAxis(self.yaxis_id)
        self.yaxis.update_axis(plot, self.yaxis_id)


class ImageAxesParam(DataSet):
    xparams = BeginGroup(_("X Axis"))
    xmin = FloatItem("x|min", help=_("Lower x-axis limit"))
    xmax = FloatItem("x|max", help=_("Upper x-axis limit"))
    _xparams = EndGroup("end X")
    yparams = BeginGroup(_("Y Axis"))
    ymin = FloatItem("y|min", help=_("Lower y-axis limit"))
    ymax = FloatItem("y|max", help=_("Upper y-axis limit"))
    _yparams = EndGroup("end Y")
    zparams = BeginGroup(_("Z Axis"))
    zmin = FloatItem("z|min", help=_("Lower z-axis limit"))
    zmax = FloatItem("z|max", help=_("Upper z-axis limit"))
    _zparams = EndGroup("end Z")

    def update_param(self, item):
        plot = item.plot()
        xaxis = plot.axisScaleDiv(item.xAxis())
        self.xmin = xaxis.lowerBound()
        self.xmax = xaxis.upperBound()
        yaxis = plot.axisScaleDiv(item.yAxis())
        self.ymin = yaxis.lowerBound()
        self.ymax = yaxis.upperBound()
        self.zmin, self.zmax = item.min, item.max

    def update_axes(self, item):
        plot = item.plot()
        plot.set_plot_limits(self.xmin, self.xmax, self.ymin, self.ymax)
        item.set_lut_range([self.zmin, self.zmax])
        plot.update_colormap_axis(item)


# ===================================================
# Label parameters
# ===================================================
class LabelParam(DataSet):
    _multiselection = False
    _legend = False
    _no_contents = True
    label = StringItem(_("Title"), default="").set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )

    _styles = BeginTabGroup("Styles")
    # -------------------------------------------------------------- Contents tab
    ___cont = BeginGroup(_("Contents")).set_prop(
        "display", icon="label.png", hide=GetAttrProp("_no_contents")
    )
    contents = TextItem("").set_prop("display", hide=GetAttrProp("_no_contents"))
    ___econt = EndGroup(_("Contents")).set_prop(
        "display", hide=GetAttrProp("_no_contents")
    )
    # ---------------------------------------------------------------- Symbol tab
    symbol = SymbolItem(_("Symbol")).set_prop(
        "display", icon="diamond.png", hide=GetAttrProp("_legend")
    )
    # ---------------------------------------------------------------- Border tab
    border = LineStyleItem(
        _("Border"), default=Obj(color="#cbcbcb"), help=_("set width to 0 to disable")
    ).set_prop("display", icon="dashdot.png")
    # ------------------------------------------------------------------ Text tab
    ___text = BeginGroup(_("Text")).set_prop("display", icon="font.png")
    font = FontItem(_("Text font"))
    color = ColorItem(_("Text color"), default="#000000")
    bgcolor = ColorItem(_("Background color"), default="#ffffff")
    bgalpha = FloatItem(_("Background transparency"), min=0.0, max=1.0, default=0.8)
    ___etext = EndGroup(_("Text"))
    # -------------------------------------------------------------- Position tab
    ___position = BeginGroup(_("Position")).set_prop("display", icon="move.png")
    _begin_anchor = BeginGroup(_("Position relative to anchor")).set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    anchor = ChoiceItem(
        _("Corner"),
        [
            ("TL", _("Top left")),
            ("TR", _("Top right")),
            ("BL", _("Bottom left")),
            ("BR", _("Bottom right")),
            ("L", _("Left")),
            ("R", _("Right")),
            ("T", _("Top")),
            ("B", _("Bottom")),
            ("C", _("Center")),
        ],
        default="TL",
        help=_("Label position relative to anchor point"),
    ).set_prop("display", hide=GetAttrProp("_multiselection"))
    xc = IntItem(
        _("ΔX"),
        default=5,
        help=_("Horizontal offset (pixels) relative to anchor point"),
    ).set_prop("display", hide=GetAttrProp("_multiselection"))
    yc = (
        IntItem(
            _("ΔY"),
            default=5,
            help=_("Vertical offset (pixels) relative to anchor point"),
        )
        .set_pos(col=1)
        .set_prop("display", hide=GetAttrProp("_multiselection"))
    )
    _end_anchor = EndGroup(_("Anchor")).set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    _begin_anchorpos = BeginGroup(_("Anchor position")).set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    _abspos_prop = GetAttrProp("abspos")
    abspos = (
        BoolItem(text=_("Attach to canvas"), label=_("Anchor"), default=True)
        .set_prop("display", store=_abspos_prop)
        .set_prop("display", hide=GetAttrProp("_multiselection"))
    )
    xg = (
        FloatItem(_("X"), default=0.0, help=_("X-axis position in canvas coordinates"))
        .set_prop("display", active=NotProp(_abspos_prop))
        .set_prop("display", hide=GetAttrProp("_multiselection"))
    )
    yg = (
        FloatItem(_("Y"), default=0.0, help=_("Y-axis position in canvas coordinates"))
        .set_pos(col=1)
        .set_prop("display", active=NotProp(_abspos_prop))
        .set_prop("display", hide=GetAttrProp("_multiselection"))
    )
    move_anchor = (
        ChoiceItem(
            _("Interact"),
            (
                (True, _("moving object changes anchor position")),
                (False, _("moving object changes label position")),
            ),
            default=True,
        )
        .set_prop("display", active=NotProp(_abspos_prop))
        .set_prop("display", hide=GetAttrProp("_multiselection"))
    )
    absg = (
        ChoiceItem(
            _("Position"),
            [
                ("TL", _("Top left")),
                ("TR", _("Top right")),
                ("BL", _("Bottom left")),
                ("BR", _("Bottom right")),
                ("L", _("Left")),
                ("R", _("Right")),
                ("T", _("Top")),
                ("B", _("Bottom")),
                ("C", _("Center")),
            ],
            default="TL",
            help=_("Absolute position on canvas"),
        )
        .set_prop("display", active=_abspos_prop)
        .set_prop("display", hide=GetAttrProp("_multiselection"))
    )
    _end_anchorpos = EndGroup(_("Anchor position")).set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    ___eposition = EndGroup(_("Position"))
    # ----------------------------------------------------------------------- End
    _endstyles = EndTabGroup("Styles")

    def update_param(self, obj):
        # The following is necessary only for shape labels:
        # when shape is just created (and not yet moved), we need to update
        # these attributes
        if self.abspos:
            self.absg = obj.G
        else:
            self.xg, self.yg = obj.G
        self.xc, self.yc = obj.C

    def update_label(self, obj):
        if not self._multiselection:
            if self.abspos:
                obj.G = self.absg
            else:
                obj.G = (self.xg, self.yg)
            obj.C = self.xc, self.yc
            obj.anchor = self.anchor
            obj.move_anchor = self.move_anchor
            obj.setTitle(self.label)
        obj.marker = self.symbol.build_symbol()
        obj.border_pen = self.border.build_pen()
        obj.set_text_style(self.font.build_font(), self.color)
        color = QColor(self.bgcolor)
        color.setAlphaF(self.bgalpha)
        obj.bg_brush = QBrush(color)


class LabelParam_MS(LabelParam):
    _multiselection = True


ItemParameters.register_multiselection(LabelParam, LabelParam_MS)


class LegendParam(LabelParam):
    _legend = True
    label = StringItem(_("Title"), default="").set_prop("display", hide=True)

    def update_label(self, obj):
        super(LegendParam, self).update_label(obj)
        if not self._multiselection:
            obj.setTitle(self.get_title())


class LegendParam_MS(LegendParam):
    _multiselection = True


ItemParameters.register_multiselection(LegendParam, LegendParam_MS)


class LabelParamWithContents(LabelParam):
    _no_contents = False

    def __init__(self, title=None, comment=None, icon=""):
        self.plain_text = None
        super(LabelParamWithContents, self).__init__(title, comment, icon)

    def update_param(self, obj):
        super(LabelParamWithContents, self).update_param(obj)
        self.contents = self.plain_text = obj.get_plain_text()

    def update_label(self, obj):
        super(LabelParamWithContents, self).update_label(obj)
        if self.plain_text is not None and self.contents != self.plain_text:
            text = self.contents.replace("\n", "<br>")
            obj.set_text(text)


class LabelParamWithContents_MS(LabelParamWithContents):
    _multiselection = True


ItemParameters.register_multiselection(
    LabelParamWithContents, LabelParamWithContents_MS
)


# ===================================================
# Curve parameters
# ===================================================
class CurveParam(DataSet):
    _multiselection = False
    label = StringItem(_("Title"), default="").set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    line = LineStyleItem(_("Line"))
    symbol = SymbolItem(_("Symbol"))
    shade = FloatItem(_("Shadow"), default=0, min=0, max=1)
    curvestyle = ImageChoiceItem(_("Curve style"), CURVESTYLE_CHOICES, default="Lines")
    baseline = FloatItem(_("Baseline"), default=0.0)

    def update_param(self, curve):
        self.label = to_text_string(curve.title().text())
        self.symbol.update_param(curve.symbol())
        self.line.update_param(curve.pen())
        self.curvestyle = CURVESTYLE_NAME[curve.style()]
        self.baseline = curve.baseline()

    def update_curve(self, curve):
        plot = curve.plot()
        if plot is not None:
            plot.blockSignals(True)  # Avoid unwanted calls of update_param
            # triggered by the setter methods below
        if not self._multiselection:
            # Non common parameters
            curve.setTitle(self.label)
        curve.setPen(self.line.build_pen())
        # Brush
        linecolor = QColor(self.line.color)
        linecolor.setAlphaF(self.shade)
        brush = QBrush(linecolor)
        if not self.shade:
            brush.setStyle(Qt.NoBrush)
        curve.setBrush(brush)
        # Symbol
        self.symbol.update_symbol(curve)
        # Curve style, type and baseline
        curve.setStyle(getattr(QwtPlotCurve, self.curvestyle))
        curve.setBaseline(self.baseline)
        if plot is not None:
            plot.blockSignals(False)


class CurveParam_MS(CurveParam):
    _multiselection = True


ItemParameters.register_multiselection(CurveParam, CurveParam_MS)


# ===================================================
# ErrorBar Curve parameters
# ===================================================
class ErrorBarParam(DataSet):
    mode = ChoiceItem(
        _("Display"),
        default=0,
        choices=[_("error bars with caps (x, y)"), _("error area (y)")],
        help=_(
            "Note: only y-axis error bars are shown in "
            "error area mode\n(width and cap parameters "
            "will also be ignored)"
        ),
    )
    color = ColorItem(_("Color"), default="darkred")
    alpha = FloatItem(
        _("Alpha"), default=0.9, min=0, max=1, help=_("Error bar transparency")
    )
    width = FloatItem(_("Width"), default=1.0, min=1)
    cap = IntItem(_("Cap"), default=4, min=0)
    ontop = BoolItem(_("set to foreground"), _("Visibility"), default=False)

    def update_param(self, curve):
        color = curve.errorPen.color()
        self.color = str(color.name())
        self.alpha = color.alphaF()
        self.width = curve.errorPen.widthF()
        self.cap = curve.errorCap
        self.ontop = curve.errorOnTop

    def update_curve(self, curve):
        color = QColor(self.color)
        color.setAlphaF(self.alpha)
        curve.errorPen = QPen(color, self.width)
        curve.errorBrush = QBrush(color)
        curve.errorCap = self.cap
        curve.errorOnTop = self.ontop


# ===================================================
# Image parameters
# ===================================================
def _create_choices():
    choices = []
    for cmap_name in get_colormap_list():
        choices.append((cmap_name, cmap_name, build_icon_from_cmap_name))
    return choices


class BaseImageParam(DataSet):
    _multiselection = False
    label = StringItem(_("Image title"), default=_("Image")).set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    alpha_mask = BoolItem(
        _("Use image level as alpha"), _("Alpha channel"), default=False
    )
    alpha = FloatItem(
        _("Global alpha"), default=1.0, min=0, max=1, help=_("Global alpha value")
    )
    _hide_colormap = False
    colormap = ImageChoiceItem(
        _("Colormap"), _create_choices(), default="jet"
    ).set_prop("display", hide=GetAttrProp("_hide_colormap"))

    interpolation = ChoiceItem(
        _("Interpolation"),
        [
            (0, _("None (nearest pixel)")),
            (1, _("Linear interpolation")),
            (2, _("2x2 antialiasing filter")),
            (3, _("3x3 antialiasing filter")),
            (5, _("5x5 antialiasing filter")),
        ],
        default=0,
        help=_("Image interpolation type"),
    )

    _formats = BeginGroup(_("Statistics string formatting"))
    xformat = StringItem(_("X-Axis"), default=r"%.1f")
    yformat = StringItem(_("Y-Axis"), default=r"%.1f")
    zformat = StringItem(_("Z-Axis"), default=r"%.1f")
    _end_formats = EndGroup(_("Statistics string formatting"))

    def update_param(self, image):
        self.label = to_text_string(image.title().text())
        self.colormap = image.get_color_map_name()
        interpolation = image.get_interpolation()
        mode = interpolation[0]
        from guiqwt.image import INTERP_NEAREST, INTERP_LINEAR

        if mode == INTERP_NEAREST:
            self.interpolation = 0
        elif mode == INTERP_LINEAR:
            self.interpolation = 1
        else:
            size = interpolation[1].shape[0]
            self.interpolation = size

    def update_image(self, image):
        plot = image.plot()
        if plot is not None:
            plot.blockSignals(True)  # Avoid unwanted calls of update_param
            # triggered by the setter methods below
        image.setTitle(self.label)
        image.set_color_map(self.colormap)
        size = self.interpolation
        from guiqwt.image import INTERP_NEAREST, INTERP_LINEAR, INTERP_AA

        if size == 0:
            mode = INTERP_NEAREST
        elif size == 1:
            mode = INTERP_LINEAR
        else:
            mode = INTERP_AA
        image.set_interpolation(mode, size)
        if plot is not None:
            plot.blockSignals(False)


class QuadGridParam(DataSet):
    _multiselection = False
    label = StringItem(_("Image title"), default=_("Image")).set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    alpha_mask = BoolItem(
        _("Use image level as alpha"), _("Alpha channel"), default=False
    )
    alpha = FloatItem(
        _("Global alpha"), default=1.0, min=0, max=1, help=_("Global alpha value")
    )
    _hide_colormap = False
    colormap = ImageChoiceItem(
        _("Colormap"), _create_choices(), default="jet"
    ).set_prop("display", hide=GetAttrProp("_hide_colormap"))

    interpolation = ChoiceItem(
        _("Interpolation"),
        [(0, _("Quadrangle interpolation")), (1, _("Flat")),],
        default=0,
        help=_(
            "Image interpolation type, "
            "Flat mode use fixed u,v "
            "interpolation parameters"
        ),
    )
    uflat = FloatItem(
        _("Fixed U interpolation parameter"),
        default=0.5,
        min=0.0,
        max=1.0,
        help=_("For flat mode only"),
    )
    vflat = FloatItem(
        _("Fixed V interpolation parameter"),
        default=0.5,
        min=0.0,
        max=1.0,
        help=_("For flat mode only"),
    )
    grid = BoolItem(_("Show grid"), default=False)
    gridcolor = ColorItem(_("Grid lines color"), default="black")

    def update_param(self, image):
        self.label = to_text_string(image.title().text())
        self.colormap = image.get_color_map_name()
        interp, uflat, vflat = image.interpolate
        self.interpolation = interp
        self.uflat = uflat
        self.vflat = vflat
        self.grid = image.grid

    def update_image(self, image):
        plot = image.plot()
        if plot is not None:
            plot.blockSignals(True)  # Avoid unwanted calls of update_param
            # triggered by the setter methods below
        image.setTitle(self.label)
        image.set_color_map(self.colormap)
        image.interpolate = (self.interpolation, self.uflat, self.vflat)
        image.grid = self.grid
        # TODO : gridcolor
        if plot is not None:
            plot.blockSignals(False)


class RawImageParam(BaseImageParam):
    _hide_background = False
    background = ColorItem(_("Background color"), default="#000000").set_prop(
        "display", hide=GetAttrProp("_hide_background")
    )

    def update_param(self, image):
        super(RawImageParam, self).update_param(image)
        self.background = str(QColor(image.bg_qcolor).name())

    def update_image(self, image):
        super(RawImageParam, self).update_image(image)
        plot = image.plot()
        if plot is not None:
            plot.blockSignals(True)  # Avoid unwanted calls of update_param
            # triggered by the setter methods below
        image.set_background_color(self.background)
        if plot is not None:
            plot.blockSignals(False)


class RawImageParam_MS(RawImageParam):
    _multiselection = True


ItemParameters.register_multiselection(RawImageParam, RawImageParam_MS)


class XYImageParam(RawImageParam):
    pass


class XYImageParam_MS(XYImageParam):
    _multiselection = True


ItemParameters.register_multiselection(XYImageParam, XYImageParam_MS)


class ImageParam(RawImageParam):
    _xdata = BeginGroup(_("Image placement along X-axis"))
    xmin = FloatItem(_("x|min"), default=None)
    xmax = FloatItem(_("x|max"), default=None)
    _end_xdata = EndGroup(_("Image placement along X-axis"))
    _ydata = BeginGroup(_("Image placement along Y-axis"))
    ymin = FloatItem(_("y|min"), default=None)
    ymax = FloatItem(_("y|max"), default=None)
    _end_ydata = EndGroup(_("Image placement along Y-axis"))

    def update_param(self, image):
        super(ImageParam, self).update_param(image)
        self.xmin = image.xmin
        if self.xmin is None:
            self.xmin = 0.0
        self.ymin = image.ymin
        if self.ymin is None:
            self.ymin = 0.0
        if image.is_empty():
            shape = (0, 0)
        else:
            shape = image.data.shape
        self.xmax = image.xmax
        if self.xmax is None:
            self.xmax = float(shape[1])
        self.ymax = image.ymax
        if self.ymax is None:
            self.ymax = float(shape[0])

    def update_image(self, image):
        super(ImageParam, self).update_image(image)
        plot = image.plot()
        if plot is not None:
            plot.blockSignals(True)  # Avoid unwanted calls of update_param
            # triggered by the setter methods below
        image.xmin = self.xmin
        image.xmax = self.xmax
        image.ymin = self.ymin
        image.ymax = self.ymax
        image.update_bounds()
        image.update_border()
        if plot is not None:
            plot.blockSignals(False)


class ImageParam_MS(ImageParam):
    _multiselection = True


ItemParameters.register_multiselection(ImageParam, ImageParam_MS)


class RGBImageParam(ImageParam):
    _hide_background = True
    _hide_colormap = True

    def update_image(self, image):
        super(RGBImageParam, self).update_image(image)
        plot = image.plot()
        if plot is not None:
            plot.blockSignals(True)  # Avoid unwanted calls of update_param
            # triggered by the setter methods below
        image.recompute_alpha_channel()
        if plot is not None:
            plot.blockSignals(False)


class RGBImageParam_MS(RGBImageParam):
    _multiselection = True


ItemParameters.register_multiselection(RGBImageParam, RGBImageParam_MS)


class MaskedImageParam(ImageParam):
    g_mask = BeginGroup(_("Mask"))
    filling_value = FloatItem(_("Filling value"))
    show_mask = BoolItem(_("Show image mask"), default=False)
    alpha_masked = FloatItem(_("Masked area alpha"), default=0.7, min=0, max=1)
    alpha_unmasked = FloatItem(_("Unmasked area alpha"), default=0.0, min=0, max=1)
    _g_mask = EndGroup(_("Mask"))

    def update_image(self, image):
        super(MaskedImageParam, self).update_image(image)
        plot = image.plot()
        if plot is not None:
            plot.blockSignals(True)  # Avoid unwanted calls of update_param
            # triggered by the setter methods below
        image.update_mask()
        if plot is not None:
            plot.blockSignals(False)


class MaskedImageParam_MS(MaskedImageParam):
    _multiselection = True


ItemParameters.register_multiselection(MaskedImageParam, MaskedImageParam_MS)


class ImageFilterParam(BaseImageParam):
    label = StringItem(_("Title"), default=_("Filter"))
    g1 = BeginGroup(_("Bounds"))
    xmin = FloatItem(_("x|min"))
    xmax = FloatItem(_("x|max"))
    ymin = FloatItem(_("y|min"))
    ymax = FloatItem(_("y|max"))
    _g1 = EndGroup("sub-group")
    use_source_cmap = BoolItem(
        _("Use image colormap and level"), _("Color map"), default=True
    )

    def update_param(self, obj):
        self.xmin, self.ymin, self.xmax, self.ymax = obj.border_rect.get_rect()
        self.use_source_cmap = obj.use_source_cmap
        super(ImageFilterParam, self).update_param(obj)

    def update_imagefilter(self, imagefilter):
        m, M = imagefilter.get_lut_range()
        set_range = False
        if not self.use_source_cmap and imagefilter.use_source_cmap:
            set_range = True
        imagefilter.use_source_cmap = self.use_source_cmap
        if set_range:
            imagefilter.set_lut_range([m, M])
        self.update_image(imagefilter)
        imagefilter.border_rect.set_rect(self.xmin, self.ymin, self.xmax, self.ymax)


class TrImageParam(RawImageParam):
    _crop = BeginGroup(_("Crop")).set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    crop_left = IntItem(_("Left"), default=0)
    crop_right = IntItem(_("Right"), default=0)
    crop_top = IntItem(_("Top"), default=0)
    crop_bottom = IntItem(_("Bottom"), default=0)
    _end_crop = EndGroup(_("Cropping")).set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    _ps = BeginGroup(_("Pixel size")).set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    dx = FloatItem(_("Width (dx)"), default=1.0)
    dy = FloatItem(_("Height (dy)"), default=1.0)
    _end_ps = EndGroup(_("Pixel size")).set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    _pos = BeginGroup(_("Translate, rotate and flip"))
    pos_x0 = FloatItem(_("x<sub>CENTER</sub>"), default=0.0).set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    hflip = BoolItem(_("Flip horizontally"), default=False).set_prop("display", col=1)
    pos_y0 = FloatItem(_("y<sub>CENTER</sub>"), default=0.0).set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    vflip = BoolItem(_("Flip vertically"), default=False).set_prop("display", col=1)
    pos_angle = FloatItem(_("θ (°)"), default=0.0).set_prop("display", col=0)
    _end_pos = EndGroup(_("Translate, rotate and flip"))

    def update_param(self, image):
        super(TrImageParam, self).update_param(image)
        # we don't get crop info from the image because
        # its not easy to extract from the transform
        # and TrImageItem keeps it's crop information
        # directly in this DataSet

    def update_image(self, image):
        RawImageParam.update_image(self, image)
        plot = image.plot()
        if plot is not None:
            plot.blockSignals(True)  # Avoid unwanted calls of update_param
            # triggered by the setter methods below
        image.set_transform(*self.get_transform())
        if plot is not None:
            plot.blockSignals(False)

    def get_transform(self):
        return (
            self.pos_x0,
            self.pos_y0,
            self.pos_angle * np.pi / 180,
            self.dx,
            self.dy,
            self.hflip,
            self.vflip,
        )

    def set_transform(self, x0, y0, angle, dx=1.0, dy=1.0, hflip=False, vflip=False):
        self.pos_x0 = x0
        self.pos_y0 = y0
        self.pos_angle = angle * 180 / np.pi
        self.dx = dx
        self.dy = dy
        self.hflip = hflip
        self.vflip = vflip

    def set_crop(self, left, top, right, bottom):
        self.crop_left = left
        self.crop_right = right
        self.crop_top = top
        self.crop_bottom = bottom

    def get_crop(self):
        return (self.crop_left, self.crop_top, self.crop_right, self.crop_bottom)


class TrImageParam_MS(TrImageParam):
    _multiselection = True


ItemParameters.register_multiselection(TrImageParam, TrImageParam_MS)


# ===================================================
# Histogram parameters
# ===================================================
class HistogramParam(DataSet):
    n_bins = IntItem(_("Bins"), default=100, min=1, help=_("Number of bins"))
    logscale = BoolItem(_("logarithmic"), _("Y-axis scale"), default=False)

    def update_param(self, obj):
        self.n_bins = obj.get_bins()
        self.logscale = obj.get_logscale()

    def update_hist(self, hist):
        hist.set_bins(self.n_bins)
        hist.set_logscale(self.logscale)


# ===================================================
# Histogram 2D parameters
# ===================================================
class Histogram2DParam(BaseImageParam):
    """Histogram"""

    _multiselection = False
    label = StringItem(_("Title"), default=_("Histogram")).set_prop(
        "display", hide=GetAttrProp("_multiselection")
    )
    nx_bins = IntItem(
        _("X-axis bins"), default=100, min=1, help=_("Number of bins along x-axis")
    )
    ny_bins = IntItem(
        _("Y-axis bins"), default=100, min=1, help=_("Number of bins along y-axis")
    )
    logscale = BoolItem(_("logarithmic"), _("Z-axis scale"), default=False)

    computation = ChoiceItem(
        _("Computation"),
        [
            (-1, _("Bin count")),
            (0, _("Maximum value")),
            (1, _("Mininum value")),
            (2, _("Sum")),
            (3, _("Product")),
            (4, _("Average")),
        ],
        default=-1,
        help=_(
            "Bin count : counts the number of points per bin,\n"
            "For max, min, sum, product, average, compute the "
            "function of a third parameter (one by default)"
        ),
    )
    auto_lut = BoolItem(
        _("Automatic LUT range"),
        default=True,
        help=_("Automatically adapt color scale " "when panning, zooming"),
    )
    background = ColorItem(
        _("Background color"),
        default="transparent",
        help=_("Background color when no data is present"),
    )

    def update_param(self, obj):
        super(Histogram2DParam, self).update_param(obj)
        self.logscale = obj.logscale
        self.nx_bins, self.ny_bins = obj.nx_bins, obj.ny_bins

    def update_histogram(self, histogram):
        histogram.logscale = int(self.logscale)
        histogram.set_background_color(self.background)
        histogram.set_bins(self.nx_bins, self.ny_bins)
        self.update_image(histogram)


class Histogram2DParam_MS(Histogram2DParam):
    _multiselection = True


ItemParameters.register_multiselection(Histogram2DParam, Histogram2DParam_MS)


# ===================================================
# Shape parameters
# ===================================================
class MarkerParam(DataSet):
    _styles = BeginTabGroup("Styles")
    # ------------------------------------------------------------------ Line tab
    ___line = BeginGroup(_("Line")).set_prop("display", icon="dashdot.png")
    line = LineStyleItem(_("Line (not selected)"))
    sel_line = LineStyleItem(_("Line (selected)"))
    ___eline = EndGroup(_("Line"))
    # ---------------------------------------------------------------- Symbol tab
    ___sym = BeginGroup(_("Symbol")).set_prop("display", icon="diamond.png")
    symbol = SymbolItem(_("Symbol (not selected)"))
    sel_symbol = SymbolItem(_("Symbol (selected)"))
    ___esym = EndGroup(_("Symbol"))
    # ------------------------------------------------------------------ Text tab
    ___text = BeginGroup(_("Text")).set_prop("display", icon="font.png")
    text = TextStyleItem(_("Text (not selected)"))
    sel_text = TextStyleItem(_("Text (selected)"))
    ___etext = EndGroup(_("Text"))
    # ----------------------------------------------------------------------- End
    _endstyles = EndTabGroup("Styles")
    markerstyle = ImageChoiceItem(
        _("Line style"), MARKERSTYLE_CHOICES, default="NoLine"
    )
    spacing = IntItem(_("Spacing"), default=10, min=0)

    def update_param(self, obj):
        self.symbol.update_param(obj.symbol())
        self.text.update_param(obj.label())
        self.line.update_param(obj.linePen())
        self.markerstyle = MARKERSTYLE_NAME[obj.lineStyle()]
        self.spacing = obj.spacing()

    def update_marker(self, obj):
        if obj.selected:
            line = self.sel_line
            symb = self.sel_symbol
            text = self.sel_text
        else:
            line = self.line
            symb = self.symbol
            text = self.text
        symb.update_symbol(obj)
        label = obj.label()
        text.update_text(label)
        obj.setLabel(label)
        obj.setLinePen(line.build_pen())
        obj.setLineStyle(getattr(QwtPlotMarker, self.markerstyle))
        obj.setSpacing(self.spacing)
        obj.update_label()

    def set_markerstyle(self, style):
        """
        Set marker line style

        style:
            
            * convenient values: '+', '-', '|' or None
            * `QwtPlotMarker.NoLine`, `QwtPlotMarker.Vertical`, ...
        """
        self.markerstyle = MARKERSTYLES.get(style, style)


class ShapeParam(DataSet):
    label = StringItem(_("Title"), default="")
    _styles = BeginTabGroup("Styles")
    # ------------------------------------------------------------------ Line tab
    ___line = BeginGroup(_("Line")).set_prop("display", icon="dashdot.png")
    line = LineStyleItem(_("Line (not selected)"))
    sel_line = LineStyleItem(_("Line (selected)"))
    ___eline = EndGroup(_("Line"))
    # ---------------------------------------------------------------- Symbol tab
    ___sym = BeginGroup(_("Symbol")).set_prop("display", icon="diamond.png")
    symbol = SymbolItem(_("Symbol (not selected)"))
    sel_symbol = SymbolItem(_("Symbol (selected)"))
    ___esym = EndGroup(_("Symbol"))
    # ------------------------------------------------------------------ Fill tab
    ___fill = BeginGroup(_("Fill pattern")).set_prop(
        "display", icon="dense6pattern.png"
    )
    fill = BrushStyleItem(_("Fill pattern (not selected)"))
    sel_fill = BrushStyleItem(_("Fill pattern (selected)"))
    ___efill = EndGroup(_("Fill pattern"))
    # ----------------------------------------------------------------------- End
    _endstyles = EndTabGroup("Styles")
    readonly = BoolItem(
        _("Read-only shape"),
        default=False,
        help=_("Read-only shapes can't be removed from " "the item list panel"),
    )
    private = BoolItem(
        _("Private shape"),
        default=False,
        help=_("Private shapes are not shown in " "the item list panel"),
    ).set_pos(col=1)

    def update_param(self, obj):
        self.label = to_text_string(obj.title().text())
        self.line.update_param(obj.pen)
        self.symbol.update_param(obj.symbol)
        self.fill.update_param(obj.brush)
        self.sel_line.update_param(obj.sel_pen)
        self.sel_symbol.update_param(obj.sel_symbol)
        self.sel_fill.update_param(obj.sel_brush)
        self.readonly = obj.is_readonly()
        self.private = obj.is_private()

    def update_shape(self, obj):
        plot = obj.plot()
        if plot is not None:
            plot.blockSignals(True)  # Avoid unwanted calls of update_param
            # triggered by the setter methods below
        obj.setTitle(self.label)
        obj.pen = self.line.build_pen()
        obj.symbol = self.symbol.build_symbol()
        obj.brush = self.fill.build_brush()
        obj.sel_pen = self.sel_line.build_pen()
        obj.sel_symbol = self.sel_symbol.build_symbol()
        obj.sel_brush = self.sel_fill.build_brush()
        obj.set_readonly(self.readonly)
        obj.set_private(self.private)
        if plot is not None:
            plot.blockSignals(False)


class AxesShapeParam(DataSet):
    arrow_angle = FloatItem(_("Arrow angle (°)"), min=0, max=90, nonzero=True)
    arrow_size = FloatItem(_("Arrow size (%)"), min=0, max=100, nonzero=True)
    _styles = BeginTabGroup("Styles")
    # ------------------------------------------------------------------ Line tab
    ___line = BeginGroup(_("Line")).set_prop("display", icon="dashdot.png")
    xarrow_pen = LineStyleItem(_("Line (X-Axis)"))
    yarrow_pen = LineStyleItem(_("Line (Y-Axis)"))
    ___eline = EndGroup(_("Line"))
    # ------------------------------------------------------------------ Fill tab
    ___fill = BeginGroup(_("Fill pattern")).set_prop(
        "display", icon="dense6pattern.png"
    )
    xarrow_brush = BrushStyleItem(_("Fill pattern (X-Axis)"))
    yarrow_brush = BrushStyleItem(_("Fill pattern (Y-Axis)"))
    ___efill = EndGroup(_("Fill pattern"))
    # ----------------------------------------------------------------------- End
    _endstyles = EndTabGroup("Styles")

    def update_param(self, obj):
        self.arrow_angle = obj.arrow_angle
        self.arrow_size = obj.arrow_size
        self.xarrow_pen.update_param(obj.x_pen)
        self.yarrow_pen.update_param(obj.y_pen)
        self.xarrow_brush.update_param(obj.x_brush)
        self.yarrow_brush.update_param(obj.y_brush)

    def update_axes(self, obj):
        obj.arrow_angle = self.arrow_angle
        obj.arrow_size = self.arrow_size
        obj.x_pen = self.xarrow_pen.build_pen()
        obj.x_brush = self.xarrow_brush.build_brush()
        obj.y_pen = self.yarrow_pen.build_pen()
        obj.y_brush = self.yarrow_brush.build_brush()


class AnnotationParam(DataSet):
    show_label = BoolItem(_("Show annotation"), default=True)
    show_computations = BoolItem(
        _("Show informations on area " "covered by this shape"), default=True
    )
    title = StringItem(_("Title"), default="")
    subtitle = StringItem(_("Subtitle"), default="")
    format = StringItem(_("String formatting"), default="%.1f")
    uncertainty = FloatItem(
        _("Uncertainty"),
        default=0.0,
        min=0.0,
        max=1.0,
        help=_("Measurement relative uncertainty"),
    ).set_pos(col=1)
    transform_matrix = FloatArrayItem(
        _("Transform matrix"), default=np.eye(3, dtype=float)
    )
    readonly = BoolItem(
        _("Read-only shape"),
        default=False,
        help=_("Read-only shapes can't be removed from " "the item list panel"),
    )
    private = BoolItem(
        _("Private shape"),
        default=False,
        help=_("Private shapes are not shown in " "the item list panel"),
    ).set_pos(col=1)

    def update_param(self, obj):
        self.show_label = obj.is_label_visible()
        self.show_computations = obj.area_computations_visible
        self.title = to_text_string(obj.title().text())
        self.readonly = obj.is_readonly()
        self.private = obj.is_private()

    def update_annotation(self, obj):
        plot = obj.plot()
        if plot is not None:
            plot.blockSignals(True)  # Avoid unwanted calls of update_param
            # triggered by the setter methods below
        obj.setTitle(self.title)
        obj.set_label_visible(self.show_label)
        obj.area_computations_visible = self.show_computations
        obj.update_label()
        obj.set_readonly(self.readonly)
        obj.set_private(self.private)
        if plot is not None:
            plot.blockSignals(False)


# ===================================================
# Range selection parameters
# ===================================================
class RangeShapeParam(DataSet):
    _styles = BeginTabGroup("Styles")
    # ------------------------------------------------------------------ Line tab
    ___line = BeginGroup(_("Line")).set_prop("display", icon="dashdot.png")
    line = LineStyleItem(_("Line (not selected)"))
    sel_line = LineStyleItem(_("Line (selected)"))
    ___eline = EndGroup(_("Line"))
    # ---------------------------------------------------------------- Symbol tab
    ___symbol = BeginGroup(_("Symbol")).set_prop("display", icon="diamond.png")
    symbol = SymbolItem(_("Symbol (not selected)"))
    sel_symbol = SymbolItem(_("Symbol (selected)"))
    ___esymbol = EndGroup(_("Symbol"))
    # ------------------------------------------------------------------ Fill tab
    ___fill = BeginGroup(_("Fill")).set_prop("display", icon="dense6pattern.png")
    fill = ColorItem(_("Fill color"))
    shade = FloatItem(_("Shade"), default=0.05, min=0, max=1)
    ___efill = EndGroup(_("Fill"))
    # ----------------------------------------------------------------------- End
    _endstyles = EndTabGroup("Styles")

    def update_param(self, range):
        self.line.update_param(range.pen)
        self.sel_line.update_param(range.sel_pen)
        self.fill = range.brush.color().name()
        self.shade = range.brush.color().alphaF()
        self.symbol.update_param(range.symbol)
        self.sel_symbol.update_param(range.sel_symbol)

    def update_range(self, range):
        range.pen = self.line.build_pen()
        range.sel_pen = self.sel_line.build_pen()
        col = QColor(self.fill)
        col.setAlphaF(self.shade)
        range.brush = QBrush(col)
        range.symbol = self.symbol.build_symbol()
        range.sel_symbol = self.sel_symbol.build_symbol()
