Skip to main content
`Ctrl`+`K`

# Source code for silx.gui.dialog.ColormapDialog

    # /*##########################################################################
    #
    # Copyright (c) 2004-2023 European Synchrotron Radiation Facility
    #
    # Permission is hereby granted, free of charge, to any person obtaining a copy
    # of this software and associated documentation files (the "Software"), to deal
    # in the Software without restriction, including without limitation the rights
    # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    # copies of the Software, and to permit persons to whom the Software is
    # furnished to do so, subject to the following conditions:
    #
    # The above copyright notice and this permission notice shall be included in
    # all copies or substantial portions of the Software.
    #
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    # THE SOFTWARE.
    #
    # ###########################################################################*/
    """A QDialog widget to set-up the colormap.

    It uses a description of colormaps as dict compatible with :class:`Plot`.

    To run the following sample code, a QApplication must be initialized.

    Create the colormap dialog and set the colormap description and data range:

    >>> from silx.gui.dialog.ColormapDialog import ColormapDialog
    >>> from silx.gui.colors import Colormap

    >>> dialog = ColormapDialog()
    >>> colormap = Colormap(name='red', normalization='log',
    ...                     vmin=1., vmax=2.)

    >>> dialog.setColormap(colormap)
    >>> colormap.setVRange(1., 100.)  # This scale the width of the plot area
    >>> dialog.show()

    Get the colormap description (compatible with :class:`Plot`) from the dialog:

    >>> cmap = dialog.getColormap()
    >>> cmap.getName()
    'red'

    It is also possible to display an histogram of the image in the dialog.
    This updates the data range with the range of the bins.

    >>> import numpy
    >>> image = numpy.random.normal(size=512 * 512).reshape(512, -1)
    >>> hist, bin_edges = numpy.histogram(image, bins=10)
    >>> dialog.setHistogram(hist, bin_edges)

    The updates of the colormap description are also available through the signal:
    :attr:`ColormapDialog.sigColormapChanged`.
    """  # noqa

    from __future__ import annotations

    __authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"]
    __license__ = "MIT"
    __date__ = "08/12/2020"

    import enum
    import logging

    import numpy

    from .. import qt
    from .. import utils
    from ..colors import Colormap, cursorColorForColormap
    from ..plot import PlotWidget
    from ..plot.items.axis import Axis
    from ..plot.items import BoundingRect
    from silx.gui.widgets.FloatEdit import FloatEdit
    import weakref
    from silx.math.combo import min_max
    from silx.gui.plot import items
    from silx.gui import icons
    from silx.gui.qt import inspect as qtinspect
    from silx.gui.widgets.ColormapNameComboBox import ColormapNameComboBox
    from silx.gui.widgets.FormGridLayout import FormGridLayout
    from silx.math.histogram import Histogramnd
    from silx.gui.plot.items.roi import RectangleROI
    from silx.gui.plot.tools.roi import RegionOfInterestManager
    from silx.utils.deprecation import deprecated
    from silx.utils.enum import Enum as _Enum

    _logger = logging.getLogger(__name__)

    _colormapIconPreview = {}

    class _DataRefHolder(items.Item, items.ColormapMixIn):
        """Holder for a weakref of a numpy array.

        It provides features from `ColormapMixIn`.
        """

        def __init__(self, dataRef):
            items.Item.__init__(self)
            items.ColormapMixIn.__init__(self)
            self.__dataRef = dataRef
            self._updated(items.ItemChangedType.DATA)

        def getColormappedData(self, copy=True):
            return self.__dataRef()

    class _BoundaryWidget(qt.QWidget):
        """Widget to edit a boundary of the colormap (vmin or vmax)"""

        sigAutoScaleChanged = qt.Signal(object)
        """Signal emitted when the autoscale was changed

        True is sent as an argument if autoscale is set to true.
        """

        sigValueChanged = qt.Signal(object)
        """Signal emitted when value is changed

        The new value is sent as an argument.
        """

        def __init__(self, parent=None, value=0.0):
            qt.QWidget.__init__(self, parent=parent)
            self.setLayout(qt.QHBoxLayout())
            self.layout().setContentsMargins(0, 0, 0, 0)
            self._numVal = FloatEdit(parent=self, value=value)

            self._iconAuto = icons.getQIcon("scale-auto")
            self._iconFixed = icons.getQIcon("scale-fixed")

            self._autoToggleAction = qt.QAction(self)
            self._autoToggleAction.setText("Auto scale")
            self._autoToggleAction.setToolTip("Toggle auto scale")
            self._autoToggleAction.setCheckable(True)
            self._autoToggleAction.setIcon(self._iconFixed)
            self._autoToggleAction.setChecked(False)
            self._autoToggleAction.toggled.connect(self._autoToggled)

            self._numVal.addAction(self._autoToggleAction, qt.QLineEdit.LeadingPosition)

            self.layout().addWidget(self._numVal)
            self._autoCB = qt.QCheckBox("auto", parent=self)
            self.layout().addWidget(self._autoCB)
            self._autoCB.setChecked(False)
            self._autoCB.setVisible(False)

            self._autoCB.toggled.connect(self._autoToggled)
            self._numVal.textEdited.connect(self.__textEdited)
            self._numVal.editingFinished.connect(self.__editingFinished)
            self.setFocusProxy(self._numVal)

            self.__textWasEdited = False
            """True if the text was edited, in order to send an event
            at the end of the user interaction"""

            self.__realValue = None
            """Store the real value set by setValue, to avoid
            rounding of the widget"""

        def __textEdited(self):
            self.__textWasEdited = True

        def __editingFinished(self):
            if self.__textWasEdited:
                value = self._numVal.value()
                self.__realValue = value
                with utils.blockSignals(self._numVal):
                    # Fix the formatting
                    self._numVal.setValue(self.__realValue)
                self.sigValueChanged.emit(value)
                self.__textWasEdited = False

        def isAutoChecked(self):
            return self._autoCB.isChecked()

        def getValue(self):
            """Returns the stored range. If autoscale is
            enabled, this returns None.
            """
            if self._autoCB.isChecked():
                return None
            if self.__realValue is not None:
                return self.__realValue
            return self._numVal.value()

        def _autoToggled(self, enabled):
            self._updateAutoScaleState(enabled)
            self._updateDisplayedText()
            self.sigAutoScaleChanged.emit(enabled)

        def _updateDisplayedText(self):
            self.__textWasEdited = False
            if self._autoCB.isChecked() and self.__realValue is not None:
                with utils.blockSignals(self._numVal):
                    self._numVal.setValue(self.__realValue)

        def setValue(self, value, isAuto=False):
            """Set the value of the boundary.

            :param float value: A finite value for the boundary
            :param bool isAuto: If true, the finite value was automatically computed
                from the data, else it is a fixed custom value.
            """
            assert value is not None
            self._autoCB.setChecked(isAuto)
            with utils.blockSignals(self._numVal):
                if isAuto or self.__realValue != value:
                    if not self.__textWasEdited:
                        self._numVal.setValue(value)
                self.__realValue = value
                self._updateAutoScaleState(isAuto)

        def _updateAutoScaleState(self, isAutoScale):
            self._numVal.setReadOnly(isAutoScale)
            palette = qt.QPalette()
            if isAutoScale:
                color = palette.color(qt.QPalette.Disabled, qt.QPalette.Base)
                icon = self._iconAuto
            else:
                color = palette.color(qt.QPalette.Active, qt.QPalette.Base)
                icon = self._iconFixed
            palette.setColor(qt.QPalette.Base, color)
            self._numVal.setPalette(palette)
            self._autoToggleAction.setIcon(icon)

    class _AutoscaleModeComboBox(qt.QComboBox):
        DATA = {
            Colormap.MINMAX: ("Min/max", "Use the data min/max"),
            Colormap.STDDEV3: ("Mean±3std", "Use the data mean ± 3 × standard deviation"),
            Colormap.PERCENTILE_1_99: ("Percentile 1-99", "Use 1st to 99th percentile of data"),
        }

        def __init__(self, parent: qt.QWidget):
            super(_AutoscaleModeComboBox, self).__init__(parent=parent)
            self.currentIndexChanged.connect(self.__updateTooltip)
            self._init()

        def _init(self):
            for mode in Colormap.AUTOSCALE_MODES:
                label, tooltip = self.DATA.get(mode, (mode, None))
                self.addItem(label, mode)
                if tooltip is not None:
                    self.setItemData(self.count() - 1, tooltip, qt.Qt.ToolTipRole)

        def setCurrentIndex(self, index):
            self.__updateTooltip(index)
            super(_AutoscaleModeComboBox, self).setCurrentIndex(index)

        def __updateTooltip(self, index):
            if index > -1:
                tooltip = self.itemData(index, qt.Qt.ToolTipRole)
            else:
                tooltip = ""
            self.setToolTip(tooltip)

        def currentMode(self):
            index = self.currentIndex()
            return self.itemData(index)

        def setCurrentMode(self, mode):
            for index in range(self.count()):
                if mode == self.itemData(index):
                    self.setCurrentIndex(index)
                    return
            if mode is None:
                # If None was not a value
                self.setCurrentIndex(-1)
                return
            self.addItem(mode, mode)
            self.setCurrentIndex(self.count() - 1)

    class _AutoScaleButton(qt.QPushButton):
        autoRangeChanged = qt.Signal(object)

        def __init__(self, parent=None):
            qt.QPushButton.__init__(self, parent=parent)
            self.setText("Autoscale")
            self.setToolTip("Enable/disable the autoscale for both min and max")
            self.setCheckable(True)
            self.toggled[bool].connect(self.__toggled)
            self.setFocusPolicy(qt.Qt.TabFocus)

        def __toggled(self, checked):
            autoRange = checked, checked
            self.setAutoRange(autoRange)
            self.autoRangeChanged.emit(autoRange)

        def setAutoRangeFromColormap(self, colormap):
            vRange = colormap.getVRange()
            autoRange = vRange[0] is None, vRange[1] is None
            self.setAutoRange(autoRange)

        def setAutoRange(self, autoRange):
            with utils.blockSignals(self):
                self.setChecked(autoRange[0] if autoRange[0] == autoRange[1] else False)

    @enum.unique
    class DisplayMode(_Enum):
        """Enum for each mode of display of the data in the plot."""

        RANGE = "range"
        HISTOGRAM = "histogram"

    class _ColormapHistogram(qt.QWidget):
        """Display the colormap and the data as a plot."""

        sigRangeMoving = qt.Signal(object, object, object)
        """Emitted when a mouse interaction moves the location
        of the colormap range in the plot.

        This signal contains 2 elements:

        - vmin: A float value if this range was moved, else None
        - vmax: A float value if this range was moved, else None
        - gammaPos: A float value if this range was moved, else None
        """

        sigRangeMoved = qt.Signal(object, object, object)
        """Emitted when a mouse interaction stop.

        This signal contains 2 elements:

        - vmin: A float value if this range was moved, else None
        - vmax: A float value if this range was moved, else None
        - gammaPos: A float value if this range was moved, else None
        """

        def __init__(self, parent):
            qt.QWidget.__init__(self, parent=parent)
            self._displayMode = DisplayMode.RANGE
            self._finiteRange = None, None
            self._initPlot()

            self._histogramData = {}
            """Histogram displayed in the plot"""

            self._dragging = False, False, False
            """True, if the min or the max handle is dragging"""

            self._dataRange = {}
            """Histogram displayed in the plot"""

            self._invalidated = False

        def paintEvent(self, event):
            if self._invalidated:
                self._updateDisplayMode()
                self._invalidated = False
            self._updateMarkerPosition()
            return super(_ColormapHistogram, self).paintEvent(event)

        def getFiniteRange(self):
            """Returns the colormap range as displayed in the plot."""
            return self._finiteRange

        def setFiniteRange(self, vRange):
            """Set the colormap range to use in the plot.

            Here there is no concept of auto. The values should
            not be None, except if there is no range or marker
            to display.
            """
            # Do not reset the limit for handle about to be dragged
            if self._dragging[0]:
                vRange = self._finiteRange[0], vRange[1]
            if self._dragging[1]:
                vRange = vRange[0], self._finiteRange[1]

            if vRange == self._finiteRange:
                return

            self._finiteRange = vRange
            self.update()

        def getColormap(self):
            return self.parent().getColormap()

        def _getNormalizedHistogram(self):
            """Return an histogram already normalized according to the colormap
            normalization.

            Returns a tuple edges, counts
            """
            norm = self._getNorm()
            histogram = self._histogramData.get(norm, None)
            if histogram is None:
                histogram = self._computeNormalizedHistogram()
                self._histogramData[norm] = histogram
            return histogram

        def _computeNormalizedHistogram(self):
            colormap = self.getColormap()
            if colormap is None:
                norm = Colormap.LINEAR
            else:
                norm = colormap.getNormalization()

            # Try to use the histogram defined in the dialog
            histo = self.parent()._getHistogram()
            if histo is not None:
                counts, edges = histo
                normalizer = Colormap(normalization=norm)._getNormalizer()
                mask = normalizer.is_valid(edges[:-1])  # Check lower bin edges only
                firstValid = numpy.argmax(mask)  # edges increases monotonically
                if firstValid == 0:  # Mask is all False or all True
                    return (counts, edges) if mask[0] else (None, None)
                else:  # Clip to valid values
                    return counts[firstValid:], edges[firstValid:]

            data = self.parent()._getArray()
            if data is None:
                return None, None
            dataRange = self._getNormalizedDataRange()
            if dataRange[0] is None or dataRange[1] is None:
                return None, None
            counts, edges = self.parent().computeHistogram(
                data, scale=norm, dataRange=dataRange
            )
            return counts, edges

        def _getNormalizedDataRange(self):
            """Return a data range already normalized according to the colormap
            normalization.

            Returns a tuple with min and max
            """
            norm = self._getNorm()
            dataRange = self._dataRange.get(norm, None)
            if dataRange is None:
                dataRange = self._computeNormalizedDataRange()
                self._dataRange[norm] = dataRange
            return dataRange

        def _computeNormalizedDataRange(self):
            colormap = self.getColormap()
            if colormap is None:
                norm = Colormap.LINEAR
            else:
                norm = colormap.getNormalization()

            # Try to use the one defined in the dialog
            dataRange = self.parent()._getDataRange()
            if dataRange is not None:
                if norm in (Colormap.LINEAR, Colormap.GAMMA, Colormap.ARCSINH):
                    return dataRange[0], dataRange[2]
                elif norm == Colormap.LOGARITHM:
                    return dataRange[1], dataRange[2]
                elif norm == Colormap.SQRT:
                    return dataRange[1], dataRange[2]
                else:
                    _logger.error("Undefined %s normalization", norm)

            # Try to use the histogram defined in the dialog
            histo = self.parent()._getHistogram()
            if histo is not None:
                _histo, edges = histo
                normalizer = Colormap(normalization=norm)._getNormalizer()
                edges = edges[normalizer.is_valid(edges)]
                if edges.size == 0:
                    return None, None
                else:
                    dataRange = min_max(edges, finite=True)
                    return dataRange.minimum, dataRange.maximum

            item = self.parent()._getItem()
            if item is not None:
                # Trick to reach data range using colormap cache
                cm = Colormap()
                cm.setVRange(None, None)
                cm.setNormalization(norm)
                dataRange = item._getColormapAutoscaleRange(cm)
                return dataRange

            # If there is no item, there is no data
            return None, None

        def _getDisplayableRange(self):
            """Returns the selected min/max range to apply to the data,
            according to the used scale.

            One or both limits can be None in case it is not displayable in the
            current axes scale.

            :returns: Tuple{float, float}
            """
            scale = self._plot.getXAxis().getScale()

            def isDisplayable(pos):
                if pos is None:
                    return False
                if scale == Axis.LOGARITHMIC:
                    return pos > 0.0
                return True

            posMin, posMax = self.getFiniteRange()
            if not isDisplayable(posMin):
                posMin = None
            if not isDisplayable(posMax):
                posMax = None

            return posMin, posMax

        def _initPlot(self):
            """Init the plot to display the range and the values"""
            self._plot = PlotWidget(self)
            self._plot.setAxesDisplayed(False)
            self._plot.setDataMargins(0.125, 0.125, 0.01, 0.01)
            self._plot.getXAxis().setLabel("Data Values")
            self._plot.getYAxis().setLabel("")
            self._plot.setInteractiveMode("select", zoomOnWheel=False)
            self._plot.setActiveCurveHandling(False)
            self._plot.setMinimumSize(qt.QSize(250, 200))
            self._plot.sigPlotSignal.connect(self._plotEventReceived)
            palette = self.palette()
            color = palette.color(qt.QPalette.Active, qt.QPalette.Window)
            self._plot.setBackgroundColor(color)
            self._plot.setDataBackgroundColor("white")

            lut = numpy.arange(256)
            lut.shape = 1, -1
            self._plot.addImage(lut, legend="lut")
            self._lutItem = self._plot._getItem("image", "lut")
            self._lutItem.setVisible(False)

            self._plot.addScatter(x=[], y=[], value=[], legend="lut2")
            self._lutItem2 = self._plot._getItem("scatter", "lut2")
            self._lutItem2.setVisible(False)
            self.__lutY = numpy.array([-0.05] * 256)
            self.__lutV = numpy.arange(256)

            self._bound = BoundingRect()
            self._plot.addItem(self._bound)
            self._bound.setVisible(True)

            # Add plot for histogram
            self._plotToolbar = qt.QToolBar(self)
            self._plotToolbar.setFloatable(False)
            self._plotToolbar.setMovable(False)
            self._plotToolbar.setIconSize(qt.QSize(8, 8))
            self._plotToolbar.setStyleSheet("QToolBar { border: 0px }")
            self._plotToolbar.setOrientation(qt.Qt.Vertical)

            group = qt.QActionGroup(self._plotToolbar)
            group.setExclusive(True)
            # data range mode
            action = qt.QAction("Data range", self)
            action.setToolTip(
                "Display the data range within the colormap range. A fast data processing have to be done."
            )
            action.setIcon(icons.getQIcon("colormap-range"))
            action.setCheckable(True)
            action.setData(DisplayMode.RANGE)
            action.setChecked(action.data() == self._displayMode)
            self._plotToolbar.addAction(action)
            group.addAction(action)
            self._dataRangeAction = action
            # histogram mode
            action = qt.QAction("Histogram", self)
            action.setToolTip(
                "Display the data histogram within the colormap range. A slow data processing have to be done. "
            )
            action.setIcon(icons.getQIcon("colormap-histogram"))
            action.setCheckable(True)
            action.setData(DisplayMode.HISTOGRAM)
            action.setChecked(action.data() == self._displayMode)
            self._plotToolbar.addAction(action)
            group.addAction(action)
            self._dataHistogramAction = action
            group.setExclusive(True)
            group.triggered.connect(self._displayModeChanged)

            plotBoxLayout = qt.QHBoxLayout()
            plotBoxLayout.setContentsMargins(0, 0, 0, 0)
            plotBoxLayout.setSpacing(2)
            plotBoxLayout.addWidget(self._plotToolbar)
            plotBoxLayout.addWidget(self._plot)
            plotBoxLayout.setSizeConstraint(qt.QLayout.SetMinimumSize)
            self.setLayout(plotBoxLayout)

        def _plotEventReceived(self, event):
            """Handle events from the plot"""
            kind = event["event"]

            if kind == "markerMoving":
                value = event["xdata"]
                if event["label"] == "Min":
                    self._dragging = True, False, False
                    self._finiteRange = value, self._finiteRange[1]
                    self._last = value, None, None
                    self._updateGammaPosition()
                    self.sigRangeMoving.emit(*self._last)
                elif event["label"] == "Max":
                    self._dragging = False, True, False
                    self._finiteRange = self._finiteRange[0], value
                    self._last = None, value, None
                    self._updateGammaPosition()
                    self.sigRangeMoving.emit(*self._last)
                elif event["label"] == "Gamma":
                    self._dragging = False, False, True
                    self._last = None, None, value
                    self.sigRangeMoving.emit(*self._last)
                self._updateLutItem(self._finiteRange)
            elif kind == "markerMoved":
                self.sigRangeMoved.emit(*self._last)
                self._plot.resetZoom()
                self._dragging = False, False, False
            else:
                pass

        def _updateMarkerPosition(self):
            colormap = self.getColormap()
            posMin, posMax = self._getDisplayableRange()

            if colormap is None:
                isDraggable = False
            else:
                isDraggable = colormap.isEditable()

            with utils.blockSignals(self):
                if posMin is not None and not self._dragging[0]:
                    self._plot.addXMarker(
                        posMin,
                        legend="Min",
                        text="Min",
                        draggable=isDraggable,
                        color="blue",
                        constraint=self._plotMinMarkerConstraint,
                    )
                self._updateGammaPosition()
                if posMax is not None and not self._dragging[1]:
                    self._plot.addXMarker(
                        posMax,
                        legend="Max",
                        text="\n\nMax",
                        draggable=isDraggable,
                        color="blue",
                        constraint=self._plotMaxMarkerConstraint,
                    )

            self._updateLutItem((posMin, posMax))
            self._plot.resetZoom()

        def _updateGammaPosition(self):
            colormap = self.getColormap()
            posMin, posMax = self._getDisplayableRange()

            if colormap is None:
                gamma = None
            else:
                if colormap.getNormalization() == Colormap.GAMMA:
                    gamma = colormap.getGammaNormalizationParameter()
                else:
                    gamma = None

            if gamma is not None:
                if not self._dragging[2]:
                    posRange = posMax - posMin
                    if posRange > 0:
                        gammaPos = posMin + posRange * 0.5 ** (1 / gamma)
                    else:
                        gammaPos = posMin
                    marker = self._plot._getMarker(
                        self._plot.addXMarker(
                            gammaPos,
                            legend="Gamma",
                            text="\nGamma",
                            draggable=True,
                            color="blue",
                            constraint=self._plotGammaMarkerConstraint,
                        )
                    )
                    marker.setZValue(2)
            else:
                try:
                    self._plot.removeMarker("Gamma")
                except Exception:
                    pass

        def _updateLutItem(self, vRange):
            colormap = self.getColormap()
            if colormap is None:
                return

            if vRange is None:
                posMin, posMax = self._getDisplayableRange()
            else:
                posMin, posMax = vRange
            if posMin is None or posMax is None:
                self._lutItem.setVisible(False)
                pos = posMax if posMin is None else posMin
                if pos is not None:
                    self._bound.setBounds((pos, pos, -0.1, 0))
                else:
                    self._bound.setBounds((0, 0, -0.1, 0))
            else:
                norm = colormap.getNormalization()
                normColormap = colormap.copy()
                normColormap.setEditable(True)
                normColormap.setVRange(0, 255)
                normColormap.setNormalization(Colormap.LINEAR)
                if norm == Colormap.LINEAR:
                    scale = (posMax - posMin) / 256
                    self._lutItem.setColormap(normColormap)
                    self._lutItem.setOrigin((posMin, -0.09))
                    self._lutItem.setScale((scale, 0.08))
                    self._lutItem.setVisible(True)
                    self._lutItem2.setVisible(False)
                elif norm == Colormap.LOGARITHM:
                    self._lutItem2.setVisible(False)
                    self._lutItem2.setColormap(normColormap)
                    xx = numpy.geomspace(posMin, posMax, 256)
                    self._lutItem2.setData(
                        x=xx, y=self.__lutY, value=self.__lutV, copy=False
                    )
                    self._lutItem2.setSymbol("|")
                    self._lutItem2.setVisible(True)
                    self._lutItem.setVisible(False)
                else:
                    # Fallback: Display with linear axis and applied normalization
                    self._lutItem2.setVisible(False)
                    normColormap.setNormalization(norm)
                    self._lutItem2.setColormap(normColormap)
                    xx = numpy.linspace(posMin, posMax, 256, endpoint=True)
                    self._lutItem2.setData(
                        x=xx, y=self.__lutY, value=self.__lutV, copy=False
                    )
                    self._lutItem2.setSymbol("|")
                    self._lutItem2.setVisible(True)
                    self._lutItem.setVisible(False)

                self._bound.setBounds((posMin, posMax, -0.1, 1))

        def _plotMinMarkerConstraint(self, x, y):
            """Constraint of the min marker"""
            _vmin, vmax = self.getFiniteRange()
            if vmax is None:
                return x, y
            return min(x, vmax), y

        def _plotMaxMarkerConstraint(self, x, y):
            """Constraint of the max marker"""
            vmin, _vmax = self.getFiniteRange()
            if vmin is None:
                return x, y
            return max(x, vmin), y

        def _plotGammaMarkerConstraint(self, x, y):
            """Constraint of the gamma marker"""
            vmin, vmax = self.getFiniteRange()
            if vmin is not None:
                x = max(x, vmin)
            if vmax is not None:
                x = min(x, vmax)
            return x, y

        def setDisplayMode(self, mode: str | DisplayMode):
            mode = DisplayMode(mode)
            if mode is DisplayMode.HISTOGRAM:
                action = self._dataHistogramAction
            elif mode is DisplayMode.RANGE:
                action = self._dataRangeAction
            else:
                raise ValueError("Mode not supported")
            action.setChecked(True)
            self._displayModeChanged(action)

        def _setDisplayMode(self, mode):
            if self._displayMode == mode:
                return
            self._displayMode = mode
            self._updateDisplayMode()

        def getDisplayMode(self) -> DisplayMode:
            return self._displayMode

        @deprecated(since_version="2.1.0", replacement="getDisplayMode")
        def getDsiplayMode(self):
            return self.getDisplayMode()

        def _displayModeChanged(self, action):
            mode = action.data()
            self._setDisplayMode(mode)

        def invalidateData(self):
            self._histogramData = {}
            self._dataRange = {}
            self._invalidated = True
            self.update()

        def _updateDisplayMode(self):
            mode = self._displayMode

            norm = self._getNorm()
            if norm == Colormap.LINEAR:
                scale = Axis.LINEAR
            elif norm == Colormap.LOGARITHM:
                scale = Axis.LOGARITHMIC
            else:
                scale = Axis.LINEAR

            axis = self._plot.getXAxis()
            axis.setScale(scale)

            if mode == DisplayMode.RANGE:
                dataRange = self._getNormalizedDataRange()
                xmin, xmax = dataRange
                if xmax is None or xmin is None:
                    self._plot.remove(legend="Data", kind="histogram")
                else:
                    histogram = numpy.array([1])
                    bin_edges = numpy.array([xmin, xmax])
                    self._plot.addHistogram(
                        histogram,
                        bin_edges,
                        legend="Data",
                        color="gray",
                        align="center",
                        fill=True,
                        z=1,
                    )

            elif mode == DisplayMode.HISTOGRAM:
                histogram, bin_edges = self._getNormalizedHistogram()
                if histogram is None or bin_edges is None:
                    self._plot.remove(legend="Data", kind="histogram")
                else:
                    histogram = numpy.array(histogram, copy=True)
                    bin_edges = numpy.array(bin_edges, copy=True)
                    with numpy.errstate(invalid="ignore"):
                        norm_histogram = histogram / numpy.nanmax(histogram)
                    self._plot.addHistogram(
                        norm_histogram,
                        bin_edges,
                        legend="Data",
                        color="gray",
                        align="center",
                        fill=True,
                        z=1,
                    )
            else:
                _logger.error("Mode unsupported")

        def sizeHint(self):
            return self.layout().minimumSize()

        def updateLut(self):
            self._updateLutItem(None)

        def _getNorm(self):
            colormap = self.getColormap()
            if colormap is None:
                return Axis.LINEAR
            else:
                norm = colormap.getNormalization()
                return norm

        def updateNormalization(self):
            self._updateDisplayMode()
            self.update()

    class ColormapDialog(qt.QDialog):
        """A QDialog widget to set the colormap.

        :param parent: See :class:`QDialog`
        :param str title: The QDialog title
        """

        visibleChanged = qt.Signal(bool)
        """This event is sent when the dialog visibility change"""

        def __init__(self, parent=None, title="Colormap Dialog"):
            qt.QDialog.__init__(self, parent)
            self.setWindowTitle(title)

            self.__aboutToDelete = False
            self._colormap = None

            self._data = None
            """Weak ref to an external numpy array
            """
            self._itemHolder = None
            """Hard ref to a private item (used as holder to the data)
            This allow to reuse the item cache
            """
            self._item = None
            """Weak ref to an external item"""

            self._colormapped = None
            """Weak ref to reduce data update"""

            self._colormapChange = utils.LockReentrant()
            """Used as a semaphore to avoid editing the colormap object when we are
            only attempt to display it.
            Used instead of n connect and disconnect of the sigChanged. The
            disconnection to sigChanged was also limiting when this colormapdialog
            is used in the colormapaction and associated to the activeImageChanged.
            (because the activeImageChanged is send when the colormap changed and
            the self.setcolormap is a callback)
            """

            self.__colormapInvalidated = False
            self.__dataInvalidated = False

            self._histogramData = None

            self._dataRange = None
            """If defined 3-tuple containing information from a data:
            minimum, positive minimum, maximum"""

            self._colormapStoredState = None

            # Colormap row
            self._comboBoxColormap = ColormapNameComboBox(parent=self)
            self._comboBoxColormap.currentIndexChanged[int].connect(
                self._comboBoxColormapUpdated
            )

            # Normalization row
            self._comboBoxNormalization = qt.QComboBox(parent=self)
            normalizations = [
                ("Linear", Colormap.LINEAR),
                ("Gamma correction", Colormap.GAMMA),
                ("Arcsinh", Colormap.ARCSINH),
                ("Logarithmic", Colormap.LOGARITHM),
                ("Square root", Colormap.SQRT),
            ]
            for name, userData in normalizations:
                try:
                    icon = icons.getQIcon("colormap-norm-%s" % userData)
                except:
                    icon = qt.QIcon()
                self._comboBoxNormalization.addItem(icon, name, userData)
            self._comboBoxNormalization.currentIndexChanged[int].connect(
                self._normalizationUpdated
            )

            self._gammaSpinBox = qt.QDoubleSpinBox(parent=self)
            self._gammaSpinBox.setEnabled(False)
            self._gammaSpinBox.setRange(0.01, 100.0)
            self._gammaSpinBox.setDecimals(4)
            if hasattr(qt.QDoubleSpinBox, "setStepType"):
                # Introduced in Qt 5.12
                self._gammaSpinBox.setStepType(qt.QDoubleSpinBox.AdaptiveDecimalStepType)
            else:
                self._gammaSpinBox.setSingleStep(0.1)
            self._gammaSpinBox.valueChanged.connect(self._gammaUpdated)
            self._gammaSpinBox.setValue(2.0)

            autoScaleCombo = _AutoscaleModeComboBox(self)
            autoScaleCombo.currentIndexChanged.connect(self._autoscaleModeUpdated)
            self._autoScaleCombo = autoScaleCombo

            # Min row
            self._minValue = _BoundaryWidget(parent=self, value=1.0)
            self._minValue.sigAutoScaleChanged.connect(self._minAutoscaleUpdated)
            self._minValue.sigValueChanged.connect(self._minValueUpdated)
            self._minValue.setMinimumWidth(140)

            # Max row
            self._maxValue = _BoundaryWidget(parent=self, value=10.0)
            self._maxValue.sigAutoScaleChanged.connect(self._maxAutoscaleUpdated)
            self._maxValue.sigValueChanged.connect(self._maxValueUpdated)
            self._maxValue.setMinimumWidth(140)

            self._autoButtons = _AutoScaleButton(self)
            self._autoButtons.autoRangeChanged.connect(self._autoRangeButtonsUpdated)

            rangeLayout = qt.QGridLayout()
            miniFont = qt.QFont(self.font())
            miniFont.setPixelSize(8)
            labelMin = qt.QLabel("Min", self)
            labelMin.setFont(miniFont)
            labelMin.setAlignment(qt.Qt.AlignHCenter)
            labelMax = qt.QLabel("Max", self)
            labelMax.setAlignment(qt.Qt.AlignHCenter)
            labelMax.setFont(miniFont)
            rangeLayout.addWidget(labelMin, 0, 1)
            rangeLayout.addWidget(labelMax, 0, 3)
            rangeLayout.addWidget(self._minValue, 1, 1)
            rangeLayout.addWidget(self._maxValue, 1, 3)
            rangeLayout.setColumnStretch(0, 1)
            rangeLayout.setColumnStretch(1, 2)
            rangeLayout.setColumnStretch(2, 1)
            rangeLayout.setColumnStretch(3, 2)
            rangeLayout.setColumnStretch(4, 1)

            self._histoWidget = _ColormapHistogram(self)
            self._histoWidget.sigRangeMoving.connect(self._histogramRangeMoving)
            self._histoWidget.sigRangeMoved.connect(self._histogramRangeMoved)
            self._histoWidget.setSizePolicy(
                qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding
            )

            # Scale to buttons
            self._visibleAreaButton = qt.QPushButton(self)
            self._visibleAreaButton.setEnabled(False)
            self._visibleAreaButton.setText("Visible Area")
            self._visibleAreaButton.clicked.connect(
                self._handleScaleToVisibleAreaClicked, type=qt.Qt.QueuedConnection
            )

            # Place-holder for selected area ROI manager
            self._roiForColormapManager = None

            self._selectedAreaButton = qt.QPushButton(self)
            self._selectedAreaButton.setCheckable(True)
            self._selectedAreaButton.setEnabled(False)
            self._selectedAreaButton.setText("Selection")
            self._selectedAreaButton.setIcon(icons.getQIcon("add-shape-rectangle"))
            self._selectedAreaButton.setCheckable(True)
            self._selectedAreaButton.toggled.connect(
                self._handleScaleToSelectionToggled, type=qt.Qt.QueuedConnection
            )

            # define modal buttons
            types = qt.QDialogButtonBox.Ok | qt.QDialogButtonBox.Cancel
            self._buttonsModal = qt.QDialogButtonBox(parent=self)
            self._buttonsModal.setStandardButtons(types)
            self._buttonsModal.accepted.connect(self.accept)
            self._buttonsModal.rejected.connect(self.reject)

            # define non modal buttons
            types = qt.QDialogButtonBox.Close | qt.QDialogButtonBox.Reset
            self._buttonsNonModal = qt.QDialogButtonBox(parent=self)
            self._buttonsNonModal.setStandardButtons(types)
            button = self._buttonsNonModal.button(qt.QDialogButtonBox.Close)
            button.clicked.connect(self.accept)
            button.setDefault(True)
            button = self._buttonsNonModal.button(qt.QDialogButtonBox.Reset)
            button.clicked.connect(self.resetColormap)

            self._buttonsModal.setFocus(qt.Qt.OtherFocusReason)
            self._buttonsNonModal.setFocus(qt.Qt.OtherFocusReason)

            # Set the colormap to default values
            self.setColormap(
                Colormap(name="gray", normalization="linear", vmin=None, vmax=None)
            )

            self.setModal(self.isModal())

            layout = qt.QHBoxLayout()
            layout.setContentsMargins(0, 0, 0, 0)
            layout.addWidget(self._visibleAreaButton)
            layout.addWidget(self._selectedAreaButton)
            layout.addStretch()
            self._scaleToAreaGroup = qt.QWidget(self)
            self._scaleToAreaGroup.setLayout(layout)
            self._scaleToAreaGroup.setVisible(False)

            layoutScale = qt.QHBoxLayout()
            layoutScale.setContentsMargins(0, 0, 0, 0)
            layoutScale.addWidget(self._autoButtons)
            layoutScale.addWidget(self._autoScaleCombo)
            layoutScale.addStretch()

            formLayout = FormGridLayout(self)
            formLayout.setContentsMargins(10, 10, 10, 10)

            formLayout.addRow("Colormap:", self._comboBoxColormap)
            formLayout.addRow("Normalization:", self._comboBoxNormalization)
            formLayout.addRow("Gamma:", self._gammaSpinBox)

            formLayout.addItem(
                qt.QSpacerItem(1, 1, qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
            )
            formLayout.addRow(self._histoWidget)
            formLayout.setRowStretch(formLayout.rowCount() - 1, 1)
            formLayout.addRow(rangeLayout)
            formLayout.addItem(
                qt.QSpacerItem(1, 1, qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
            )
            formLayout.addRow("Scale:", layoutScale)
            formLayout.addRow("Fixed scale on:", self._scaleToAreaGroup)
            formLayout.addRow(self._buttonsModal)
            formLayout.addRow(self._buttonsNonModal)
            formLayout.setSizeConstraint(qt.QLayout.SetMinAndMaxSize)

            self.setTabOrder(self._comboBoxColormap, self._comboBoxNormalization)
            self.setTabOrder(self._comboBoxNormalization, self._gammaSpinBox)
            self.setTabOrder(self._gammaSpinBox, self._minValue)
            self.setTabOrder(self._minValue, self._maxValue)
            self.setTabOrder(self._maxValue, self._autoButtons)
            self.setTabOrder(self._autoButtons, self._autoScaleCombo)
            self.setTabOrder(self._autoScaleCombo, self._visibleAreaButton)
            self.setTabOrder(self._visibleAreaButton, self._selectedAreaButton)
            self.setTabOrder(self._selectedAreaButton, self._buttonsModal)
            self.setTabOrder(self._buttonsModal, self._buttonsNonModal)

            self._applyColormap()

        def getHistogramWidget(self):
            return self._histoWidget

        def _invalidateColormap(self):
            if self.isVisible():
                self._applyColormap()
            else:
                self.__colormapInvalidated = True

        def _invalidateData(self):
            if self.isVisible():
                self._updateWidgetRange()
                self._histoWidget.invalidateData()
            else:
                self.__dataInvalidated = True

        def _validate(self):
            if self.__colormapInvalidated:
                self._applyColormap()
            if self.__dataInvalidated:
                self._histoWidget.invalidateData()
            if self.__dataInvalidated or self.__colormapInvalidated:
                self._updateWidgetRange()
            self.__dataInvalidated = False
            self.__colormapInvalidated = False

        def showEvent(self, event):
            self.visibleChanged.emit(True)
            super(ColormapDialog, self).showEvent(event)
            if self.isVisible():
                self._validate()

        def closeEvent(self, event):
            if not self.isModal():
                self.accept()
            super(ColormapDialog, self).closeEvent(event)

        def hideEvent(self, event):
            if self._selectedAreaButton.isChecked():
                self._selectedAreaButton.setChecked(False)
            self.visibleChanged.emit(False)
            super(ColormapDialog, self).hideEvent(event)

        def close(self):
            if self._selectedAreaButton.isChecked():
                self._selectedAreaButton.setChecked(False)
            self.accept()
            qt.QDialog.close(self)

        def setModal(self, modal):
            assert type(modal) is bool
            self._buttonsNonModal.setVisible(not modal)
            self._buttonsModal.setVisible(modal)
            qt.QDialog.setModal(self, modal)

        def event(self, event):
            if event.type() == qt.QEvent.DeferredDelete:
                self.__aboutToDelete = True
            return super(ColormapDialog, self).event(event)

        def exec(self):
            wasModal = self.isModal()
            self.setModal(True)
            result = super(ColormapDialog, self).exec()
            if not self.__aboutToDelete:
                self.setModal(wasModal)
            return result

        def exec_(self):  # Qt5 compatibility wrapper
            return self.exec()

        def _getFiniteColormapRange(self):
            """Return a colormap range where auto ranges are fixed
            according to the available data.
            """
            colormap = self.getColormap()
            if colormap is None:
                return 1, 10

            item = self._getItem()
            if item is not None:
                return colormap.getColormapRange(item)
            # If there is not item, there is no data
            return colormap.getColormapRange(None)

        @staticmethod
        def computeDataRange(data):
            """Compute the data range as used by :meth:`setDataRange`.

            :param data: The data to process
            :rtype: List[Union[None,float]]
            """
            if data is None or len(data) == 0:
                return None, None, None

            dataRange = min_max(data, min_positive=True, finite=True)
            if dataRange.minimum is None:
                # Only non-finite data
                dataRange = None

            if dataRange is not None:
                dataRange = dataRange.minimum, dataRange.min_positive, dataRange.maximum

            if dataRange is None or len(dataRange) != 3:
                qt.QMessageBox.warning(
                    None, "No Data", "Image data does not contain any real value"
                )
                dataRange = 1.0, 1.0, 10.0

            return dataRange

        @staticmethod
        def computeHistogram(data, scale=Axis.LINEAR, dataRange=None):
            """Compute the data histogram as used by :meth:`setHistogram`.

            :param data: The data to process
            :param dataRange: Optional range to compute the histogram, which is a
                tuple of min, max
            :rtype: Tuple(List(float),List(float)
            """
            # For compatibility
            if scale == Axis.LOGARITHMIC:
                scale = Colormap.LOGARITHM

            if data is None:
                return None, None

            if len(data) == 0:
                return None, None

            if data.ndim == 3:  # RGB(A) images
                _logger.info(
                    "Converting current image from RGB(A) to grayscale\
                    in order to compute the intensity distribution"
                )
                data = data[:, :, 0] * 0.299 + data[:, :, 1] * 0.587 + data[:, :, 2] * 0.114

            # bad hack: get 256 continuous bins in the case we have a B&W
            normalizeData = True
            if numpy.issubdtype(data.dtype, numpy.ubyte):
                normalizeData = False
            elif numpy.issubdtype(data.dtype, numpy.integer):
                if dataRange is not None:
                    xmin, xmax = dataRange
                    if xmin is not None and xmax is not None:
                        normalizeData = (xmax - xmin) > 255

            if normalizeData:
                if scale == Colormap.LOGARITHM:
                    with numpy.errstate(divide="ignore", invalid="ignore"):
                        data = numpy.log10(data)

            if dataRange is not None:
                xmin, xmax = dataRange
                if xmin is None:
                    return None, None
                if normalizeData:
                    if scale == Colormap.LOGARITHM:
                        xmin, xmax = numpy.log10(xmin), numpy.log10(xmax)
            else:
                xmin, xmax = min_max(data, min_positive=False, finite=True)

            if xmin is None:
                return None, None

            nbins = min(256, int(numpy.sqrt(data.size)))
            data_range = xmin, xmax

            # bad hack: get 256 bins in the case we have a B&W
            if numpy.issubdtype(data.dtype, numpy.integer):
                if nbins > xmax - xmin:
                    nbins = int(xmax - xmin)

            nbins = max(2, nbins)
            data = data.ravel().astype(numpy.float32)

            histogram = Histogramnd(data, n_bins=nbins, histo_range=data_range)
            bins = histogram.edges[0]
            if normalizeData:
                if scale == Colormap.LOGARITHM:
                    bins = 10**bins
            return histogram.histo, bins

        def _getItem(self):
            if self._itemHolder is not None:
                return self._itemHolder
            if self._item is None:
                return None
            return self._item()

        def setItem(self, item):
            """Store the plot item.

            According to the state of the dialog, the item will be used to display
            the data range or the histogram of the data using :meth:`setDataRange`
            and :meth:`setHistogram`
            """
            old = self._getItem()
            if old is item:
                # While event from items are not supported, we can't ignore dup items
                if item is not None:
                    array = item.getColormappedData(copy=False)
                else:
                    array = None
                colormapped = self._colormapped
                if colormapped is not None:
                    oldArray = colormapped()
                else:
                    oldArray = None
                if oldArray is array:
                    return

            self.__resetItem()
            try:
                if item is None:
                    self._item = None
                else:
                    if not isinstance(item, items.ColormapMixIn):
                        self._item = None
                        raise ValueError("Item %s is not supported" % item)
                    item.sigItemChanged.connect(self.__itemChanged)
                    self._item = weakref.ref(item, self._itemAboutToFinalize)
            finally:
                self._syncScaleToButtonsEnabled()
                self._dataRange = None
                self._histogramData = None
                self._invalidateData()

        def __resetItem(self):
            """Reset item and data used by the dialog"""
            self._data = None
            self._itemHolder = None
            if self._item is not None:
                item = self._item()
                self._item = None
                if item is not None:
                    item.sigItemChanged.disconnect(self.__itemChanged)

        def __itemChanged(self, event):
            if event == items.ItemChangedType.DATA:
                self._invalidateData()

        def _getData(self):
            if self._data is None:
                return None
            return self._data()

        def setData(self, data):
            """Store the data

            According to the state of the dialog, the data will be used to display
            the data range or the histogram of the data using :meth:`setDataRange`
            and :meth:`setHistogram`
            """
            oldData = self._getData()
            if oldData is data:
                return

            self.__resetItem()
            self._syncScaleToButtonsEnabled()
            if data is not None:
                self._data = weakref.ref(data, self._dataAboutToFinalize)
                self._itemHolder = _DataRefHolder(self._data)

            self._dataRange = None
            self._histogramData = None

            self._invalidateData()

        def _getArray(self):
            data = self._getData()
            if data is not None:
                return data
            item = self._getItem()
            if item is not None:
                colormapped = item.getColormappedData(copy=False)
                if colormapped is not None:
                    self._colormapped = weakref.ref(colormapped)
                else:
                    self._colormapped = None
                return colormapped
            return None

        def _colormapAboutToFinalize(self, weakrefColormap):
            """Callback when the data weakref is about to be finalized."""
            if self._colormap is weakrefColormap and qtinspect.isValid(self):
                self.setColormap(None)

        def _dataAboutToFinalize(self, weakrefData):
            """Callback when the data weakref is about to be finalized."""
            if self._data is weakrefData and qtinspect.isValid(self):
                self.setData(None)

        def _itemAboutToFinalize(self, weakref):
            """Callback when the data weakref is about to be finalized."""
            if self._item is weakref and qtinspect.isValid(self):
                self.setItem(None)

        def _getHistogram(self):
            """Returns the histogram defined by the dialog as metadata
            to describe the data in order to speed up the dialog.

            :return: (hist, bin_edges)
            :rtype: 2-tuple of numpy arrays"""
            return self._histogramData

        def setHistogram(self, hist=None, bin_edges=None):
            """Set the histogram to display.

            This update the data range with the bounds of the bins.

            :param hist: array-like of counts or None to hide histogram
            :param bin_edges: array-like of bins edges or None to hide histogram
            """
            if hist is None or bin_edges is None:
                self._histogramData = None
            else:
                self._histogramData = numpy.array(hist), numpy.array(bin_edges)

            self._invalidateData()

        def getColormap(self):
            """Return the colormap description.

            :rtype: ~silx.gui.colors.Colormap
            """
            if self._colormap is None:
                return None
            return self._colormap()

        def resetColormap(self):
            """
            Reset the colormap state before modification.

            ..note :: the colormap reference state is the state when set or the
                      state when validated
            """
            colormap = self.getColormap()
            if colormap is not None and self._colormapStoredState is not None:
                if colormap != self._colormapStoredState:
                    with self._colormapChange:
                        colormap.setFromColormap(self._colormapStoredState)
                    self._applyColormap()

        def _getDataRange(self):
            """Returns the data range defined by the dialog as metadata
            to describe the data in order to speed up the dialog.

            :return: (minimum, positiveMin, maximum)
            :rtype: 3-tuple of floats or None"""
            return self._dataRange

        def setDataRange(self, minimum=None, positiveMin=None, maximum=None):
            """Set the range of data to use for the range of the histogram area.

            :param float minimum: The minimum of the data
            :param float positiveMin: The positive minimum of the data
            :param float maximum: The maximum of the data
            """
            self._dataRange = minimum, positiveMin, maximum
            self._invalidateData()

        def _setColormapRange(self, xmin, xmax):
            """Set a new range to the held colormap and update the
            widget."""
            colormap = self.getColormap()
            if colormap is not None:
                with self._colormapChange:
                    colormap.setVRange(xmin, xmax)
            self._updateWidgetRange()

        def setColormapRangeFromDataBounds(self, bounds):
            """Set the range of the colormap from current item and rect.

            If there is no ColormapMixIn item attached to the ColormapDialog,
            nothing is done.

            :param Union[List[float],None] bounds:
                (xmin, xmax, ymin, ymax) Rectangular region in data space
            """
            if bounds is None:
                return  # no-op

            colormap = self.getColormap()
            if colormap is None:
                return  # no-op

            item = self._getItem()
            if not isinstance(item, items.ColormapMixIn):
                return  # no-op

            data = item.getColormappedData(copy=False)
            xmin, xmax, ymin, ymax = bounds

            if isinstance(item, items.ImageBase):
                if data.ndim != 2:
                    return  # no-op

                ox, oy = item.getOrigin()
                sx, sy = item.getScale()

                ystart = max(0, int((ymin - oy) / sy))
                ystop = max(0, int(numpy.ceil((ymax - oy) / sy)))
                xstart = max(0, int((xmin - ox) / sx))
                xstop = max(0, int(numpy.ceil((xmax - ox) / sx)))

                subset = data[ystart:ystop, xstart:xstop]

            elif isinstance(item, items.Scatter):
                x = item.getXData(copy=False)
                y = item.getYData(copy=False)
                subset = data[
                    numpy.logical_and(
                        numpy.logical_and(xmin <= x, x <= xmax),
                        numpy.logical_and(ymin <= y, y <= ymax),
                    )
                ]

            if subset.size == 0:
                return  # no-op

            vmin, vmax = colormap._computeAutoscaleRange(subset)
            self._setColormapRange(vmin, vmax)

        def _updateWidgetRange(self):
            """Update the colormap range displayed into the widget."""
            xmin, xmax = self._getFiniteColormapRange()
            colormap = self.getColormap()
            if colormap is not None:
                vRange = colormap.getVRange()
                autoMin, autoMax = (r is None for r in vRange)
            else:
                autoMin, autoMax = False, False

            with utils.blockSignals(self._minValue):
                self._minValue.setValue(xmin, autoMin)
            with utils.blockSignals(self._maxValue):
                self._maxValue.setValue(xmax, autoMax)
            with utils.blockSignals(self._histoWidget):
                self._histoWidget.setFiniteRange((xmin, xmax))
            with utils.blockSignals(self._autoButtons):
                self._autoButtons.setAutoRange((autoMin, autoMax))

        def accept(self):
            self.storeCurrentState()
            qt.QDialog.accept(self)

        def storeCurrentState(self):
            """
            save the current value sof the colormap if the user want to undo is
            modifications
            """
            colormap = self.getColormap()
            if colormap is not None:
                self._colormapStoredState = colormap.copy()
            else:
                self._colormapStoredState = None

        def reject(self):
            self.resetColormap()
            qt.QDialog.reject(self)

        def setColormap(self, colormap):
            """Set the colormap description

            :param ~silx.gui.colors.Colormap colormap: the colormap to edit
            """
            assert colormap is None or isinstance(colormap, Colormap)
            if self._colormapChange.locked():
                return

            oldColormap = self.getColormap()
            if oldColormap is colormap:
                return
            if oldColormap is not None:
                oldColormap.sigChanged.disconnect(self._applyColormap)

            if colormap is not None:
                colormap.sigChanged.connect(self._applyColormap)
                colormap = weakref.ref(colormap, self._colormapAboutToFinalize)

            self._colormap = colormap
            self.storeCurrentState()
            self._invalidateColormap()

        def _updateResetButton(self):
            resetButton = self._buttonsNonModal.button(qt.QDialogButtonBox.Reset)
            rStateEnabled = False
            colormap = self.getColormap()
            if colormap is not None and colormap.isEditable():
                # can reset only in the case the colormap changed
                rStateEnabled = colormap != self._colormapStoredState
            resetButton.setEnabled(rStateEnabled)

        def _applyColormap(self):
            self._updateResetButton()
            if self._colormapChange.locked():
                return

            self._syncScaleToButtonsEnabled()

            colormap = self.getColormap()
            if colormap is None:
                self._comboBoxColormap.setEnabled(False)
                self._comboBoxNormalization.setEnabled(False)
                self._gammaSpinBox.setEnabled(False)
                self._autoScaleCombo.setEnabled(False)
                self._minValue.setEnabled(False)
                self._maxValue.setEnabled(False)
                self._autoButtons.setEnabled(False)
                self._histoWidget.setVisible(False)
                self._histoWidget.setFiniteRange((None, None))
            else:
                assert colormap.getNormalization() in Colormap.NORMALIZATIONS
                with utils.blockSignals(self._comboBoxColormap):
                    self._comboBoxColormap.setCurrentLut(colormap)
                    self._comboBoxColormap.setEnabled(colormap.isEditable())
                with utils.blockSignals(self._comboBoxNormalization):
                    index = self._comboBoxNormalization.findData(
                        colormap.getNormalization()
                    )
                    if index < 0:
                        _logger.error(
                            "Unsupported normalization: %s" % colormap.getNormalization()
                        )
                    else:
                        self._comboBoxNormalization.setCurrentIndex(index)
                    self._comboBoxNormalization.setEnabled(colormap.isEditable())
                with utils.blockSignals(self._gammaSpinBox):
                    self._gammaSpinBox.setValue(colormap.getGammaNormalizationParameter())
                    self._gammaSpinBox.setEnabled(
                        colormap.getNormalization() == Colormap.GAMMA
                        and colormap.isEditable()
                    )
                with utils.blockSignals(self._autoScaleCombo):
                    self._autoScaleCombo.setCurrentMode(colormap.getAutoscaleMode())
                    self._autoScaleCombo.setEnabled(colormap.isEditable())
                with utils.blockSignals(self._autoButtons):
                    self._autoButtons.setEnabled(colormap.isEditable())
                    self._autoButtons.setAutoRangeFromColormap(colormap)

                vmin, vmax = colormap.getVRange()
                if vmin is None or vmax is None:
                    # Compute it only if needed
                    dataRange = self._getFiniteColormapRange()
                else:
                    dataRange = vmin, vmax

                with utils.blockSignals(self._minValue):
                    self._minValue.setValue(vmin or dataRange[0], isAuto=vmin is None)
                    self._minValue.setEnabled(colormap.isEditable())
                with utils.blockSignals(self._maxValue):
                    self._maxValue.setValue(vmax or dataRange[1], isAuto=vmax is None)
                    self._maxValue.setEnabled(colormap.isEditable())

                with utils.blockSignals(self._histoWidget):
                    self._histoWidget.setVisible(True)
                    self._histoWidget.setFiniteRange(dataRange)
                    self._histoWidget.updateNormalization()

        def _comboBoxColormapUpdated(self):
            """Callback executed when the combo box with the colormap LUT
            is updated by user input.
            """
            colormap = self.getColormap()
            if colormap is not None:
                with self._colormapChange:
                    name = self._comboBoxColormap.getCurrentName()
                    if name is not None:
                        colormap.setName(name)
                    else:
                        lut = self._comboBoxColormap.getCurrentColors()
                        colormap.setColormapLUT(lut)
            self._histoWidget.updateLut()

        def _autoRangeButtonsUpdated(self, autoRange):
            """Callback executed when the autoscale buttons widget
            is updated by user input.
            """
            dataRange = self._getFiniteColormapRange()

            # Final colormap range
            vmin = dataRange[0] if not autoRange[0] else None
            vmax = dataRange[1] if not autoRange[1] else None

            with self._colormapChange:
                colormap = self.getColormap()
                colormap.setVRange(vmin, vmax)

            with utils.blockSignals(self._minValue):
                self._minValue.setValue(vmin or dataRange[0], isAuto=vmin is None)
            with utils.blockSignals(self._maxValue):
                self._maxValue.setValue(vmax or dataRange[1], isAuto=vmax is None)

            self._updateWidgetRange()

        def _normalizationUpdated(self, index):
            """Callback executed when the normalization widget
            is updated by user input.
            """
            colormap = self.getColormap()
            if colormap is not None:
                normalization = self._comboBoxNormalization.itemData(index)
                self._gammaSpinBox.setEnabled(normalization == "gamma")

                with self._colormapChange:
                    colormap.setNormalization(normalization)
                    self._histoWidget.updateNormalization()

            self._updateWidgetRange()

        def _gammaUpdated(self, value):
            """Callback used to update the gamma normalization parameter"""
            colormap = self.getColormap()
            if colormap is not None:
                colormap.setGammaNormalizationParameter(value)

        def _autoscaleModeUpdated(self):
            """Callback executed when the autoscale mode widget
            is updated by user input.
            """
            mode = self._autoScaleCombo.currentMode()

            colormap = self.getColormap()
            if colormap is not None:
                with self._colormapChange:
                    colormap.setAutoscaleMode(mode)

            self._updateWidgetRange()

        def _minAutoscaleUpdated(self, autoEnabled):
            """Callback executed when the min autoscale from
            the lineedit is updated by user input"""
            colormap = self.getColormap()
            xmin, xmax = colormap.getVRange()
            if autoEnabled:
                xmin = None
            else:
                xmin, _xmax = self._getFiniteColormapRange()
            self._setColormapRange(xmin, xmax)

        def _maxAutoscaleUpdated(self, autoEnabled):
            """Callback executed when the max autoscale from
            the lineedit is updated by user input"""
            colormap = self.getColormap()
            xmin, xmax = colormap.getVRange()
            if autoEnabled:
                xmax = None
            else:
                _xmin, xmax = self._getFiniteColormapRange()
            self._setColormapRange(xmin, xmax)

        def _minValueUpdated(self, value):
            """Callback executed when the lineedit min value is
            updated by user input"""
            xmin = value
            xmax = self._maxValue.getValue()
            if xmax is not None and xmin > xmax:
                # FIXME: This should be done in the widget itself
                xmin = xmax
                with utils.blockSignals(self._minValue):
                    self._minValue.setValue(xmin)
            self._setColormapRange(xmin, xmax)

        def _maxValueUpdated(self, value):
            """Callback executed when the lineedit max value is
            updated by user input"""
            xmin = self._minValue.getValue()
            xmax = value
            if xmin is not None and xmin > xmax:
                # FIXME: This should be done in the widget itself
                xmax = xmin
                with utils.blockSignals(self._maxValue):
                    self._maxValue.setValue(xmax)
            self._setColormapRange(xmin, xmax)

        def _histogramRangeMoving(self, vmin, vmax, gammaPos):
            """Callback executed when for colormap range displayed in
            the histogram widget is moving.

            :param vmin: Update of the minimum range, else None
            :param vmax: Update of the maximum range, else None
            :param gammaPos: Update of the gamma location, else None
            """
            colormap = self.getColormap()
            if vmin is not None:
                with self._colormapChange:
                    colormap.setVMin(vmin)
                self._minValue.setValue(vmin)
            if vmax is not None:
                with self._colormapChange:
                    colormap.setVMax(vmax)
                self._maxValue.setValue(vmax)
            if gammaPos is not None:
                vmin, vmax = self._histoWidget.getFiniteRange()
                if vmax < vmin:
                    gamma = 1
                elif gammaPos >= vmax:
                    gamma = self._gammaSpinBox.maximum()
                elif gammaPos <= vmin:
                    gamma = self._gammaSpinBox.minimum()
                else:
                    gamma = numpy.clip(
                        numpy.log(0.5) / numpy.log((gammaPos - vmin) / (vmax - vmin)),
                        self._gammaSpinBox.minimum(),
                        self._gammaSpinBox.maximum(),
                    )
                with self._colormapChange:
                    colormap.setGammaNormalizationParameter(gamma)
                with utils.blockSignals(self._gammaSpinBox):
                    self._gammaSpinBox.setValue(gamma)

        def _histogramRangeMoved(self, vmin, vmax, gammaPos):
            """Callback executed when for colormap range displayed in
            the histogram widget has finished to move
            """
            if vmin is None and vmax is None:
                return
            xmin = self._minValue.getValue()
            xmax = self._maxValue.getValue()
            if vmin is None:
                vmin = xmin
            if vmax is None:
                vmax = xmax
            self._setColormapRange(vmin, vmax)

        def _syncScaleToButtonsEnabled(self):
            """Set the state of scale to buttons according to current item and colormap"""
            colormap = self.getColormap()
            enabled = (
                self._item is not None and colormap is not None and colormap.isEditable()
            )
            self._scaleToAreaGroup.setVisible(enabled)
            self._visibleAreaButton.setEnabled(enabled)
            if not enabled:
                self._selectedAreaButton.setChecked(False)
            self._selectedAreaButton.setEnabled(enabled)

        def _handleScaleToVisibleAreaClicked(self):
            """Set colormap range from current item's visible area"""
            item = self._getItem()
            if item is None:
                return  # no-op

            bounds = item.getVisibleBounds()
            if bounds is None:
                return  # no-op

            self.setColormapRangeFromDataBounds(bounds)

        def _handleScaleToSelectionToggled(self, checked=False):
            """Handle toggle of scale to selected are button"""
            # Reset any previous ROI manager
            if self._roiForColormapManager is not None:
                self._roiForColormapManager.clear()
                self._roiForColormapManager.stop()
                self._roiForColormapManager = None

            if not checked:  # Reset button status
                self._selectedAreaButton.setText("Selection")
                return

            item = self._getItem()
            if item is None:
                self._selectedAreaButton.setChecked(False)
                return  # no-op

            plotWidget = item.getPlot()
            if plotWidget is None:
                self._selectedAreaButton.setChecked(False)
                return  # no-op

            self._selectedAreaButton.setText("Draw Area...")

            self._roiForColormapManager = RegionOfInterestManager(parent=plotWidget)
            cmap = self.getColormap()
            self._roiForColormapManager.setColor(
                "black" if cmap is None else cursorColorForColormap(cmap.getName())
            )
            self._roiForColormapManager.sigInteractiveModeFinished.connect(
                self.__roiInteractiveModeFinished
            )
            self._roiForColormapManager.sigInteractiveRoiFinalized.connect(
                self.__roiFinalized
            )
            self._roiForColormapManager.start(RectangleROI)

        def __roiInteractiveModeFinished(self):
            self._selectedAreaButton.setChecked(False)

        def __roiFinalized(self, roi):
            if roi is not None:
                ox, oy = roi.getOrigin()
                width, height = roi.getSize()
                self.setColormapRangeFromDataBounds((ox, ox + width, oy, oy + height))
                # clear ROI
                self._roiForColormapManager.removeRoi(roi)

        def keyPressEvent(self, event):
            """Override key handling.

            It disables leaving the dialog when editing a text field.

            But several press of Return key can be use to validate and close the
            dialog.
            """
            if event.key() in (qt.Qt.Key_Enter, qt.Qt.Key_Return):
                # Bypass QDialog keyPressEvent
                # To avoid leaving the dialog when pressing enter on a text field
                if self._minValue.hasFocus():
                    nextFocus = self._maxValue
                elif self._maxValue.hasFocus():
                    if self.isModal():
                        nextFocus = self._buttonsModal.button(qt.QDialogButtonBox.Apply)
                    else:
                        nextFocus = self._buttonsNonModal.button(qt.QDialogButtonBox.Close)
                else:
                    nextFocus = None
                if nextFocus is not None:
                    nextFocus.setFocus(qt.Qt.OtherFocusReason)
            else:
                super(ColormapDialog, self).keyPressEvent(event)