Skip to main content
`Ctrl`+`K`

# Source code for silx.gui.plot3d.SFViewParamTree

    # /*##########################################################################
    #
    # Copyright (c) 2015-2021 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.
    #
    # ###########################################################################*/
    """
    This module provides a tree widget to set/view parameters of a ScalarFieldView.
    """

    __authors__ = ["D. N."]
    __license__ = "MIT"
    __date__ = "24/04/2018"

    import logging
    import sys
    import weakref

    import numpy

    from silx.gui import qt
    from silx.gui.icons import getQIcon
    from silx.gui.colors import Colormap
    from silx.gui.widgets.FloatEdit import FloatEdit

    from .ScalarFieldView import Isosurface

    _logger = logging.getLogger(__name__)

    class ModelColumns(object):
        NameColumn, ValueColumn, ColumnMax = range(3)
        ColumnNames = ["Name", "Value"]

    class SubjectItem(qt.QStandardItem):
        """
        Base class for observers items.

        Subclassing:
        ------------
        The following method can/should be reimplemented:
        - _init
        - _pullData
        - _pushData
        - _setModelData
        - _subjectChanged
        - getEditor
        - getSignals
        - leftClicked
        - queryRemove
        - setEditorData

        Also the following attributes are available:
        - editable
        - persistent

        :param subject: object that this item will be observing.
        """

        editable = False
        """ boolean: set to True to make the item editable. """

        persistent = False
        """
        boolean: set to True to make the editor persistent.
            See : Qt.QAbstractItemView.openPersistentEditor
        """

        def __init__(self, subject, *args):
            super(SubjectItem, self).__init__(*args)

            self.setEditable(self.editable)

            self.__subject = None
            self.subject = subject

        def setData(self, value, role=qt.Qt.UserRole, pushData=True):
            """
            Overloaded method from QStandardItem. The pushData keyword tells
            the item to push data to the subject if the role is equal to EditRole.
            This is useful to let this method know if the setData method was called
            internally or from the view.

            :param value: the value ti set to data
            :param role: role in the item
            :param pushData: if True push value in the existing data.
            """
            if role == qt.Qt.EditRole and pushData:
                setValue = self._pushData(value, role)
                if setValue != value:
                    value = setValue
            super(SubjectItem, self).setData(value, role)

        @property
        def subject(self):
            """The subject this item is observing"""
            return None if self.__subject is None else self.__subject()

        @subject.setter
        def subject(self, subject):
            if self.__subject is not None:
                raise ValueError("Subject already set " " (subject change not supported).")
            if subject is None:
                self.__subject = None
            else:
                self.__subject = weakref.ref(subject)
            if subject is not None:
                self._init()
                self._connectSignals()

        def _connectSignals(self):
            """
            Connects the signals. Called when the subject is set.
            """

            def gen_slot(_sigIdx):
                def slotfn(*args, **kwargs):
                    self._subjectChanged(signalIdx=_sigIdx, args=args, kwargs=kwargs)

                return slotfn

            if self.__subject is not None:
                self.__slots = slots = []

                signals = self.getSignals()

                if signals:
                    if not isinstance(signals, (list, tuple)):
                        signals = [signals]
                    for sigIdx, signal in enumerate(signals):
                        slot = gen_slot(sigIdx)
                        signal.connect(slot)
                        slots.append((signal, slot))

        def _disconnectSignals(self):
            """
            Disconnects all subject's signal
            """
            if self.__slots:
                for signal, slot in self.__slots:
                    try:
                        signal.disconnect(slot)
                    except (RuntimeError, TypeError, SystemError):
                        pass

        def _enableRow(self, enable):
            """
            Set the enabled state for this cell, or for the whole row
            if this item has a parent.

            :param bool enable: True if we wan't to enable the cell
            """
            parent = self.parent()
            model = self.model()
            if model is None or parent is None:
                # no parent -> no siblings
                self.setEnabled(enable)
                return

            for col in range(model.columnCount()):
                sibling = parent.child(self.row(), col)
                sibling.setEnabled(enable)

        #################################################################
        # Overloadable methods
        #################################################################

        def getSignals(self):
            """
            Returns the list of this items subject's signals that
            this item will be listening to.

            :return: list.
            """
            return None

        def _subjectChanged(self, signalIdx=None, args=None, kwargs=None):
            """
            Called when one of the signals is triggered. Default implementation
            just calls _pullData, compares the result to the current value stored
            as Qt.EditRole, and stores the new value if it is different. It also
            stores its str representation as Qt.DisplayRole

            :param signalIdx: index of the triggered signal. The value passed
                is the same as the signal position in the list returned by
                SubjectItem.getSignals.
            :param args: arguments received from the signal
            :param kwargs: keyword arguments received from the signal
            """
            data = self._pullData()
            if data == self.data(qt.Qt.EditRole):
                return
            self.setData(data, role=qt.Qt.DisplayRole, pushData=False)
            self.setData(data, role=qt.Qt.EditRole, pushData=False)

        def _pullData(self):
            """
            Pulls data from the subject.

            :return: subject data
            """
            return None

        def _pushData(self, value, role=qt.Qt.UserRole):
            """
            Pushes data to the subject and returns the actual value that was stored

            :return: the value that was stored
            """
            return value

        def _init(self):
            """
            Called when the subject is set.
            :return:
            """
            self._subjectChanged()

        def getEditor(self, parent, option, index):
            """
            Returns the editor widget used to edit this item's data. The arguments
            are the one passed to the QStyledItemDelegate.createEditor method.

            :param parent: the Qt parent of the editor
            :param option:
            :param index:
            :return:
            """
            return None

        def setEditorData(self, editor):
            """
            This is called by the View's delegate just before the editor is shown,
            its purpose it to setup the editors contents. Return False to use
            the delegate's default behaviour.

            :param editor:
            :return:
            """
            return True

        def _setModelData(self, editor):
            """
            This is called by the View's delegate just before the editor is closed,
            its allows this item to update itself with data from the editor.

            :param editor:
            :return:
            """
            return False

        def queryRemove(self, view=None):
            """
            This is called by the view to ask this items if it (the view) can
            remove it. Return True to let the view know that the item can be
            removed.

            :param view:
            :return:
            """
            return False

        def leftClicked(self):
            """
            This method is called by the view when the item's cell if left clicked.

            :return:
            """
            pass

    # View settings ###############################################################

    class ColorItem(SubjectItem):
        """color item."""

        editable = True
        persistent = True

        def getEditor(self, parent, option, index):
            editor = QColorEditor(parent)
            editor.color = self.getColor()

            # Wrapping call in lambda is a workaround for PySide with Python 3
            editor.sigColorChanged.connect(lambda color: self._editorSlot(color))
            return editor

        def _editorSlot(self, color):
            self.setData(color, qt.Qt.EditRole)

        def _pushData(self, value, role=qt.Qt.UserRole):
            self.setColor(value)
            return self.getColor()

        def _pullData(self):
            self.getColor()

        def setColor(self, color):
            """Override to implement actual color setter"""
            pass

    class BackgroundColorItem(ColorItem):
        itemName = "Background"

        def setColor(self, color):
            self.subject.setBackgroundColor(color)

        def getColor(self):
            return self.subject.getBackgroundColor()

    class ForegroundColorItem(ColorItem):
        itemName = "Foreground"

        def setColor(self, color):
            self.subject.setForegroundColor(color)

        def getColor(self):
            return self.subject.getForegroundColor()

    class HighlightColorItem(ColorItem):
        itemName = "Highlight"

        def setColor(self, color):
            self.subject.setHighlightColor(color)

        def getColor(self):
            return self.subject.getHighlightColor()

    class _LightDirectionAngleBaseItem(SubjectItem):
        """Base class for directional light angle item."""

        editable = True
        persistent = True

        def _init(self):
            pass

        def getSignals(self):
            """Override to provide signals to listen"""
            raise NotImplementedError("MUST be implemented in subclass")

        def _pullData(self):
            """Override in subclass to get current angle"""
            raise NotImplementedError("MUST be implemented in subclass")

        def _pushData(self, value, role=qt.Qt.UserRole):
            """Override in subclass to set the angle"""
            raise NotImplementedError("MUST be implemented in subclass")

        def getEditor(self, parent, option, index):
            editor = qt.QSlider(parent)
            editor.setOrientation(qt.Qt.Horizontal)
            editor.setMinimum(-90)
            editor.setMaximum(90)
            editor.setValue(int(self._pullData()))

            # Wrapping call in lambda is a workaround for PySide with Python 3
            editor.valueChanged.connect(lambda value: self._pushData(value))

            return editor

        def setEditorData(self, editor):
            editor.setValue(int(self._pullData()))
            return True

        def _setModelData(self, editor):
            value = editor.value()
            self._pushData(value)
            return True

    class LightAzimuthAngleItem(_LightDirectionAngleBaseItem):
        """Light direction azimuth angle item."""

        def getSignals(self):
            return self.subject.sigAzimuthAngleChanged

        def _pullData(self):
            return self.subject.getAzimuthAngle()

        def _pushData(self, value, role=qt.Qt.UserRole):
            self.subject.setAzimuthAngle(value)

    class LightAltitudeAngleItem(_LightDirectionAngleBaseItem):
        """Light direction altitude angle item."""

        def getSignals(self):
            return self.subject.sigAltitudeAngleChanged

        def _pullData(self):
            return self.subject.getAltitudeAngle()

        def _pushData(self, value, role=qt.Qt.UserRole):
            self.subject.setAltitudeAngle(value)

    class _DirectionalLightProxy(qt.QObject):
        """Proxy to handle directional light with angles rather than vector."""

        sigAzimuthAngleChanged = qt.Signal()
        """Signal sent when the azimuth angle has changed."""

        sigAltitudeAngleChanged = qt.Signal()
        """Signal sent when altitude angle has changed."""

        def __init__(self, light):
            super(_DirectionalLightProxy, self).__init__()
            self._light = light
            light.addListener(self._directionUpdated)
            self._azimuth = 0.0
            self._altitude = 0.0

        def getAzimuthAngle(self):
            """Returns the signed angle in the horizontal plane.

             Unit: degrees.
            The 0 angle corresponds to the axis perpendicular to the screen.

            :rtype: float
            """
            return self._azimuth

        def getAltitudeAngle(self):
            """Returns the signed vertical angle from the horizontal plane.

            Unit: degrees.
            Range: [-90, +90]

            :rtype: float
            """
            return self._altitude

        def setAzimuthAngle(self, angle):
            """Set the horizontal angle.

            :param float angle: Angle from -z axis in zx plane in degrees.
            """
            if angle != self._azimuth:
                self._azimuth = angle
                self._updateLight()
                self.sigAzimuthAngleChanged.emit()

        def setAltitudeAngle(self, angle):
            """Set the horizontal angle.

            :param float angle: Angle from -z axis in zy plane in degrees.
            """
            if angle != self._altitude:
                self._altitude = angle
                self._updateLight()
                self.sigAltitudeAngleChanged.emit()

        def _directionUpdated(self, *args, **kwargs):
            """Handle light direction update in the scene"""
            # Invert direction to manipulate the 'source' pointing to
            # the center of the viewport
            x, y, z = -self._light.direction

            # Horizontal plane is plane xz
            azimuth = numpy.degrees(numpy.arctan2(x, z))
            altitude = numpy.degrees(numpy.pi / 2.0 - numpy.arccos(y))

            if (
                abs(azimuth - self.getAzimuthAngle()) > 0.01
                and abs(abs(altitude) - 90.0) >= 0.001
            ):  # Do not update when at zenith
                self.setAzimuthAngle(azimuth)

            if abs(altitude - self.getAltitudeAngle()) > 0.01:
                self.setAltitudeAngle(altitude)

        def _updateLight(self):
            """Update light direction in the scene"""
            azimuth = numpy.radians(self._azimuth)
            delta = numpy.pi / 2.0 - numpy.radians(self._altitude)
            z = -numpy.sin(delta) * numpy.cos(azimuth)
            x = -numpy.sin(delta) * numpy.sin(azimuth)
            y = -numpy.cos(delta)
            self._light.direction = x, y, z

    class DirectionalLightGroup(SubjectItem):
        """
        Root Item for the directional light
        """

        def __init__(self, subject, *args):
            self._light = _DirectionalLightProxy(subject.getPlot3DWidget().viewport.light)

            super(DirectionalLightGroup, self).__init__(subject, *args)

        def _init(self):
            nameItem = qt.QStandardItem("Azimuth")
            nameItem.setEditable(False)
            valueItem = LightAzimuthAngleItem(self._light)
            self.appendRow([nameItem, valueItem])

            nameItem = qt.QStandardItem("Altitude")
            nameItem.setEditable(False)
            valueItem = LightAltitudeAngleItem(self._light)
            self.appendRow([nameItem, valueItem])

    class BoundingBoxItem(SubjectItem):
        """Bounding box, axes labels and grid visibility item.

        Item is checkable.
        """

        itemName = "Bounding Box"

        def _init(self):
            visible = self.subject.isBoundingBoxVisible()
            self.setCheckable(True)
            self.setCheckState(qt.Qt.Checked if visible else qt.Qt.Unchecked)

        def leftClicked(self):
            checked = self.checkState() == qt.Qt.Checked
            if checked != self.subject.isBoundingBoxVisible():
                self.subject.setBoundingBoxVisible(checked)

    class OrientationIndicatorItem(SubjectItem):
        """Orientation indicator visibility item.

        Item is checkable.
        """

        itemName = "Axes indicator"

        def _init(self):
            plot3d = self.subject.getPlot3DWidget()
            visible = plot3d.isOrientationIndicatorVisible()
            self.setCheckable(True)
            self.setCheckState(qt.Qt.Checked if visible else qt.Qt.Unchecked)

        def leftClicked(self):
            plot3d = self.subject.getPlot3DWidget()
            checked = self.checkState() == qt.Qt.Checked
            if checked != plot3d.isOrientationIndicatorVisible():
                plot3d.setOrientationIndicatorVisible(checked)

    class ViewSettingsItem(qt.QStandardItem):
        """Viewport settings"""

        def __init__(self, subject, *args):
            super(ViewSettingsItem, self).__init__(*args)

            self.setEditable(False)

            classes = (
                BackgroundColorItem,
                ForegroundColorItem,
                HighlightColorItem,
                BoundingBoxItem,
                OrientationIndicatorItem,
            )
            for cls in classes:
                titleItem = qt.QStandardItem(cls.itemName)
                titleItem.setEditable(False)
                self.appendRow([titleItem, cls(subject)])

            nameItem = DirectionalLightGroup(subject, "Light Direction")
            valueItem = qt.QStandardItem()
            self.appendRow([nameItem, valueItem])

    # Data information ############################################################

    class DataChangedItem(SubjectItem):
        """
        Base class for items listening to ScalarFieldView.sigDataChanged
        """

        def getSignals(self):
            subject = self.subject
            if subject:
                return subject.sigDataChanged, subject.sigTransformChanged
            return None

        def _init(self):
            self._subjectChanged()

    class DataTypeItem(DataChangedItem):
        itemName = "dtype"

        def _pullData(self):
            data = self.subject.getData(copy=False)
            return ((data is not None) and str(data.dtype)) or "N/A"

    class DataShapeItem(DataChangedItem):
        itemName = "size"

        def _pullData(self):
            data = self.subject.getData(copy=False)
            if data is None:
                return "N/A"
            else:
                return str(list(reversed(data.shape)))

    class OffsetItem(DataChangedItem):
        itemName = "offset"

        def _pullData(self):
            offset = self.subject.getTranslation()
            return ((offset is not None) and str(offset)) or "N/A"

    class ScaleItem(DataChangedItem):
        itemName = "scale"

        def _pullData(self):
            scale = self.subject.getScale()
            return ((scale is not None) and str(scale)) or "N/A"

    class MatrixItem(DataChangedItem):
        def __init__(self, subject, row, *args):
            self.__row = row
            super(MatrixItem, self).__init__(subject, *args)

        def _pullData(self):
            matrix = self.subject.getTransformMatrix()
            return str(matrix[self.__row])

    class DataSetItem(qt.QStandardItem):
        def __init__(self, subject, *args):
            super(DataSetItem, self).__init__(*args)

            self.setEditable(False)

            klasses = [DataTypeItem, DataShapeItem, OffsetItem]
            for klass in klasses:
                titleItem = qt.QStandardItem(klass.itemName)
                titleItem.setEditable(False)
                self.appendRow([titleItem, klass(subject)])

            matrixItem = qt.QStandardItem("matrix")
            matrixItem.setEditable(False)
            valueItem = qt.QStandardItem()
            self.appendRow([matrixItem, valueItem])

            for row in range(3):
                titleItem = qt.QStandardItem()
                titleItem.setEditable(False)
                valueItem = MatrixItem(subject, row)
                matrixItem.appendRow([titleItem, valueItem])

            titleItem = qt.QStandardItem(ScaleItem.itemName)
            titleItem.setEditable(False)
            self.appendRow([titleItem, ScaleItem(subject)])

    # Isosurface ##################################################################

    class IsoSurfaceRootItem(SubjectItem):
        """
        Root (i.e : column index 0) Isosurface item.
        """

        def __init__(self, subject, normalization, *args):
            self._isoLevelSliderNormalization = normalization
            super(IsoSurfaceRootItem, self).__init__(subject, *args)

        def getSignals(self):
            subject = self.subject
            return [subject.sigColorChanged, subject.sigVisibilityChanged]

        def _subjectChanged(self, signalIdx=None, args=None, kwargs=None):
            if signalIdx == 0:
                color = self.subject.getColor()
                self.setData(color, qt.Qt.DecorationRole)
            elif signalIdx == 1:
                visible = args[0]
                self.setCheckState((visible and qt.Qt.Checked) or qt.Qt.Unchecked)

        def _init(self):
            self.setCheckable(True)

            isosurface = self.subject
            color = isosurface.getColor()
            visible = isosurface.isVisible()
            self.setData(color, qt.Qt.DecorationRole)
            self.setCheckState((visible and qt.Qt.Checked) or qt.Qt.Unchecked)

            nameItem = qt.QStandardItem("Level")
            sliderItem = IsoSurfaceLevelSlider(
                self.subject, self._isoLevelSliderNormalization
            )
            self.appendRow([nameItem, sliderItem])

            nameItem = qt.QStandardItem("Color")
            nameItem.setEditable(False)
            valueItem = IsoSurfaceColorItem(self.subject)
            self.appendRow([nameItem, valueItem])

            nameItem = qt.QStandardItem("Opacity")
            nameItem.setTextAlignment(qt.Qt.AlignLeft | qt.Qt.AlignTop)
            nameItem.setEditable(False)
            valueItem = IsoSurfaceAlphaItem(self.subject)
            self.appendRow([nameItem, valueItem])

            nameItem = qt.QStandardItem()
            nameItem.setEditable(False)
            valueItem = IsoSurfaceAlphaLegendItem(self.subject)
            valueItem.setEditable(False)
            self.appendRow([nameItem, valueItem])

        def queryRemove(self, view=None):
            buttons = qt.QMessageBox.Ok | qt.QMessageBox.Cancel
            ans = qt.QMessageBox.question(
                view,
                "Remove isosurface",
                "Remove the selected iso-surface?",
                buttons=buttons,
            )
            if ans == qt.QMessageBox.Ok:
                sfview = self.subject.parent()
                if sfview:
                    sfview.removeIsosurface(self.subject)
                    return False
            return False

        def leftClicked(self):
            checked = self.checkState() == qt.Qt.Checked
            visible = self.subject.isVisible()
            if checked != visible:
                self.subject.setVisible(checked)

    class IsoSurfaceLevelItem(SubjectItem):
        """
        Base class for the isosurface level items.
        """

        editable = True

        def getSignals(self):
            subject = self.subject
            return [subject.sigLevelChanged, subject.sigVisibilityChanged]

        def getEditor(self, parent, option, index):
            return FloatEdit(parent)

        def setEditorData(self, editor):
            editor.setValue(self._pullData())
            return False

        def _setModelData(self, editor):
            self._pushData(editor.value())
            return True

        def _pullData(self):
            return self.subject.getLevel()

        def _pushData(self, value, role=qt.Qt.UserRole):
            self.subject.setLevel(value)
            return self.subject.getLevel()

    class _IsoLevelSlider(qt.QSlider):
        """QSlider used for iso-surface level with linear scale"""

        def __init__(self, parent, subject, normalization):
            super(_IsoLevelSlider, self).__init__(parent=parent)
            self.subject = subject

            if normalization == "arcsinh":
                self.__norm = numpy.arcsinh
                self.__invNorm = numpy.sinh
            elif normalization == "linear":
                self.__norm = lambda x: x
                self.__invNorm = lambda x: x
            else:
                raise ValueError("Unsupported normalization %s", normalization)

            self.sliderReleased.connect(self.__sliderReleased)

            self.subject.sigLevelChanged.connect(self.setLevel)
            self.subject.parent().sigDataChanged.connect(self.__dataChanged)

        def setLevel(self, level):
            """Set slider from iso-surface level"""
            dataRange = self.subject.parent().getDataRange()

            if dataRange is not None:
                min_ = self.__norm(dataRange[0])
                max_ = self.__norm(dataRange[-1])

                width = max_ - min_
                if width > 0:
                    sliderWidth = self.maximum() - self.minimum()
                    sliderPosition = sliderWidth * (self.__norm(level) - min_) / width
                    self.setValue(int(sliderPosition))

        def __dataChanged(self):
            """Handles data update to refresh slider range if needed"""
            self.setLevel(self.subject.getLevel())

        def __sliderReleased(self):
            value = self.value()
            dataRange = self.subject.parent().getDataRange()
            if dataRange is not None:
                min_ = self.__norm(dataRange[0])
                max_ = self.__norm(dataRange[-1])
                width = max_ - min_
                sliderWidth = self.maximum() - self.minimum()
                level = min_ + width * value / sliderWidth
                self.subject.setLevel(self.__invNorm(level))

    class IsoSurfaceLevelSlider(IsoSurfaceLevelItem):
        """
        Isosurface level item with a slider editor.
        """

        nTicks = 1000
        persistent = True

        def __init__(self, subject, normalization):
            self.normalization = normalization
            super(IsoSurfaceLevelSlider, self).__init__(subject)

        def getEditor(self, parent, option, index):
            editor = _IsoLevelSlider(parent, self.subject, self.normalization)
            editor.setOrientation(qt.Qt.Horizontal)
            editor.setMinimum(0)
            editor.setMaximum(self.nTicks)

            editor.setSingleStep(1)

            editor.setLevel(self.subject.getLevel())
            return editor

        def setEditorData(self, editor):
            return True

        def _setModelData(self, editor):
            return True

    class IsoSurfaceColorItem(SubjectItem):
        """
        Isosurface color item.
        """

        editable = True
        persistent = True

        def getSignals(self):
            return self.subject.sigColorChanged

        def getEditor(self, parent, option, index):
            editor = QColorEditor(parent)
            color = self.subject.getColor()
            color.setAlpha(255)
            editor.color = color
            # Wrapping call in lambda is a workaround for PySide with Python 3
            editor.sigColorChanged.connect(lambda color: self.__editorChanged(color))
            return editor

        def __editorChanged(self, color):
            color.setAlpha(self.subject.getColor().alpha())
            self.subject.setColor(color)

        def _pushData(self, value, role=qt.Qt.UserRole):
            self.subject.setColor(value)
            return self.subject.getColor()

    class QColorEditor(qt.QWidget):
        """
        QColor editor.
        """

        sigColorChanged = qt.Signal(object)

        color = property(lambda self: qt.QColor(self.__color))

        @color.setter
        def color(self, color):
            self._setColor(color)
            self.__previousColor = color

        def __init__(self, *args, **kwargs):
            super(QColorEditor, self).__init__(*args, **kwargs)
            layout = qt.QHBoxLayout(self)
            layout.setContentsMargins(0, 0, 0, 0)
            button = qt.QToolButton()
            icon = qt.QIcon(qt.QPixmap(32, 32))
            button.setIcon(icon)
            layout.addWidget(button)
            button.clicked.connect(self.__showColorDialog)
            layout.addStretch(1)

            self.__color = None
            self.__previousColor = None

        def sizeHint(self):
            return qt.QSize(0, 0)

        def _setColor(self, qColor):
            button = self.findChild(qt.QToolButton)
            pixmap = qt.QPixmap(32, 32)
            pixmap.fill(qColor)
            button.setIcon(qt.QIcon(pixmap))
            self.__color = qColor

        def __showColorDialog(self):
            dialog = qt.QColorDialog(parent=self)
            if sys.platform == "darwin":
                # Use of native color dialog on macos might cause problems
                dialog.setOption(qt.QColorDialog.DontUseNativeDialog, True)

            self.__previousColor = self.__color
            dialog.setAttribute(qt.Qt.WA_DeleteOnClose)
            dialog.setModal(True)
            dialog.currentColorChanged.connect(self.__colorChanged)
            dialog.finished.connect(self.__dialogClosed)
            dialog.show()

        def __colorChanged(self, color):
            self.__color = color
            self._setColor(color)
            self.sigColorChanged.emit(color)

        def __dialogClosed(self, result):
            if result == qt.QDialog.Rejected:
                self.__colorChanged(self.__previousColor)
            self.__previousColor = None

    class IsoSurfaceAlphaItem(SubjectItem):
        """
        Isosurface alpha item.
        """

        editable = True
        persistent = True

        def _init(self):
            pass

        def getSignals(self):
            return self.subject.sigColorChanged

        def getEditor(self, parent, option, index):
            editor = qt.QSlider(parent)
            editor.setOrientation(qt.Qt.Horizontal)
            editor.setMinimum(0)
            editor.setMaximum(255)

            color = self.subject.getColor()
            editor.setValue(color.alpha())

            # Wrapping call in lambda is a workaround for PySide with Python 3
            editor.valueChanged.connect(lambda value: self.__editorChanged(value))

            return editor

        def __editorChanged(self, value):
            color = self.subject.getColor()
            color.setAlpha(value)
            self.subject.setColor(color)

        def setEditorData(self, editor):
            return True

        def _setModelData(self, editor):
            return True

    class IsoSurfaceAlphaLegendItem(SubjectItem):
        """Legend to place under opacity slider"""

        editable = False
        persistent = True

        def getEditor(self, parent, option, index):
            layout = qt.QHBoxLayout()
            layout.setContentsMargins(0, 0, 0, 0)
            layout.setSpacing(0)
            layout.addWidget(qt.QLabel("0"))
            layout.addStretch(1)
            layout.addWidget(qt.QLabel("1"))

            editor = qt.QWidget(parent)
            editor.setLayout(layout)
            return editor

    class IsoSurfaceCount(SubjectItem):
        """
        Item displaying the number of isosurfaces.
        """

        def getSignals(self):
            subject = self.subject
            return [subject.sigIsosurfaceAdded, subject.sigIsosurfaceRemoved]

        def _pullData(self):
            return len(self.subject.getIsosurfaces())

    class IsoSurfaceAddRemoveWidget(qt.QWidget):
        sigViewTask = qt.Signal(str)
        """Signal for the tree view to perform some task"""

        def __init__(self, parent, item):
            super(IsoSurfaceAddRemoveWidget, self).__init__(parent)
            self._item = item
            layout = qt.QHBoxLayout(self)
            layout.setContentsMargins(0, 0, 0, 0)
            layout.setSpacing(0)

            addBtn = qt.QToolButton(self)
            addBtn.setText("+")
            addBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly)
            layout.addWidget(addBtn)
            addBtn.clicked.connect(self.__addClicked)

            removeBtn = qt.QToolButton(self)
            removeBtn.setText("-")
            removeBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly)
            layout.addWidget(removeBtn)
            removeBtn.clicked.connect(self.__removeClicked)

            layout.addStretch(1)

        def __addClicked(self):
            sfview = self._item.subject
            if not sfview:
                return
            dataRange = sfview.getDataRange()
            if dataRange is None:
                dataRange = [0, 1]

            sfview.addIsosurface(numpy.mean((dataRange[0], dataRange[-1])), "#0000FF")

        def __removeClicked(self):
            self.sigViewTask.emit("remove_iso")

    class IsoSurfaceAddRemoveItem(SubjectItem):
        """
        Item displaying a simple QToolButton allowing to add an isosurface.
        """

        persistent = True

        def getEditor(self, parent, option, index):
            return IsoSurfaceAddRemoveWidget(parent, self)

    class IsoSurfaceGroup(SubjectItem):
        """
        Root item for the list of isosurface items.
        """

        def __init__(self, subject, normalization, *args):
            self._isoLevelSliderNormalization = normalization
            super(IsoSurfaceGroup, self).__init__(subject, *args)

        def getSignals(self):
            subject = self.subject
            return [subject.sigIsosurfaceAdded, subject.sigIsosurfaceRemoved]

        def _subjectChanged(self, signalIdx=None, args=None, kwargs=None):
            if signalIdx == 0:
                if len(args) >= 1:
                    isosurface = args[0]
                    if not isinstance(isosurface, Isosurface):
                        raise ValueError("Expected an isosurface instance.")
                    self.__addIsosurface(isosurface)
                else:
                    raise ValueError("Expected an isosurface instance.")
            elif signalIdx == 1:
                if len(args) >= 1:
                    isosurface = args[0]
                    if not isinstance(isosurface, Isosurface):
                        raise ValueError("Expected an isosurface instance.")
                    self.__removeIsosurface(isosurface)
                else:
                    raise ValueError("Expected an isosurface instance.")

        def __addIsosurface(self, isosurface):
            valueItem = IsoSurfaceRootItem(
                subject=isosurface, normalization=self._isoLevelSliderNormalization
            )
            nameItem = IsoSurfaceLevelItem(subject=isosurface)
            self.insertRow(max(0, self.rowCount() - 1), [valueItem, nameItem])

        def __removeIsosurface(self, isosurface):
            for row in range(self.rowCount()):
                child = self.child(row)
                subject = getattr(child, "subject", None)
                if subject == isosurface:
                    self.takeRow(row)
                    break

        def _init(self):
            nameItem = IsoSurfaceAddRemoveItem(self.subject)
            valueItem = qt.QStandardItem()
            valueItem.setEditable(False)
            self.appendRow([nameItem, valueItem])

            subject = self.subject
            isosurfaces = subject.getIsosurfaces()
            for isosurface in isosurfaces:
                self.__addIsosurface(isosurface)

    # Cutting Plane ###############################################################

    class ColormapBase(SubjectItem):
        """
        Mixin class for colormap items.
        """

        def getSignals(self):
            return [self.subject.getCutPlanes()[0].sigColormapChanged]

    class PlaneMinRangeItem(ColormapBase):
        """
        colormap minVal item.
        Editor is a QLineEdit with a QDoubleValidator
        """

        editable = True

        def _pullData(self):
            colormap = self.subject.getCutPlanes()[0].getColormap()
            auto = colormap.isAutoscale()
            if auto == self.isEnabled():
                self._enableRow(not auto)
            return colormap.getVMin()

        def _pushData(self, value, role=qt.Qt.UserRole):
            self._setVMin(value)

        def _setVMin(self, value):
            colormap = self.subject.getCutPlanes()[0].getColormap()
            vMin = value
            vMax = colormap.getVMax()

            if vMax is not None and value > vMax:
                vMin = vMax
                vMax = value
            colormap.setVRange(vMin, vMax)

        def getEditor(self, parent, option, index):
            return FloatEdit(parent)

        def setEditorData(self, editor):
            editor.setValue(self._pullData())
            return True

        def _setModelData(self, editor):
            value = editor.value()
            self._setVMin(value)
            return True

    class PlaneMaxRangeItem(ColormapBase):
        """
        colormap maxVal item.
        Editor is a QLineEdit with a QDoubleValidator
        """

        editable = True

        def _pullData(self):
            colormap = self.subject.getCutPlanes()[0].getColormap()
            auto = colormap.isAutoscale()
            if auto == self.isEnabled():
                self._enableRow(not auto)
            return self.subject.getCutPlanes()[0].getColormap().getVMax()

        def _setVMax(self, value):
            colormap = self.subject.getCutPlanes()[0].getColormap()
            vMin = colormap.getVMin()
            vMax = value
            if vMin is not None and value < vMin:
                vMax = vMin
                vMin = value
            colormap.setVRange(vMin, vMax)

        def getEditor(self, parent, option, index):
            return FloatEdit(parent)

        def setEditorData(self, editor):
            editor.setText(str(self._pullData()))
            return True

        def _setModelData(self, editor):
            value = editor.value()
            self._setVMax(value)
            return True

    class PlaneOrientationItem(SubjectItem):
        """
        Plane orientation item.
        Editor is a QComboBox.
        """

        editable = True

        _PLANE_ACTIONS = (
            (
                "3d-plane-normal-x",
                "Plane 0",
                "Set plane perpendicular to red axis",
                (1.0, 0.0, 0.0),
            ),
            (
                "3d-plane-normal-y",
                "Plane 1",
                "Set plane perpendicular to green axis",
                (0.0, 1.0, 0.0),
            ),
            (
                "3d-plane-normal-z",
                "Plane 2",
                "Set plane perpendicular to blue axis",
                (0.0, 0.0, 1.0),
            ),
        )

        def getSignals(self):
            return [self.subject.getCutPlanes()[0].sigPlaneChanged]

        def _pullData(self):
            currentNormal = self.subject.getCutPlanes()[0].getNormal(coordinates="scene")
            for _, text, _, normal in self._PLANE_ACTIONS:
                if numpy.allclose(normal, currentNormal):
                    return text
            return ""

        def getEditor(self, parent, option, index):
            editor = qt.QComboBox(parent)
            for iconName, text, tooltip, normal in self._PLANE_ACTIONS:
                editor.addItem(getQIcon(iconName), text)

            # Wrapping call in lambda is a workaround for PySide with Python 3
            editor.currentIndexChanged[int].connect(
                lambda index: self.__editorChanged(index)
            )
            return editor

        def __editorChanged(self, index):
            normal = self._PLANE_ACTIONS[index][3]
            plane = self.subject.getCutPlanes()[0]
            plane.setNormal(normal, coordinates="scene")
            plane.moveToCenter()

        def setEditorData(self, editor):
            currentText = self._pullData()
            index = 0
            for normIdx, (_, text, _, _) in enumerate(self._PLANE_ACTIONS):
                if text == currentText:
                    index = normIdx
                    break
            editor.setCurrentIndex(index)
            return True

        def _setModelData(self, editor):
            return True

    class PlaneInterpolationItem(SubjectItem):
        """Toggle cut plane interpolation method: nearest or linear.

        Item is checkable
        """

        def _init(self):
            interpolation = self.subject.getCutPlanes()[0].getInterpolation()
            self.setCheckable(True)
            self.setCheckState(
                qt.Qt.Checked if interpolation == "linear" else qt.Qt.Unchecked
            )
            self.setData(self._pullData(), role=qt.Qt.DisplayRole, pushData=False)

        def getSignals(self):
            return [self.subject.getCutPlanes()[0].sigInterpolationChanged]

        def leftClicked(self):
            checked = self.checkState() == qt.Qt.Checked
            self._setInterpolation("linear" if checked else "nearest")

        def _pullData(self):
            interpolation = self.subject.getCutPlanes()[0].getInterpolation()
            self._setInterpolation(interpolation)
            return interpolation[0].upper() + interpolation[1:]

        def _setInterpolation(self, interpolation):
            self.subject.getCutPlanes()[0].setInterpolation(interpolation)

    class PlaneDisplayBelowMinItem(SubjectItem):
        """Toggle whether to display or not values <= colormap min of the cut plane

        Item is checkable
        """

        def _init(self):
            display = self.subject.getCutPlanes()[0].getDisplayValuesBelowMin()
            self.setCheckable(True)
            self.setCheckState(qt.Qt.Checked if display else qt.Qt.Unchecked)
            self.setData(self._pullData(), role=qt.Qt.DisplayRole, pushData=False)

        def getSignals(self):
            return [self.subject.getCutPlanes()[0].sigTransparencyChanged]

        def leftClicked(self):
            checked = self.checkState() == qt.Qt.Checked
            self._setDisplayValuesBelowMin(checked)

        def _pullData(self):
            display = self.subject.getCutPlanes()[0].getDisplayValuesBelowMin()
            self._setDisplayValuesBelowMin(display)
            return "Displayed" if display else "Hidden"

        def _setDisplayValuesBelowMin(self, display):
            self.subject.getCutPlanes()[0].setDisplayValuesBelowMin(display)

    class PlaneColormapItem(ColormapBase):
        """
        colormap name item.
        Editor is a QComboBox
        """

        editable = True

        listValues = [
            "gray",
            "reversed gray",
            "temperature",
            "red",
            "green",
            "blue",
            "viridis",
            "magma",
            "inferno",
            "plasma",
        ]

        def getEditor(self, parent, option, index):
            editor = qt.QComboBox(parent)
            editor.addItems(self.listValues)

            # Wrapping call in lambda is a workaround for PySide with Python 3
            editor.currentIndexChanged[int].connect(
                lambda index: self.__editorChanged(index)
            )

            return editor

        def __editorChanged(self, index):
            colormapName = self.listValues[index]
            colormap = self.subject.getCutPlanes()[0].getColormap()
            colormap.setName(colormapName)

        def setEditorData(self, editor):
            colormapName = self.subject.getCutPlanes()[0].getColormap().getName()
            try:
                index = self.listValues.index(colormapName)
            except ValueError:
                _logger.error("Unsupported colormap: %s", colormapName)
            else:
                editor.setCurrentIndex(index)
            return True

        def _setModelData(self, editor):
            self.__editorChanged(editor.currentIndex())
            return True

        def _pullData(self):
            return self.subject.getCutPlanes()[0].getColormap().getName()

    class PlaneAutoScaleItem(ColormapBase):
        """
        colormap autoscale item.
        Item is checkable.
        """

        def _init(self):
            colorMap = self.subject.getCutPlanes()[0].getColormap()
            self.setCheckable(True)
            self.setCheckState(
                (colorMap.isAutoscale() and qt.Qt.Checked) or qt.Qt.Unchecked
            )
            self.setData(self._pullData(), role=qt.Qt.DisplayRole, pushData=False)

        def leftClicked(self):
            checked = self.checkState() == qt.Qt.Checked
            self._setAutoScale(checked)

        def _setAutoScale(self, auto):
            view3d = self.subject
            colormap = view3d.getCutPlanes()[0].getColormap()

            if auto != colormap.isAutoscale():
                if auto:
                    vMin = vMax = None
                else:
                    dataRange = view3d.getDataRange()
                    if dataRange is None:
                        vMin = vMax = None
                    else:
                        vMin, vMax = dataRange[0], dataRange[-1]
                colormap.setVRange(vMin, vMax)

        def _pullData(self):
            auto = self.subject.getCutPlanes()[0].getColormap().isAutoscale()
            self._setAutoScale(auto)
            if auto:
                data = "Auto"
            else:
                data = "User"
            return data

    class NormalizationNode(ColormapBase):
        """
        colormap normalization item.
        Item is a QComboBox.
        """

        editable = True
        listValues = list(Colormap.NORMALIZATIONS)

        def getEditor(self, parent, option, index):
            editor = qt.QComboBox(parent)
            editor.addItems(self.listValues)

            # Wrapping call in lambda is a workaround for PySide with Python 3
            editor.currentIndexChanged[int].connect(
                lambda index: self.__editorChanged(index)
            )

            return editor

        def __editorChanged(self, index):
            colorMap = self.subject.getCutPlanes()[0].getColormap()
            normalization = self.listValues[index]
            self.subject.getCutPlanes()[0].setColormap(
                name=colorMap.getName(),
                norm=normalization,
                vmin=colorMap.getVMin(),
                vmax=colorMap.getVMax(),
            )

        def setEditorData(self, editor):
            normalization = self.subject.getCutPlanes()[0].getColormap().getNormalization()
            index = self.listValues.index(normalization)
            editor.setCurrentIndex(index)
            return True

        def _setModelData(self, editor):
            self.__editorChanged(editor.currentIndex())
            return True

        def _pullData(self):
            return self.subject.getCutPlanes()[0].getColormap().getNormalization()

    class PlaneGroup(SubjectItem):
        """
        Root Item for the plane items.
        """

        def _init(self):
            valueItem = qt.QStandardItem()
            valueItem.setEditable(False)
            nameItem = PlaneVisibleItem(self.subject, "Visible")
            self.appendRow([nameItem, valueItem])

            nameItem = qt.QStandardItem("Colormap")
            nameItem.setEditable(False)
            valueItem = PlaneColormapItem(self.subject)
            self.appendRow([nameItem, valueItem])

            nameItem = qt.QStandardItem("Normalization")
            nameItem.setEditable(False)
            valueItem = NormalizationNode(self.subject)
            self.appendRow([nameItem, valueItem])

            nameItem = qt.QStandardItem("Orientation")
            nameItem.setEditable(False)
            valueItem = PlaneOrientationItem(self.subject)
            self.appendRow([nameItem, valueItem])

            nameItem = qt.QStandardItem("Interpolation")
            nameItem.setEditable(False)
            valueItem = PlaneInterpolationItem(self.subject)
            self.appendRow([nameItem, valueItem])

            nameItem = qt.QStandardItem("Autoscale")
            nameItem.setEditable(False)
            valueItem = PlaneAutoScaleItem(self.subject)
            self.appendRow([nameItem, valueItem])

            nameItem = qt.QStandardItem("Min")
            nameItem.setEditable(False)
            valueItem = PlaneMinRangeItem(self.subject)
            self.appendRow([nameItem, valueItem])

            nameItem = qt.QStandardItem("Max")
            nameItem.setEditable(False)
            valueItem = PlaneMaxRangeItem(self.subject)
            self.appendRow([nameItem, valueItem])

            nameItem = qt.QStandardItem("Values<=Min")
            nameItem.setEditable(False)
            valueItem = PlaneDisplayBelowMinItem(self.subject)
            self.appendRow([nameItem, valueItem])

    class PlaneVisibleItem(SubjectItem):
        """
        Plane visibility item.
        Item is checkable.
        """

        def _init(self):
            plane = self.subject.getCutPlanes()[0]
            self.setCheckable(True)
            self.setCheckState((plane.isVisible() and qt.Qt.Checked) or qt.Qt.Unchecked)

        def leftClicked(self):
            plane = self.subject.getCutPlanes()[0]
            checked = self.checkState() == qt.Qt.Checked
            if checked != plane.isVisible():
                plane.setVisible(checked)
                if plane.isVisible():
                    plane.moveToCenter()

    # Tree ########################################################################

    class ItemDelegate(qt.QStyledItemDelegate):
        """
        Delegate for the QTreeView filled with SubjectItems.
        """

        sigDelegateEvent = qt.Signal(str)

        def __init__(self, parent=None):
            super(ItemDelegate, self).__init__(parent)

        def createEditor(self, parent, option, index):
            item = index.model().itemFromIndex(index)
            if item:
                if isinstance(item, SubjectItem):
                    editor = item.getEditor(parent, option, index)
                    if editor:
                        editor.setAutoFillBackground(True)
                        if hasattr(editor, "sigViewTask"):
                            editor.sigViewTask.connect(self.__viewTask)
                        return editor

            editor = super(ItemDelegate, self).createEditor(parent, option, index)
            return editor

        def updateEditorGeometry(self, editor, option, index):
            editor.setGeometry(option.rect)

        def setEditorData(self, editor, index):
            item = index.model().itemFromIndex(index)
            if item:
                if isinstance(item, SubjectItem) and item.setEditorData(editor):
                    return
            super(ItemDelegate, self).setEditorData(editor, index)

        def setModelData(self, editor, model, index):
            item = index.model().itemFromIndex(index)
            if isinstance(item, SubjectItem) and item._setModelData(editor):
                return
            super(ItemDelegate, self).setModelData(editor, model, index)

        def __viewTask(self, task):
            self.sigDelegateEvent.emit(task)

    class TreeView(qt.QTreeView):
        """
        TreeView displaying the SubjectItems for the ScalarFieldView.
        """

        def __init__(self, parent=None):
            super(TreeView, self).__init__(parent)
            self.__openedIndex = None
            self._isoLevelSliderNormalization = "linear"

            self.setIconSize(qt.QSize(16, 16))

            header = self.header()
            header.setSectionResizeMode(qt.QHeaderView.ResizeToContents)

            delegate = ItemDelegate()
            self.setItemDelegate(delegate)
            delegate.sigDelegateEvent.connect(self.__delegateEvent)
            self.setSelectionBehavior(qt.QAbstractItemView.SelectRows)
            self.setSelectionMode(qt.QAbstractItemView.SingleSelection)

            self.clicked.connect(self.__clicked)

        def setSfView(self, sfView):
            """
            Sets the ScalarFieldView this view is controlling.

            :param sfView: A `ScalarFieldView`
            """
            model = qt.QStandardItemModel()
            model.setColumnCount(ModelColumns.ColumnMax)
            model.setHorizontalHeaderLabels(["Name", "Value"])

            item = qt.QStandardItem()
            item.setEditable(False)
            model.appendRow([ViewSettingsItem(sfView, "Style"), item])

            item = qt.QStandardItem()
            item.setEditable(False)
            model.appendRow([DataSetItem(sfView, "Data"), item])

            item = IsoSurfaceCount(sfView)
            item.setEditable(False)
            model.appendRow(
                [
                    IsoSurfaceGroup(
                        sfView, self._isoLevelSliderNormalization, "Isosurfaces"
                    ),
                    item,
                ]
            )

            item = qt.QStandardItem()
            item.setEditable(False)
            model.appendRow([PlaneGroup(sfView, "Cutting Plane"), item])

            self.setModel(model)

        def setModel(self, model):
            """
            Reimplementation of the QTreeView.setModel method. It connects the
            rowsRemoved signal and opens the persistent editors.

            :param qt.QStandardItemModel model: the model
            """

            prevModel = self.model()
            if prevModel:
                self.__openPersistentEditors(qt.QModelIndex(), False)
                try:
                    prevModel.rowsRemoved.disconnect(self.rowsRemoved)
                except (RuntimeError, TypeError, SystemError):
                    pass

            super(TreeView, self).setModel(model)
            model.rowsRemoved.connect(self.rowsRemoved)
            self.__openPersistentEditors(qt.QModelIndex())

        def __openPersistentEditors(self, parent=None, openEditor=True):
            """
            Opens or closes the items persistent editors.

            :param qt.QModelIndex parent: starting index, or None if the whole tree
                is to be considered.
            :param bool openEditor: True to open the editors, False to close them.
            """
            model = self.model()

            if not model:
                return

            if not parent or not parent.isValid():
                parent = self.model().invisibleRootItem().index()

            if openEditor:
                meth = self.openPersistentEditor
            else:
                meth = self.closePersistentEditor

            curParent = parent
            children = [
                model.index(row, 0, curParent) for row in range(model.rowCount(curParent))
            ]

            columnCount = model.columnCount()

            while len(children) > 0:
                curParent = children.pop(-1)

                children.extend(
                    [
                        model.index(row, 0, curParent)
                        for row in range(model.rowCount(curParent))
                    ]
                )

                for colIdx in range(columnCount):
                    sibling = model.sibling(curParent.row(), colIdx, curParent)
                    item = model.itemFromIndex(sibling)
                    if isinstance(item, SubjectItem) and item.persistent:
                        meth(sibling)

        def rowsAboutToBeRemoved(self, parent, start, end):
            """
            Reimplementation of the QTreeView.rowsAboutToBeRemoved. Closes all
            persistent editors under parent.

            :param qt.QModelIndex parent: Parent index
            :param int start: Start index from parent index (inclusive)
            :param int end: End index from parent index (inclusive)
            """
            self.__openPersistentEditors(parent, False)
            super(TreeView, self).rowsAboutToBeRemoved(parent, start, end)

        def rowsRemoved(self, parent, start, end):
            """
            Called when QTreeView.rowsRemoved is emitted. Opens all persistent
            editors under parent.

            :param qt.QModelIndex parent: Parent index
            :param int start: Start index from parent index (inclusive)
            :param int end: End index from parent index (inclusive)
            """
            super(TreeView, self).rowsRemoved(parent, start, end)
            self.__openPersistentEditors(parent, True)

        def rowsInserted(self, parent, start, end):
            """
            Reimplementation of the QTreeView.rowsInserted. Opens all persistent
            editors under parent.

            :param qt.QModelIndex parent: Parent index
            :param int start: Start index from parent index
            :param int end: End index from parent index
            """
            self.__openPersistentEditors(parent, False)
            super(TreeView, self).rowsInserted(parent, start, end)
            self.__openPersistentEditors(parent)

        def keyReleaseEvent(self, event):
            """
            Reimplementation of the QTreeView.keyReleaseEvent.
            At the moment only Key_Delete is handled. It calls the selected item's
            queryRemove method, and deleted the item if needed.

            :param qt.QKeyEvent event: A key event
            """

            # TODO : better filtering
            key = event.key()
            modifiers = event.modifiers()

            if key == qt.Qt.Key_Delete and modifiers == qt.Qt.NoModifier:
                self.__removeIsosurfaces()

            super(TreeView, self).keyReleaseEvent(event)

        def __removeIsosurfaces(self):
            model = self.model()
            selected = self.selectedIndexes()
            items = []
            # WARNING : the selection mode is set to single, so we re not
            # supposed to have more than one item here.
            # Multiple selection deletion has not been tested.
            # Watch out for index invalidation
            for index in selected:
                leftIndex = model.sibling(index.row(), 0, index)
                leftItem = model.itemFromIndex(leftIndex)
                if isinstance(leftItem, SubjectItem) and leftItem not in items:
                    items.append(leftItem)

            isos = [item for item in items if isinstance(item, IsoSurfaceRootItem)]
            if isos:
                for iso in isos:
                    if iso.queryRemove(self):
                        parentItem = iso.parent()
                        parentItem.removeRow(iso.row())
            else:
                qt.QMessageBox.information(
                    self, "Remove isosurface", "Select an iso-surface to remove it"
                )

        def __clicked(self, index):
            """
            Called when the QTreeView.clicked signal is emitted. Calls the item's
            leftClick method.

            :param qt.QIndex index: An index
            """
            item = self.model().itemFromIndex(index)
            if isinstance(item, SubjectItem):
                item.leftClicked()

        def __delegateEvent(self, task):
            if task == "remove_iso":
                self.__removeIsosurfaces()

        def setIsoLevelSliderNormalization(self, normalization):
            """Set the normalization for iso level slider

            This MUST be called *before* :meth:`setSfView` to have an effect.

            :param str normalization: Either 'linear' or 'arcsinh'
            """
            assert normalization in ("linear", "arcsinh")
            self._isoLevelSliderNormalization = normalization