DEBUGGING = ('-d' in sys.argv or '--debug' in sys.argv) class  BoolCti(AbstractCti):
    """ Config Tree Item to store an boolean. It can be edited using a check box
    """
    def __init__(self, nodeName, defaultData, expanded=True,
                 childrenDisabledValue=False):
        """ Constructor. For the parameters see the AbstractCti constructor documentation.

            If the data is equal to childrenDisabledValue, the node's children will be
            disabled.

            (The data will always be a bool so if childrenDisabledValue == None,
            the children will never be disabled.)
        """
        super(BoolCti, self).__init__(nodeName, defaultData, expanded=expanded)
        self.childrenDisabledValue = childrenDisabledValue


    @property
    def debugInfo(self):
        """ Returns a string with debugging information
        """
        return "{} (enabled={}, childDisabledVal={})".format(self.configValue, self.enabled,
                                                             self.childrenDisabledValue)


    def _enforceDataType(self, data):
        """ Converts to bool so that self.data always is of that type.
        """
        return bool(data)


    @property
    def data(self):
        """ Returns the data of this item.
        """
        return self._data


    @data.setter
    def data(self, data):
        """ Sets the data of this item.
            Does type conversion to ensure data is always of the correct type.
        """
        # Descendants should convert the data to the desired type here
        self._data = self._enforceDataType(data)

        #logger.debug("BoolCti.setData: {} for {}".format(data, self))
        enabled = self.enabled
        self.enableBranch(enabled and self.data != self.childrenDisabledValue)
        self.enabled = enabled


    @property
    def displayValue(self):
        """ Returns empty string since a checkbox will displayed in the value-column instead.
        """
        return ""


    def insertChild(self, childItem, position=None):
        """ Inserts a child item to the current item.

            Overridden from BaseTreeItem.
        """
        childItem = super(BoolCti, self).insertChild(childItem, position=None)

        enableChildren = self.enabled and self.data != self.childrenDisabledValue
        #logger.debug("BoolCti.insertChild: {} enableChildren={}".format(childItem, enableChildren))
        childItem.enableBranch(enableChildren)
        childItem.enabled = enableChildren
        return childItem


    @property
    def valueColumnItemFlags(self):
        """ Returns Qt.ItemIsUserCheckable so that a check box will be drawn in the config tree.
            Note that the flags include Qt.ItemIsEditable; this makes the reset button will appear.
        """
        return Qt.ItemIsUserCheckable | Qt.ItemIsEditable


    @property
    def checkState(self):
        """ Returns Qt.Checked or Qt.Unchecked.
        """
        if self.data is True:
            return Qt.Checked
        elif self.data is False:
            return Qt.Unchecked
        else:
            raise ValueError("Unexpected data: {!r}".format(self.data))


    @checkState.setter
    def checkState(self, checkState):
        """ Sets the data to given a Qt.CheckState (Qt.Checked or Qt.Unchecked).
        """
        if checkState == Qt.Checked:
            logger.debug("BoolCti.checkState setting to True")
            self.data = True
        elif checkState == Qt.Unchecked:
            logger.debug("BoolCti.checkState setting to False")
            self.data = False
        else:
            raise ValueError("Unexpected check state: {!r}".format(checkState))


    def enableBranch(self, enabled):
        """ Sets the enabled member to True or False for a node and all it's children
        """
        self.enabled = enabled

        # Disabled children and further descendants
        enabled = enabled and self.data != self.childrenDisabledValue

        for child in self.childItems:
            child.enableBranch(enabled)


    def createEditor(self, delegate, parent, _option):
        """ Creates a hidden widget so that only the reset button is visible during editing.
            :type option: QStyleOptionViewItem
        """
        return GroupCtiEditor(self, delegate, parent=parent) class BoolGroupCti(AbstractCti):
    """ Config Tree Item to store a nullable boolean (True, False or None).

        It can be edited using a tri-state check box. However, the user can not set the value
        to partially checked directly, it will only be partially checked if some of its children
        are checked and others are not! This is a bug/behavior of Qt that won't be fixed.
        See: https://bugreports.qt.io/browse/QTBUG-7674 and
             http://comments.gmane.org/gmane.comp.lib.qt.general/925
    """
    def __init__(self, nodeName, defaultData=None, expanded=True):
        """ Constructor. For the parameters see the AbstractCti constructor documentation.
        """
        super(BoolGroupCti, self).__init__(nodeName, defaultData, expanded=expanded)


    def _enforceDataType(self, data):
        """ Converts to bool so that self.data always is of that type.
        """
        return None if data is None else bool(data)


    @property
    def displayValue(self):
        """ Returns empty string since a checkbox will displayed in the value column instead.
        """
        return ""


    @property
    def valueColumnItemFlags(self):
        """ Returns Qt.ItemIsUserCheckable so that a check box will be drawn in the config tree.
            Note that the flags include Qt.ItemIsEditable; this makes the reset button will appear.
        """
        return Qt.ItemIsTristate | Qt.ItemIsUserCheckable | Qt.ItemIsEditable


    @property
    def checkState(self):
        """ Returns Qt.Checked or Qt.Unchecked if all children are checked or unchecked, else
            returns Qt.PartiallyChecked
        """
        #commonData = self.childItems[0].data if self.childItems else Qt.PartiallyChecked
        commonData = None

        for child in self.childItems:
            if isinstance(child, BoolCti):
                if commonData is not None and child.data != commonData:
                    return Qt.PartiallyChecked
                commonData = child.data

        if commonData is True:
            return Qt.Checked
        elif commonData is False:
            return Qt.Unchecked
        else:
            raise AssertionError("Please report this bug: commonData: {!r}".format(commonData))


    @checkState.setter
    def checkState(self, checkState):
        """ Sets the data to given a Qt.CheckState (Qt.Checked or Qt.Unchecked).
        """
        logger.debug("checkState setter: {}".format(checkState))
        if checkState == Qt.Checked:
            commonData = True
        elif checkState == Qt.Unchecked:
            commonData = False
        elif checkState == Qt.PartiallyChecked:
            commonData = None
            # This never occurs, see remarks above in the classes' docstring
            assert False, "This never happens. Please report if it does."
        else:
            raise ValueError("Unexpected check state: {!r}".format(checkState))

        for child in self.childItems:
            if isinstance(child, BoolCti):
                child.data = commonData


    def createEditor(self, delegate, parent, _option):
        """ Creates a hidden widget so that only the reset button is visible during editing.
            :type option: QStyleOptionViewItem
        """
        return GroupCtiEditor(self, delegate, parent=parent) class ChoiceCti(AbstractCti):
    """ Config Tree Item to store a choice between strings.

        A QCombobox will pop-up if the user clicks on the cell to edit it.
    """
    def __init__(self, nodeName, defaultData=0, configValues=None, displayValues=None,
                 editable=False, insertPolicy=QtWidgets.QComboBox.InsertAtBottom,
                 completer=NOT_SPECIFIED):
        """ Constructor.

            The defaultData is an integers that is used to store the currentIndex.
            The displayValues parameter must be a list of strings, which will be displayed in the
            combo box. The configValues should be a list of the same size with the configValues
            that each 'choice' represents, e.g. choice 'dashed' maps to configValue Qt.DashLine.
            If displayValues is None, the configValues are used as displayValues.

            The defaultData can be set to a negative value, e.g. use -1 to select the last item
            by default. However, it will be converted to a positive value in the constructor.

            :param defaultData: the default index in the combobox that is used for editing
            :param configValues: list of configValue items to populate the combobox with
            :param displayValues: list of possible displayValue items.
                If None the config values are used as display values as well.
            :param editable: if True the combobox will be editable
            :param insertPolicy: determines where user-inserted items should appear in the combobox
                must be of type QtWidgets.QComboBox.InsertPolicy
            :param completer: a QCompleter that will be used by the combobox for auto-completion.
                If NotSpecified a default completer is used, i.e. case-insensitive auto-completion.
                Use None, 0 or False to disable auto-completion.

            For the  parameters see the AbstractCti constructor documentation.

        """
        check_class(insertPolicy, QtWidgets.QComboBox.InsertPolicy)
        if completer: # completer may be False
            check_class(completer, QtWidgets.QCompleter)

        self.editable = editable
        self.insertPolicy = insertPolicy
        self.completer = completer

        self._configValues = [] if configValues is None else configValues
        if displayValues is None:
            self._displayValues = copy.copy(self._configValues)
        else:
            assert not editable, "No separate displayValues may be set if the CTI is editable"
            self._displayValues = displayValues

        assert len(self._configValues) == len(self._displayValues),\
            "If set, configValues must have the same length as displayValues."

        self._defaultConfigValues = copy.copy(self._configValues)

        # Set after self._displayValues are defined. The parent constructor call _enforceDataType
        super(ChoiceCti, self).__init__(nodeName, defaultData)


    def _enforceDataType(self, data):
        """ Converts to int so that this CTI always stores that type.

            The data be set to a negative value, e.g. use -1 to select the last item
            by default. However, it will be converted to a positive by this method.
        """
        idx = int(data)
        if idx < 0:
            idx += len(self._displayValues)
        assert 0 <= idx < len(self._displayValues), \
            "Index should be >= 0 and < {}. Got {}".format(len(self._displayValues), idx)
        return idx


    @property
    def configValue(self):
        """ The currently selected configValue
        """
        return self._configValues[self.data]


    @property
    def iterConfigValues(self):
        """ Returns a generator expression to iterate over al config values
        """
        return (value for value in self._configValues)


    @property
    def iterDisplayValues(self):
        """ Returns a generator expression to iterate over al display values
        """
        return (value for value in self._displayValues)


    def _dataToString(self, data):
        """ Conversion function used to convert the (default)data to the display value.
        """
        choices = self._displayValues
        return str(choices[data])


    def _nodeMarshall(self):
        """ Returns the non-recursive marshalled value of this CTI. Is called by marshall()
        """
        if self.editable:
            return [self.data, self._configValues]
        else:
            return self.data


    def _nodeUnmarshall(self, data):
        """ Initializes itself non-recursively from data. Is called by unmarshall()
        """
        if self.editable:
            curIdx, configValues = data
            self._configValues = configValues
            self._displayValues = configValues
            self.data = curIdx
        else:
            self.data = data


    @property
    def debugInfo(self):
        """ Returns the string with debugging information
        """
        return repr(self._displayValues)


    def createEditor(self, delegate, parent, option):
        """ Creates a ChoiceCtiEditor.
            For the parameters see the AbstractCti constructor documentation.
        """
        return ChoiceCtiEditor(self, delegate, parent=parent)


    def insertValue(self, pos, configValue, displayValue=None):
        """ Will insert the configValue in the configValues and the displayValue in the
            displayValues list.
            If displayValue is None, the configValue is set in the displayValues as well
        """
        self._configValues.insert(pos, configValue)
        self._displayValues.insert(pos, displayValue if displayValue is not None else configValue)


    def removeValue(self, configValue):
        """ Will remove the configValue and the corresponding displayValue from the lists.
        """
        self.removeValueByIndex(self._configValues.index(configValue))


    def removeValueByIndex(self, index):
        """ Will remove the configValue and the corresponding displayValue given the index.
        """
        del self._configValues[index]
        del self._displayValues[index] class MainGroupCti(GroupCti):
    """ Read only config Tree Item that only stores None.
        To be used as a high level group (e.g. the inspector group)
        Is the same as a groupCti but drawn as light text on a dark grey back ground
    """
    _backgroundBrush = QtGui.QBrush(QtGui.QColor("#606060")) # create only once
    _foregroundBrush = QtGui.QBrush(QtGui.QColor(Qt.white)) # create only once
    _font = QtGui.QFont()
    _font.setWeight(QtGui.QFont.Bold)

    def __init__(self, nodeName, defaultData=None):
        """ Constructor. For the parameters see the AbstractCti constructor documentation.
        """
        super(MainGroupCti, self).__init__(nodeName, defaultData, expanded=True) # always expand


    @property
    def font(self):
        """ Returns a font for displaying this item's text in the tree.
        """
        return self._font


    @property
    def backgroundBrush(self):
        """ Returns a (dark gray) brush for drawing the background role in the tree.
        """
        return self._backgroundBrush


    @property
    def foregroundBrush(self):
        """ Returns a (white) brush for drawing the foreground role in the tree.
        """
        return self._foregroundBrush


    def resetRangesToDefault(self):
        """ Resets range settings to the default data.

            The base implementation does nothing. Descendants should override
        """
        pass class AbstractInspector(QtWidgets.QStackedWidget):
    """ Abstract base class for inspectors.
        An inspector is a stacked widget; it has a contents page and and error page.
    """
    _fullName = "base" # see the fullName() class method for explanation
    ERROR_PAGE_IDX = 0
    CONTENTS_PAGE_IDX = 1

    sigShowMessage = QtSignal(str)
    sigUpdateFailed = QtSignal()  # Emitted when updateContents fails and displays the error tab

    def __init__(self, collector, parent=None):
        """ Constructor.

            When subclassing the AbstractInspector try to minimize the work done in the constructor.
            Place all functionality that may raise an exception in updateContents or clearContents,
            these functions will show an error page and prevent the application from aborting. If
            an exception occurs in the constructor it is not caught!

            :param collector: the data collector from where this inspector gets its data
            :param parent: parent widget.
        """

        super(AbstractInspector, self).__init__(parent)

        self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)

        self._config = MainGroupCti(nodeName='inspector') # Is typically redefined.
        self._collector = collector

        self.errorWidget = MessageDisplay()
        self.addWidget(self.errorWidget)

        self.contentsWidget = BasePanel()
        self.addWidget(self.contentsWidget)

        self.contentsLayout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.TopToBottom)
        self.contentsLayout.setSpacing(DOCK_SPACING)
        self.contentsLayout.setContentsMargins(DOCK_MARGIN, DOCK_MARGIN, DOCK_MARGIN, DOCK_MARGIN)
        self.contentsWidget.setLayout(self.contentsLayout)

        self.setCurrentIndex(self.CONTENTS_PAGE_IDX)


    def finalize(self):
        """ Is called before destruction. Can be used to clean-up resources
        """
        logger.debug("Finalizing: {}".format(self))


    @property
    def config(self):
        """ The root config tree item for this inspector.
        """
        return self._config


    @property
    def windowNumber(self):
        """ The instance number of the window this inspector belongs to.
        """
        return self.collector.windowNumber


    @classmethod
    def descriptionHtml(cls):
        """ A long description that will be displayed as help in the inspector-open dialog box.
        """
        return ""


    @classmethod
    def axesNames(cls):
        """ The names of the axes that this inspector visualizes.

            This determines the dimensionality of the inspector. For example an inspector that shows
            an image, and has axes names 'X' and 'Y', is 2-dimensional. Also the slicedArray that
            will be returned by the collector will then be 2-dimensional. This invariant also holds
            when the axesNames is an empty tuple. The slicedArray will then be zero-dimensional.
            For a 0-D example, see qtplugins.text.TextInspector.

            The axesNames should be row-first, so in this example: ['Y', 'X']

            The names should not include the string "Axis"; the Collector.fullAxisNames()
            returns that.
        """
        return tuple()


    @property
    def collector(self):
        """ The data collector from where this inspector gets its data
        """
        return self._collector


    def configValue(self, nodePath):
        """ Returns the config value data at the node path
        """
        return self.config.findByNodePath(nodePath).configValue


    def updateContents(self, reason=None, initiator=None):
        """ Tries to draw the widget contents with the updated RTI.
            Shows the error page in case an exception is raised while drawing the contents.
            Descendants should override _drawContents, not updateContents.

            During the call of _drawContents, the updating of the configuration tree is blocked to
            avoid circular effects. After that, a call to self.config.refreshFromTarget() is
            made to refresh the configuration tree with possible new values from the inspector
            (the inspector is the configuration's target, hence the name).

            The reason parameter is a string (one of the UpdateReason values) that indicates why
            the inspector contents was updated. This can, for instance, be used to optimize
            drawing the inspector contents. Note that the reason may be undefined (None).

            The initiator may contain the object that initiated the updated. The type depends on the
            reason. At the moment the initiator is only implemented for the "config changed" reason. In
            this case the initiator will be the Config Tree Item (CTI that has changed).
        """
        UpdateReason.checkValid(reason)
        logger.debug("---- Inspector updateContents, reason: {}, initiator: {}"
                     .format(reason, initiator))
        logger.debug("Inspector: {}".format(self))
        logger.debug("RTI: {}".format(self.collector.rti))
        try:
            self.setCurrentIndex(self.CONTENTS_PAGE_IDX)

            wasBlocked = self.config.model.setRefreshBlocked(True)
            try:
                self._drawContents(reason=reason, initiator=initiator)
                logger.debug("_drawContents finished successfully")

                # Update the config tree from the (possibly) new state of the PgLinePlot1d inspector,
                # e.g. the axis range may have changed while drawing.
                # self.config.updateTarget() # TODO: perhaps enable this here (instead of doing it in the inspector._drawContents when needed)?
            finally:
                self.config.model.setRefreshBlocked(wasBlocked)

            # Call refreshFromTarget in case the newly applied configuration resulted in a change
            # of the state of the configuration's target's (i.e. the inspector state)
            logger.debug("_drawContents finished successfully, calling refreshFromTarget...")
            self.config.refreshFromTarget()
            logger.debug("refreshFromTarget finished successfully")

        except InvalidDataError as ex:
            logger.info("Unable to draw the inspector contents: '{}'".format(ex))
            if str(ex):
                # Exception message can be empty in common cases. Don't clear message label then.
                self.sigShowMessage.emit(str(ex))

        except Exception as ex:####
            if DEBUGGING:
                raise

            logger.error("Error while drawing the inspector: {} ----".format(ex))
            logger.exception(ex)
            self._clearContents()
            self.setCurrentIndex(self.ERROR_PAGE_IDX)
            self._showError(msg=str(ex), title=type_name(ex))

            self.sigUpdateFailed.emit()  # so that test walk can give summary on errors.

        else:
            logger.debug("---- updateContents finished successfully")


    def _resetRequired(self, reason, _initiator=None):
        """ Uses reason parameter (and optionally the _initiator) to determine axis must be reset.
        """
        return (self.config.model.autoReset and
                (reason == UpdateReason.RTI_CHANGED or reason == UpdateReason.COLLECTOR_COMBO_BOX))


    def resetConfig(self):
        """ Resets config depending on the config reset mode.
        """
        resetMode = self.config.model.resetMode
        if resetMode == ResetMode.All:
            self.config.resetToDefault()
        elif resetMode == ResetMode.Ranges:
            self.config.resetRangesToDefault()
        else:
            raise ValueError("Unexpected config mode")



    def _drawContents(self, reason=None, initiator=None):
        """ Is called by updateContents to do the actual drawing.
            Descendants should override _drawContents and not worry about exceptions;
            the updateContents will show the error page if an exception is raised.

            See the updateContents() docstring for the reason and initiator parameters.
        """
        raise NotImplementedError()


    def _clearContents(self):
        """ Override to clear the widget contents.

            Is called to empty the inspector when an exception occurs during _drawContents,
            to prevent further errors.
        """
        raise NotImplementedError()


    def _showError(self, msg="", title="Error"):
        """ Shows an error message.
        """
        self.errorWidget.setError(msg=msg, title=title) class InvalidDataError(Exception):
    """ Exception that should be raised if the inspector cannot handle this type of data.
        Can be used to distuingish the situation from other exceptions an then, for example,
        draw an empty plot instead of the error message pane.
    """
    pass class UpdateReason(object):
    """ Enumeration class that contains the reason for updating the inspector contents.
    """
    NEW_MAIN_WINDOW     = "new main window"
    INSPECTOR_CHANGED   = "inspector changed"
    RTI_CHANGED         = "repo tree item changed"
    COLLECTOR_COMBO_BOX = "collector combobox changed"
    COLLECTOR_SPIN_BOX  = "collector spinbox changed"
    CONFIG_CHANGED      = "config changed"

    __VALID_REASONS = (NEW_MAIN_WINDOW, INSPECTOR_CHANGED, RTI_CHANGED,
                       COLLECTOR_COMBO_BOX, COLLECTOR_SPIN_BOX, CONFIG_CHANGED)


    @classmethod
    def validReasons(cls):
        """ Returns a tuple with all valid reasond
        """
        return cls.__VALID_REASONS


    @classmethod
    def checkValid(cls, reason):
        """ Raises ValueError if the reason is not one of the valid enumerations
        """
        if reason not in cls.__VALID_REASONS:
            raise ValueError("reason must be one of {}, got {}".format(cls.__VALID_REASONS, reason)) class ArgosColorLegendItem(ColorLegendItem):
    """ Wrapper around pgcolorbar.colorlegend.ColorLegendItem.

        Suppresses the FutureWarning of PyQtGraph in _updateHistogram.
        Overrides the _imageItemHasIntegerData method.
        Adds context menu with reset color scale action.
        Middle mouse click resets the axis with the settings in the config tree.
    """
    # Use a QueuedConnection to connect to sigResetColorScale so that the reset is scheduled after
    # all current events have been processed. Otherwise the mouseReleaseEvent may look for a
    # PlotCurveItem that is no longer present after the reset, which results in a RuntimeError:
    # wrapped C/C++ object of type PlotCurveItem has been deleted.
    sigResetColorScale = QtSignal()  # Signal the inspectors to reset the color scale


    def __init__(self, *args, histHeightPercentile=99.0, **kwargs):
        """ Constructor
        """
        super(ArgosColorLegendItem, self).__init__(
            *args, histHeightPercentile=histHeightPercentile, **kwargs)
        self.resetColorScaleAction = QtWidgets.QAction("Reset Color Range", self)
        self.resetColorScaleAction.triggered.connect(self.emitResetColorScaleSignal)
        self.resetColorScaleAction.setToolTip("Reset the range of the color scale.")
        self.addAction(self.resetColorScaleAction)


    @classmethod
    def _imageItemHasIntegerData(cls, imageItem):
        """ Returns True if the imageItem contains integer data.

            Overriden so that the ImagePlotItem can replace integer arrays with float arrays (to
            plot masked values as NaNs) while it still calculates the histogram bins as if it
            where integers (to prevent aliasing)
        """
        check_class(imageItem, pg.ImageItem, allow_none=True)

        if hasattr(imageItem, '_wasIntegerData'):
            return imageItem._wasIntegerData
        else:
            return super(ArgosColorLegendItem, cls)._imageItemHasIntegerData(imageItem)


    def emitResetColorScaleSignal(self):
        """ Emits the sigColorScaleReset to request the inspectors to reset the color scale
        """
        logger.debug("Emitting sigColorScaleReset() for {!r}".format(self))
        self.sigResetColorScale.emit()


    def mouseClickEvent(self, mouseClickEvent):
        """ Handles (PyQtGraph) mouse click events.

            Overrides the middle mouse click to reset using the settings in the config tree.

            Opens the context menu if a right mouse button was clicked. (We can't simply use
            setContextMenuPolicy(Qt.ActionsContextMenu because the PlotItem class does not derive
            from QWidget).

            :param mouseClickEvent: pyqtgraph.GraphicsScene.mouseEvents.MouseClickEvent
        """
        if mouseClickEvent.button() in self.resetRangeMouseButtons:
            self.emitResetColorScaleSignal()
            mouseClickEvent.accept()

        elif mouseClickEvent.button() == QtCore.Qt.RightButton:
            contextMenu = QtWidgets.QMenu()
            for action in self.actions():
                contextMenu.addAction(action)

            screenPos = mouseClickEvent.screenPos() # Screenpos is a QPointF, convert to QPoint.
            screenX = round(screenPos.x())
            screenY = round(screenPos.y())
            contextMenu.exec_(QtCore.QPoint(screenX, screenY))

        else:
            super(ArgosColorLegendItem, self).mouseClickEvent(mouseClickEvent) X_AXIS = pg.ViewBox.XAxis Y_AXIS = pg.ViewBox.YAxis BOTH_AXES = pg.ViewBox.XYAxes NO_LABEL_STR = '-- none --' def defaultAutoRangeMethods(inspector, intialItems=None):
    """ Creates an ordered dict with default autorange methods for an inspector.

        :param inspector: the range methods will work on (the sliced array) of this inspector.
        :param intialItems: will be passed on to the  OrderedDict constructor.
    """
    rangeFunctions = OrderedDict({} if intialItems is None else intialItems)
    rangeFunctions['use all data'] = partial(inspectorDataRange, inspector, 0.0)
    for percentage in [0.1, 0.2, 0.5, 1, 2, 5, 10, 20]:
        label = "discard {}%".format(percentage)
        rangeFunctions[label] = partial(inspectorDataRange, inspector, percentage)
    return rangeFunctions class PgAxisLabelCti(ChoiceCti):
    """ Configuration tree item that is linked to the axis label.
    """
    def __init__(self, plotItem, axisPosition, collector,
                 nodeName='label', defaultData=0, configValues=None):
        """ Constructor
            :param plotItem:
            :param axisPosition: 'left', 'right', 'bottom', 'top'
            :param collector: needed to get the collector.rtiInfo
            :param nodeName: the node name of this config tree item (default = label
            :param defaultData:
            :param configValues:
        """
        super(PgAxisLabelCti, self).__init__(nodeName, editable=True,
                                             defaultData=defaultData, configValues=configValues)
        assert axisPosition in VALID_AXIS_POSITIONS,\
            "axisPosition must be in {}".format(VALID_AXIS_POSITIONS)
        self.collector = collector
        self.plotItem = plotItem
        self.axisPosition = axisPosition


    def _updateTargetFromNode(self):
        """ Applies the configuration to the target axis it monitors.
            The axis label will be set to the configValue. If the configValue equals
             NO_LABEL_STR, the label will be hidden.
        """
        rtiInfo = self.collector.rtiInfo
        self.plotItem.setLabel(self.axisPosition, self.configValue.format(**rtiInfo))
        self.plotItem.showLabel(self.axisPosition, self.configValue != NO_LABEL_STR) class PgAxisCti(GroupCti):
    """ Configuration tree item for a PyQtGraph plot axis.

        Currently nothing more than a GroupCti.
    """
    pass class PgAxisFlipCti(BoolCti):
    """ BoolCti that flips an axis when True.
    """
    def __init__(self, viewBox, axisNumber, nodeName='flipped', defaultData=False
                 ):
        """ Constructor.
            The target axis is specified by viewBox and axisNumber (0 for x-axis, 1 for y-axis)
        """
        super(PgAxisFlipCti, self).__init__(nodeName, defaultData=defaultData)
        check_class(viewBox, pg.ViewBox)
        assert axisNumber in (X_AXIS, Y_AXIS), "axisNumber must be 0 or 1"
        self.viewBox = viewBox
        self.axisNumber = axisNumber


    def _updateTargetFromNode(self):
        """ Applies the configuration to its target axis
        """
        if self.axisNumber == X_AXIS:
            self.viewBox.invertX(self.configValue)
        else:
            self.viewBox.invertY(self.configValue) class PgAspectRatioCti(BoolCti):
    """ BoolCti for locking and specifying the aspect ratio (x/y)
    """
    def __init__(self, viewBox, nodeName="lock aspect ratio", defaultData=False, expanded=False):
        """ Constructor.
            The target axis is specified by viewBox and axisNumber (0 for x-axis, 1 for y-axis)
        """
        super(PgAspectRatioCti, self).__init__(nodeName, defaultData=defaultData, expanded=expanded)
        check_class(viewBox, pg.ViewBox)

        self.aspectRatioCti = self.insertChild(FloatCti("ratio", 1.0, minValue=0.0))
        self.viewBox = viewBox


    def _updateTargetFromNode(self):
        """ Applies the configuration to its target axis
        """
        self.viewBox.setAspectLocked(lock=self.configValue, ratio=self.aspectRatioCti.configValue) class PgAxisRangeCti(AbstractRangeCti):
    """ Configuration tree item is linked to the axis range.
    """
    PYQT_RANGE = 'by PyQtGraph'

    def __init__(self, viewBox, axisNumber, autoRangeFunctions=None,
                 nodeName='range', expanded=True):
        """ Constructor.
            The target axis is specified by viewBox and axisNumber (0 for x-axis, 1 for y-axis)

            If given, autoRangeFunctions must be a (label to function) dictionary that will be used
            to populate the (auto range) method ChoiceCti. If not give, the there will not be
            a method choice and the autorange implemented by PyQtGraph will be used.
        """
        if autoRangeFunctions is None:
            autoRangeFunctions = {self.PYQT_RANGE: partial(viewBoxAxisRange, viewBox, axisNumber)}

        super(PgAxisRangeCti, self).__init__(autoRangeFunctions=autoRangeFunctions,
                                             nodeName=nodeName, expanded=expanded)
        check_class(viewBox, pg.ViewBox)
        assert axisNumber in (X_AXIS, Y_AXIS), "axisNumber must be 0 or 1"

        self.viewBox = viewBox
        self.axisNumber = axisNumber

        # Autorange must be disabled as not to interfere with this class.
        # Note that autorange of ArgosPgPlotItem is set to False by default.
        axisAutoRange = self.viewBox.autoRangeEnabled()[axisNumber]
        assert axisAutoRange is False, \
            "Autorange is {!r} for axis {} of {}".format(axisAutoRange, axisNumber, self.nodePath)

        # Connect signals
        self.viewBox.sigRangeChangedManually.connect(self.setAutoRangeOff)
        self.viewBox.sigRangeChanged.connect(self.refreshMinMax)


    def _closeResources(self):
        """ Disconnects signals.
            Is called by self.finalize when the cti is deleted.
        """
        self.viewBox.sigRangeChangedManually.disconnect(self.setAutoRangeOff)
        self.viewBox.sigRangeChanged.disconnect(self.refreshMinMax)


    def getTargetRange(self):
        """ Gets the range of the target
        """
        return self.viewBox.state['viewRange'][self.axisNumber]


    def setTargetRange(self, targetRange, padding=None):
        """ Sets the range of the target.
        """
        # viewBox.setRange doesn't accept an axis number :-(
        if self.axisNumber == X_AXIS:
            xRange, yRange = targetRange, None
        else:
            xRange, yRange = None, targetRange

        # Do not set disableAutoRange to True in setRange; it triggers 'one last' auto range.
        # This is why the viewBox' autorange must be False at construction.
        self.viewBox.setRange(xRange = xRange, yRange=yRange, padding=padding,
                              update=False, disableAutoRange=False)

        # Sanity check (can only work if we calculate padding ourselves)
        #if self.viewBox.state['viewRange'][self.axisNumber] != targetRange:
        #    raise AssertionError("ViewRange update failed: {} != {}"
        #        .format(self.viewBox.state['viewRange'][self.axisNumber], targetRange)) class PgGridCti(BoolGroupCti):
    """ CTI for toggling the the grid on and off.

        Has child CTIs for toggling the X and Y axes separately. These are typically not added
        as children of PgAxisCti objects so that the user can enable both grids with one checkbox.
        Also includes a child to configure the transparency (alpha) of the grid.
    """
    def __init__(self, plotItem, nodeName="grid", defaultData=True, expanded=False):
        """ Constructor.
            The target axis is specified by viewBox and axisNumber (0 for x-axis, 1 for y-axis)
        """
        super(PgGridCti, self).__init__(nodeName, defaultData=defaultData, expanded=expanded)
        check_class(plotItem, pg.PlotItem)
        self.plotItem = plotItem

        self.xGridCti = self.insertChild(BoolCti('x-axis', defaultData))
        self.yGridCti = self.insertChild(BoolCti('y-axis', defaultData))
        self.alphaCti = self.insertChild(FloatCti('alpha', 0.20,
            minValue=0.0, maxValue=1.0, stepSize=0.01, decimals=2))


    def _updateTargetFromNode(self):
        """ Applies the configuration to the grid of the plot item.
        """
        self.plotItem.showGrid(x=self.xGridCti.configValue, y=self.yGridCti.configValue,
                               alpha=self.alphaCti.configValue)
        self.plotItem.updateGrid() class PgColorMapCti(AbstractCti):
    """ Lets the user select one of color maps of the color map library
    """
    SUB_SAMPLING_OFF = 1

    def __init__(self, colorLegendItem,
                 nodeName="color map", defaultData=DEFAULT_COLOR_MAP, expanded=False):
        """ Constructor.

            Stores a color map as data.

            :param defaultData: the default index in the combobox that is used for editing
        """
        check_class(colorLegendItem, ArgosColorLegendItem)
        self.colorLegendItem = colorLegendItem
        self.cmLibModel = CmLibModelSingleton.instance()

        # grey scale color map for when no color map is selected.
        lutRgba = np.outer(np.arange(256, dtype=np.uint8), np.array([1, 1, 1, 1], dtype=np.uint8))
        lutRgba[:,3] = 255
        self.greyScaleColorMap = ColorMap(CmMetaData("-- none --"), CatalogMetaData("Argos"))
        self.greyScaleColorMap.set_rgba_uint8_array(lutRgba)

        super(PgColorMapCti, self).__init__(nodeName, defaultData, expanded=expanded)

        self.reverseCti = self.insertChild(
            BoolCti("reversed", False))

        self.subSampleCti = self.insertChild(
            IntCti("subsample", self.SUB_SAMPLING_OFF, specialValueText="off",
                   minValue=self.SUB_SAMPLING_OFF, maxValue=64, stepSize=1)
        )


    def _enforceDataType(self, data):
        """ Converts to int so that this CTI always stores that type.
        """
        if data is None:
            return self.greyScaleColorMap
        elif isinstance(data, ColorMap):
            return data
        else:
            return self.cmLibModel.getColorMapByKey(data)


    @property
    def configValue(self):
        """ The currently selected configValue

            :rtype: ColorMap
        """
        return self.data


    def _updateTargetFromNode(self):
        """ Applies the configuration to its target axis.
            Sets the image item's lookup table to the LUT of the selected color map.
        """
        lut = self.data.rgb_uint8_array

        targetSize = self.subSampleCti.configValue
        if targetSize > self.SUB_SAMPLING_OFF:
            sourceSize, _ = lut.shape
            subIdx = np.round(np.linspace(0, sourceSize-1, targetSize)).astype(np.uint)
            lut = lut[subIdx, :]

        if self.reverseCti.configValue:
            lut = np.flipud(lut)

        self.colorLegendItem.setLut(lut)


    def _dataToString(self, data):
        """ Conversion function used to convert the (default)data to the display value.
        """
        return "" if data is None else data.meta_data.pretty_name


    @property
    def decoration(self):
        """ Returns a pixmap of the color map to show as icon
        """
        return makeColorBarPixmap(
            self.data,
            width=self.cmLibModel.iconBarWidth * 0.65,
            height=self.cmLibModel.iconBarHeight * 0.65,
            drawBorder=self.cmLibModel.drawIconBarBorder)


    def _nodeMarshall(self):
        """ Returns the non-recursive marshalled value of this CTI. Is called by marshall()
        """
        return self.data.key


    def _nodeUnmarshall(self, key):
        """ Initializes itself non-recursively from data. Is called by unmarshall()
        """
        self.data = self.cmLibModel.getColorMapByKey(key)


    def createEditor(self, delegate, parent, option):
        """ Creates a ChoiceCtiEditor.
            For the parameters see the AbstractCti constructor documentation.
        """
        return PgColorMapCtiEditor(self, delegate, parent=parent) class PgColorLegendCti(AbstractRangeCti):
    """ Configuration tree item is linked to the HistogramLUTItem range.
    """
    def __init__(self, legend, autoRangeFunctions=None, nodeName='color range', expanded=True):
        """ Constructor.
            The target axis is specified by viewBox and axisNumber (0 for x-axis, 1 for y-axis)

            If given, autoRangeFunctions must be a (label to function) dictionary that will be used
            to populate the (auto range) method ChoiceCti. If not give, the there will not be
            a method choice and the autorange implemented by PyQtGraph will be used.
        """
        super(PgColorLegendCti, self).__init__(
            autoRangeFunctions=autoRangeFunctions, nodeName=nodeName,
            expanded=True, paddingDefault=0)

        check_class(legend, ColorLegendItem)
        self.legend = legend

        self.paddingCti.defaultData = 0
        # Connect signals
        self.legend.sigLevelsChanged.connect(self.setAutoRangeOff)
        self.legend.sigLevelsChanged.connect(self.refreshMinMax)


    def _closeResources(self):
        """ Disconnects signals.
            Is called by self.finalize when the cti is deleted.
        """
        self.legend.sigLevelsChanged.disconnect(self.setAutoRangeOff)
        self.legend.sigLevelsChanged.disconnect(self.refreshMinMax)


    def getTargetRange(self):
        """ Gets the (color) range of the HistogramLUTItem
        """
        return self.legend.getLevels()


    def setTargetRange(self, targetRange, padding=None):
        """ Sets the (color) range of the HistogramLUTItem
        """
        self.legend.setLevels(targetRange, padding=padding) class PgColorLegendLabelCti(ChoiceCti):
    """ Configuration tree item that is linked to the axis label of a color legend .
    """
    def __init__(self, colorLegendItem, collector,
                 nodeName='label', defaultData=0, configValues=None):
        """ Constructor
            :param colorLegendItem PgColorLegendLabelCti:
            :param collector: needed to get the collector.rtiInfo
            :param nodeName: the node name of this config tree item (default = label
            :param defaultData:
            :param configValues:
        """
        super(PgColorLegendLabelCti, self).__init__(
            nodeName, editable=True, defaultData=defaultData, configValues=configValues)

        check_class(colorLegendItem, ColorLegendItem)
        self.colorLegendItem = colorLegendItem
        self.collector = collector


    def _updateTargetFromNode(self):
        """ Applies the configuration to the target axis it monitors.
            The axis label will be set to the configValue. If the configValue equals
            NO_LABEL_STR, the label will be hidden.
        """
        if self.configValue == NO_LABEL_STR:
            self.colorLegendItem.setLabel(None)
        else:
            rtiInfo = self.collector.rtiInfo
            self.colorLegendItem.setLabel(self.configValue.format(**rtiInfo)) class PgShowHistCti(BoolCti):
    """ BoolCti that shows/hides the histogram of a color bar.
    """
    def __init__(self, colorLegendItem, nodeName='show histogram', defaultData=True):
        """ Constructor.
            The target axis is specified by viewBox and axisNumber (0 for x-axis, 1 for y-axis)
        """
        super(PgShowHistCti, self).__init__(nodeName, defaultData=defaultData)

        check_class(colorLegendItem, ColorLegendItem)
        self.colorLegendItem = colorLegendItem


    def _updateTargetFromNode(self):
        """ Applies the configuration to its target axis
        """
        self.colorLegendItem.showHistogram(self.configValue) class PgShowDragLinesCti(BoolCti):
    """ BoolCti that shows/hides the drag lines a color bar.
    """
    def __init__(self, colorLegendItem, nodeName='show drag lines',
                 defaultData=True, expanded=False):
        """ Constructor.
            The target axis is specified by viewBox and axisNumber (0 for x-axis, 1 for y-axis)
        """
        super(PgShowDragLinesCti, self).__init__(nodeName,
                                                 defaultData=defaultData, expanded=expanded)

        self.marginCti = self.insertChild(
            IntCti("margins", 40, minValue=0, maxValue=250, stepSize=5))

        check_class(colorLegendItem, ColorLegendItem)
        self.colorLegendItem = colorLegendItem


    def _updateTargetFromNode(self):
        """ Applies the configuration to its target axis
        """
        try:
            self.colorLegendItem.showDragLines(self.configValue)
            self.colorLegendItem.setEdgeMargins(self.marginCti.configValue)
        except AttributeError as ex:
            logger.warning("Please update cmlib to latest version. No drag lines: {}".format(ex)) def setXYAxesAutoRangeOn(commonCti, xAxisRangeCti, yAxisRangeCti, axisNumber):
    """ Turns on the auto range of an X and Y axis simultaneously.
        It sets the autoRangeCti.data of the xAxisRangeCti and yAxisRangeCti to True.
        After that, it emits the sigItemChanged signal of the commonCti.

        Can be used with functools.partial to make a slot that atomically resets the X and Y axis.
        That is, only one sigItemChanged will be emitted.

        This function is necessary because, if one would call PgAxisRangeCti.sigItemChanged
        separately on the X and Y axes the sigItemChanged signal would be emitted twice. This in
        not only slower, but autoscaling one axis may slightly change the others range, so the
        second call to sigItemChanged may unset the autorange of the first.

        axisNumber must be one of: 0 (X-axis), 1 (Y-axis), 2, (Both X and Y axes).
    """
    assert axisNumber in VALID_AXIS_NUMBERS, \
        "Axis number should be one of {}, got {}".format(VALID_AXIS_NUMBERS, axisNumber)

    logger.debug("setXYAxesAutoRangeOn, axisNumber: {}".format(axisNumber))
    if axisNumber == X_AXIS or axisNumber == BOTH_AXES:
        xAxisRangeCti.autoRangeCti.data = True

    if axisNumber == Y_AXIS or axisNumber == BOTH_AXES:
        yAxisRangeCti.autoRangeCti.data = True

    commonCti.model.sigItemChanged.emit(commonCti) class PgPlotDataItemCti(GroupCti):
    """ Configuration tree item for a PyQtGraph plot data item.
        It allows for configuring a line style, color and symbols.

        Note that using a line width > 1 in comination with anti-aliassing may be slow.
        See https://github.com/pyqtgraph/pyqtgraph/issues/533
    """
    def __init__(self, nodeName="pen",  defaultData=None, expanded=True):
        """ Constructor.
        """
        super(PgPlotDataItemCti, self).__init__(nodeName, defaultData=defaultData, expanded=expanded)
        self.antiAliasCti = self.insertChild(BoolCti("anti-alias", True))

        self.colorCti = self.insertChild(ColorCti('color', QtGui.QColor('#FF0066')))
        self.lineCti = self.insertChild(BoolCti('line', True, expanded=False,
                                                childrenDisabledValue=False))
        self.lineStyleCti = self.lineCti.insertChild(createPenStyleCti('style'))
        self.lineWidthCti = self.lineCti.insertChild(createPenWidthCti('width'))

        defaultShadowPen = QtGui.QPen(QtGui.QColor('#BFBFBF'))
        defaultShadowPen.setWidth(0)
        self.lineCti.insertChild(PenCti("shadow", False, expanded=False,
                                        resetTo=QtGui.QPen(defaultShadowPen),
                                        includeNoneStyle=True, includeZeroWidth=True))

        self.symbolCti = self.insertChild(BoolCti("symbol", False, expanded=False,
                                             childrenDisabledValue=False))
        self.symbolShapeCti = self.symbolCti.insertChild(ChoiceCti("shape", 0,
           displayValues=['circle', 'square', 'triangle', 'diamond', 'plus'],
           configValues=['o', 's', 't', 'd', '+']))
        self.symbolSizeCti = self.symbolCti.insertChild(IntCti('size', 5, minValue=0,
                                                               maxValue=100, stepSize=1))

    @property
    def penColor(self):
        """ Returns the pen/color value"""
        return self.colorCti.configValue


    def createPlotDataItem(self):
        """ Creates a PyQtGraph PlotDataItem from the config values
        """
        antialias = self.antiAliasCti.configValue

        color = self.penColor
        if self.lineCti.configValue:
            pen = QtGui.QPen()
            pen.setCosmetic(True)
            pen.setColor(color)
            pen.setWidthF(self.lineWidthCti.configValue)
            pen.setStyle(self.lineStyleCti.configValue)
            shadowCti = self.lineCti.findByNodePath('shadow')
            shadowPen = shadowCti.createPen(altStyle=pen.style(), altWidth=2.0 * pen.widthF())
        else:
            pen = None
            shadowPen = None

        drawSymbols = self.symbolCti.configValue
        symbolShape = self.symbolShapeCti.configValue if drawSymbols else None
        symbolSize  = self.symbolSizeCti.configValue if drawSymbols else 0.0
        symbolPen = None # otherwise the symbols will also have dotted/solid line.
        symbolBrush = QtGui.QBrush(color) if drawSymbols else None

        plotDataItem = pg.PlotDataItem(antialias=antialias, pen=pen, shadowPen=shadowPen,
                                       symbol=symbolShape, symbolSize=symbolSize,
                                       symbolPen=symbolPen, symbolBrush=symbolBrush)
        return plotDataItem class ArgosPgPlotItem(PlotItem):
    """ Wrapper around pyqtgraph.graphicsItems.PlotItem

        Overrides the autoBtnClicked method.
        Middle mouse click resets the axis.

        The original PyQtGraph menu is disabled (all settings I want can be set using the config
        tree; the other settings I don't want to support). Furthermore a context menu is created
        that allows the user to rescale the axes.

        Autorange is disabled as it is expected that the (viewbox of the) plot item will be
        connected to two PgAxisRangeCti objects that control the (auto)range of the X and Y axes.

        Adds a black border of width 1.

        Sets the cursor to a cross if it's inside the viewbox.
    """

    # Use a QueuedConnection to connect to sigResetAxis so that the reset is scheduled after all
    # current events have been processed. Otherwise the mouseReleaseEvent may look for a
    # PlotCurveItem that is no longer present after the reset, which results in a RuntimeError:
    # wrapped C/C++ object of type PlotCurveItem has been deleted.
    sigResetAxis = QtSignal(int)

    def __init__(self,
                 borderPen=DEFAULT_BORDER_PEN,
                 *args, **kwargs):
        """ Constructor.

            :param borderPen: pen for drawing the viewBox border. Default black and width of 1.
        """
        super(ArgosPgPlotItem, self).__init__(*args, **kwargs)

        self.setMenuEnabled(False)
        #pg.setConfigOption('leftButtonPan', False)

        viewBox = self.getViewBox()
        viewBox.border = borderPen
        viewBox.setCursor(Qt.CrossCursor)
        viewBox.disableAutoRange(BOTH_AXES)
        viewBox.mouseClickEvent = lambda ev: self._axesMouseClickEvent(ev, BOTH_AXES)

        # Color of zoom-rectangle.
        alpha = 100
        greyVal = 160
        viewBox.rbScaleBox.setPen(pg.mkPen((0, 0, 0, alpha), width=2))
        viewBox.rbScaleBox.setBrush(pg.mkBrush(greyVal, greyVal, greyVal, alpha))

        # Add mouseClickEvent event handlers to the X and Y axis. This allows for resetting
        # the scale of each axes separately by middle mouse clicking the axis.
        for xAxisItem in (self.getAxis('bottom'), self.getAxis('top')):
            xAxisItem.mouseClickEvent = lambda ev: self._axesMouseClickEvent(ev, X_AXIS)
            xAxisItem.setCursor(Qt.SizeHorCursor)

        for yAxisItem in (self.getAxis('left'), self.getAxis('right')):
            yAxisItem.mouseClickEvent = lambda ev: self._axesMouseClickEvent(ev, Y_AXIS)
            yAxisItem.setCursor(Qt.SizeVerCursor)

        self.resetAxesAction = QtWidgets.QAction("Reset Axes", self,
                                 triggered = lambda: self.emitResetAxisSignal(BOTH_AXES),
                                 toolTip = "Resets the zoom factor of the X-axis and Y-axis")
        self.addAction(self.resetAxesAction)

        self.resetXAxisAction = QtWidgets.QAction("Reset X-axis", self,
                                 triggered = lambda: self.emitResetAxisSignal(X_AXIS),
                                 toolTip = "Resets the zoom factor of the X-axis")
        self.addAction(self.resetXAxisAction)

        self.resetYAxisAction = QtWidgets.QAction("Reset Y-axis", self,
                                 triggered = lambda: self.emitResetAxisSignal(Y_AXIS),
                                 toolTip = "Resets the zoom factor of the Y-axis")
        self.addAction(self.resetYAxisAction)



    def close(self):
        """ Is called before destruction. Can be used to clean-up resources
            Could be called 'finalize' but PlotItem already has a close so we reuse that.
        """
        logger.debug("Finalizing: {}".format(self))
        super(ArgosPgPlotItem, self).close()


    def _axesMouseClickEvent(self, mouseClickEvent, axisNumber):
        """ Handles the mouse clicks events when clicked on the axes or in the central viewbox

            :param mouseClickEvent: pyqtgraph.GraphicsScene.mouseEvents.MouseClickEvent
            :param axisNumber: the axis (X=0, Y=1, both=2)

            Emits sigResetAxis when the middle mouse button is clicked.
        """
        if mouseClickEvent.button() == QtCore.Qt.MiddleButton:
            mouseClickEvent.accept()
            self.emitResetAxisSignal(axisNumber)


    def mouseClickEvent(self, mouseClickEvent):
        """ Handles (PyQtGraph) mouse click events.

            Opens the context menu if a right mouse button was clicked. (We can't simply use
            setContextMenuPolicy(Qt.ActionsContextMenu because the PlotItem class does not derive
            from QWidget).

            :param mouseClickEvent: pyqtgraph.GraphicsScene.mouseEvents.MouseClickEvent
        """
        if mouseClickEvent.button() == QtCore.Qt.RightButton:
            contextMenu = QtWidgets.QMenu()
            for action in self.actions():
                contextMenu.addAction(action)

            screenPos = mouseClickEvent.screenPos() # Screenpos is a QPointF, convert to QPoint.
            screenX = round(screenPos.x())
            screenY = round(screenPos.y())
            contextMenu.exec_(QtCore.QPoint(screenX, screenY))


    def autoBtnClicked(self):
        """ Hides the button but does not enable/disable autorange.
            That will be done by PgAxisRangeCti
        """
        self.resetAxesAction.trigger()


    def emitResetAxisSignal(self, axisNumber):
        """ Emits the sigResetAxis with the axisNumber as parameter
            axisNumber should be 0 for X, 1 for Y, and 2 for both axes.
        """
        assert axisNumber in (VALID_AXES_NUMBERS), \
            "Axis Nr should be one of {}, got {}".format(VALID_AXES_NUMBERS, axisNumber)

        # Hide 'auto-scale (A)' button
        logger.debug("ArgosPgPlotItem.autoBtnClicked, mode:{}".format(self.autoBtn.mode))
        if self.autoBtn.mode == 'auto':
            self.autoBtn.hide()
        else:
            # Does this occur?
            msg = "Unexpected autobutton mode: {}".format(self.autoBtn.mode)
            if DEBUGGING:
                raise ValueError(msg)
            else:
                logger.warning(msg)

        logger.debug("Emitting sigResetAxis({}) for {!r}".format(axisNumber, self))
        self.sigResetAxis.emit(axisNumber)


    def setRectangleZoomOn(self, boolean):
        """ Turns on rectangle zoom mode.

            In rectangle zoom mode, the left mouse drag draws a rectangle, which defines the new
            view limits. In panning mode (the default), a left mouse drag will pan the scene.

            :param boolean: if True plotting is set to 'rect'
        """
        viewbox = self.getViewBox()
        if boolean:
            viewbox.setMouseMode(pg.ViewBox.RectMode)
        else:
            viewbox.setMouseMode(pg.ViewBox.PanMode) API_PYSIDE2 = 'pyside2'
API_PYQT5 = 'pyqt5'
ALL_API = [API_PYQT5, API_PYSIDE2]
QT_API_NAME = os.environ.get('QT_API')
    QT_API_NAME = None
        QT_API_NAME = API_PYQT5
        QT_API_NAME = API_PYSIDE2
        QT_API_NAME = API_PYQT5
        QT_API_NAME = API_PYSIDE2 def array_has_real_numbers(array):
    """ Uses the dtype kind of the numpy array to determine if it represents real numbers.

        That is, the array kind should be one of: i u f

        Possible dtype.kind values.

    """
    kind = array.dtype.kind
    return kind in 'iuf' def check_class(obj, target_class, allow_none = False):
    """ Checks that the  obj is a (sub)type of target_class.
        Raises a TypeError if this is not the case.

        :param obj: object whos type is to be checked
        :type obj: any type
        :param target_class: target type/class
        :type target_class: any class or type
        :param allow_none: if true obj may be None
        :type allow_none: boolean
    """
    if not isinstance(obj, target_class):
        if not (allow_none and obj is None):
            raise TypeError("obj must be a of type {}, got: {}"
                            .format(target_class, type(obj))) def is_an_array(var, allow_none=False):
    """ Returns True if var is a numpy array.
    """
    return isinstance(var, np.ndarray) or (var is None and allow_none) def to_string(var, masked=None, decode_bytes='utf-8', maskFormat='', strFormat='{}',
              intFormat='{}', numFormat=DEFAULT_NUM_FORMAT, noneFormat='{!r}', otherFormat='{}'):
    """ Converts var to a python string or unicode string so Qt widgets can display them.

        If var consists of bytes, the decode_bytes is used to decode the bytes.

        If var consists of a numpy.str_, the result will be converted to a regular Python string.
        This is necessary to display the string in Qt widgets.

        For the possible format string (replacement fields) see:
            https://docs.python.org/3/library/string.html#format-string-syntax

        :param masked: if True, the element is masked. The maskFormat is used.
        :param decode_bytes': string containing the expected encoding when var is of type bytes
        :param strFormat' : new style format string used to format strings
        :param intFormat' : new style format string used to format integers
        :param numFormat' : new style format string used to format all numbers except integers.
        :param noneFormat': new style format string used to format Nones.
        :param maskFormat': override with this format used if masked is True.
            If the maskFormat is empty, the format is never overriden.
        :param otherFormat': new style format string used to format all other types
    """
    #logger.debug("to_string: {!r} ({})".format(var, type(var)))

    # Decode and select correct format specifier.
    if is_binary(var):
        fmt = strFormat
        try:
            decodedVar = var.decode(decode_bytes, 'replace')
        except LookupError as ex:
            # Add URL to exception message.
            raise LookupError("{}\n\nFor a list of encodings in Python see: {}"
                              .format(ex, URL_PYTHON_ENCODINGS_DOC))
    elif is_text(var):
        fmt = strFormat
        decodedVar = six.text_type(var)
    elif is_a_string(var):
        fmt = strFormat
        decodedVar = str(var)
    elif isinstance(var, numbers.Integral):
        fmt = intFormat
        decodedVar = var
    elif isinstance(var, numbers.Number):
        fmt = numFormat
        decodedVar = var
    elif var is None:
        fmt = noneFormat
        decodedVar = var
    else:
        fmt = otherFormat
        decodedVar = var

    if maskFormat != '{}':
        try:
            allMasked = all(masked)
        except TypeError as ex:
            allMasked = bool(masked)

        if allMasked:
            fmt = maskFormat

    try:
        result = fmt.format(decodedVar)
    except Exception:
        result = "Invalid format {!r} for: {!r}".format(fmt, decodedVar)

    #if masked:
    #    logger.debug("to_string (fmt={}): {!r} ({}) -> result = {!r}".format(maskFormat, var, type(var), result))

    return result def array_kind_label(array):
    """ Returns short string describing the array data type kind
    """
    return KIND_LABEL[array.dtype.kind] RIGHT_ARROW = "\u2794" class ArrayWithMask(object):
    """ Class for storing an arrays together with a mask.

        Used instead of the Numpy MaskedArray class, which has too many issues.
    """
    def __init__(self, data, mask, fill_value):
        """ Constructor

            :param data:
            :param mask: array with mask or single boolean for the complete mask
            :param fill_value:
        """
        check_is_an_array(data)
        check_class(mask, (np.ndarray, bool, np.bool_))

        # Init fields
        self._data = None
        self._mask = None
        self._fill_value= None

        # Use setters
        self.data = data
        self.mask = mask
        self.fill_value= fill_value


    @property
    def data(self):
        """ The array values. Will be a numpy array."""
        return self._data


    @data.setter
    def data(self, values):
        """ The array values. Must be a numpy array."""
        check_is_an_array(values)
        self._data = values


    @property
    def mask(self):
        """ The mask values. Will be an array or a boolean scalar."""
        return self._mask


    @mask.setter
    def mask(self, mask):
        """ The mask values. Must be an array or a boolean scalar."""
        check_class(mask, (np.ndarray, bool, np.bool_))
        if isinstance(mask, (bool, np.bool_)):
            self._mask = bool(mask)
        else:
            self._mask = mask


    @property
    def fill_value(self):
        """ The fill_value."""
        return self._fill_value


    @fill_value.setter
    def fill_value(self, fill_value):
        """ The fill_value."""
        self._fill_value = fill_value


    def checkIsConsistent(self):
        """ Raises a ConsistencyError if the mask has an incorrect shape.
        """
        if is_an_array(self.mask) and self.mask.shape != self.data.shape:
            raise ConsistencyError("Shape mismatch mask={}, data={}"
                                   .format(self.mask.shape, self.data.shape))


    @classmethod
    def createFromMaskedArray(cls, masked_arr):
        """ Creates an ArrayWithMak

            :param masked_arr: a numpy MaskedArray or numpy array
            :return: ArrayWithMask
        """
        if isinstance(masked_arr, ArrayWithMask):
            return masked_arr

        check_class(masked_arr, (np.ndarray, ma.MaskedArray))

        # A MaskedConstant (i.e. masked) is a special case of MaskedArray. It does not seem to have
        # a fill_value so we use None to use the default.
        # https://docs.scipy.org/doc/numpy/reference/maskedarray.baseclass.html#numpy.ma.masked
        fill_value = getattr(masked_arr, 'fill_value', None)

        return cls(masked_arr.data, masked_arr.mask, fill_value)


    def asMaskedArray(self):
        """ Creates converts to a masked array
        """
        return ma.masked_array(data=self.data, mask=self.mask, fill_value=self.fill_value)


    def maskAt(self, index):
        """ Returns the mask at the index.

            It the mask is a boolean it is returned since this boolean representes the mask for
            all array elements.
        """
        if isinstance(self.mask, bool):
            return self.mask
        else:
            return self.mask[index]


    def maskIndex(self):
        """ Returns a boolean index with True if the value is masked.

            Always has the same shape as the maksedArray.data, event if the mask is a single boolan.
        """
        if isinstance(self.mask, bool):
            return np.full(self.data.shape, self.mask, dtype=np.bool)
        else:
            return self.mask


    @property
    def shape(self):
        """ Convenience method, returns the shape of the data.
        """
        return self.data.shape


    @property
    def dtype(self):
        """ Convenience method, returns the dtype of the data.
        """
        return self.data.dtype


    def __getitem__(self, index):
        """ Called when using the awm with an index (e.g. rti[0]).
        """
        newMask = self.mask if isinstance(self.mask, bool) else self.maskIndex()[index]
        return ArrayWithMask(self.data[index], newMask, fill_value=self.fill_value)


    def transpose(self, *args, **kwargs):
        """ Transposes the array and mask separately

            :param awm: ArrayWithMask
            :return: copy/view with transposed
        """
        tdata = np.transpose(self.data, *args, **kwargs)
        tmask = np.transpose(self.mask, *args, **kwargs) if is_an_array(self.mask) else self.mask
        return ArrayWithMask(tdata, tmask, self.fill_value)


    def replaceMaskedValue(self, replacementValue):
        """ Replaces values where the mask is True with the replacement value.
        """
        self.data = replaceMaskedValue(self.data, self.mask, replacementValue)


    def replaceMaskedValueWithNan(self):
        """ Replaces values where the mask is True with the replacement value.

            Will change the data type to float if the data is an integer.
            If the data is not a float (or int) the function does nothing.
        """
        replaceMaskedValueWithFloat(self.data, self.mask, np.nan, copyOnReplace=False) def replaceMaskedValueWithFloat(data, mask, replacementValue, copyOnReplace=True):
    """ Replaces values where the mask is True with the replacement value.

        Will change the data type to float if the data is an integer.
        If the data is not a float (or int) the function does nothing. Otherwise it will call
        replaceMaskedValue with the same parameters.

        :copyOnReplace makeCopy: If True (the default) it makes a copy if data is replaced.
    """
    kind = data.dtype.kind
    if kind == 'i' or kind == 'u': # signed/unsigned int
        data = data.astype(np.float, casting='safe')

    if data.dtype.kind != 'f':
        return # only replace for floats
    else:
        return replaceMaskedValue(data, mask, replacementValue, copyOnReplace=copyOnReplace) def nanPercentileOfSubsampledArrayWithMask(arrayWithMask, percentiles, subsample, *args, **kwargs):
    """ Sub samples the array and then calls maskedNanPercentile on this.

        If subsample is False, no sub sampling is done and it just calls maskedNanPercentile
    """
    check_class(subsample, bool)
    check_class(arrayWithMask, ArrayWithMask)

    maskedArray = arrayWithMask.asMaskedArray()

    if subsample:
        maskedArray = _subsampleArray(maskedArray)

    return _maskedNanPercentile(maskedArray, percentiles, *args, **kwargs)
import os
import logging, math
import numpy as np
import pyqtgraph as pg
from functools import partial
from collections import OrderedDict
from argos.info import DEBUGGING
from argos.config.boolcti import BoolCti, BoolGroupCti
from argos.config.choicecti import ChoiceCti
from argos.config.groupcti import MainGroupCti
from argos.inspector.abstract import AbstractInspector, InvalidDataError, UpdateReason
from argos.inspector.pgplugins.colorbar import ArgosColorLegendItem
from argos.inspector.pgplugins.pgctis import (
    X_AXIS, Y_AXIS, BOTH_AXES, NO_LABEL_STR, defaultAutoRangeMethods, PgAxisLabelCti,
    PgAxisCti, PgAxisFlipCti, PgAspectRatioCti, PgAxisRangeCti, PgGridCti,
    PgColorMapCti, PgColorLegendCti, PgColorLegendLabelCti, PgShowHistCti,
    PgShowDragLinesCti, setXYAxesAutoRangeOn, PgPlotDataItemCti)
from argos.inspector.pgplugins.pgplotitem import ArgosPgPlotItem
from argos.qt import Qt, QtCore, QtGui, QtSlot
from argos.utils.cls import array_has_real_numbers, check_class, is_an_array, to_string
from argos.utils.cls import array_kind_label
from argos.utils.defs import RIGHT_ARROW
from argos.utils.masks import (ArrayWithMask, replaceMaskedValueWithFloat,
                               nanPercentileOfSubsampledArrayWithMask)

logger = logging.getLogger(__name__)

ROW_TITLE,    COL_TITLE    = 0, 0  # colspan = 3
ROW_COLOR,    COL_COLOR    = 1, 2  # rowspan = 2
ROW_HOR_LINE, COL_HOR_LINE = 1, 0
ROW_IMAGE,    COL_IMAGE    = 2, 0
ROW_VER_LINE, COL_VER_LINE = 2, 1
ROW_PROBE,    COL_PROBE    = 3, 0  # colspan = 2


def calcPgImagePlot2dDataRange(pgImagePlot2d, percentage, crossPlot, subsample):
    """ Calculates the range from the inspectors' sliced array. Discards percentage of the minimum
        and percentage of the maximum values of the inspector.slicedArray

        :param pgImagePlot2d: the range methods will work on (the sliced array) of this inspector.
        :param float percentage: percentage that will be discarded.
        :param bool crossPlot: if None, the range will be calculated from the entire sliced array,
            if "horizontal" or "vertical" the range will be calculated from the data under the
            horizontal or vertical cross hairs.
            If the cursor is outside the image, there is no valid data under the cross-hair and
            the range will be determined from the sliced array as a fall back.
        :param bool subsample: if True, the image will be subsampled (to 200 by 200) before
            calculating the range. This to improve performance by large images.
    """
    check_class(pgImagePlot2d.slicedArray, ArrayWithMask) # sanity check

    if crossPlot is None:
        array = pgImagePlot2d.slicedArray  # the whole image

    elif crossPlot == 'horizontal':
        if pgImagePlot2d.crossPlotRow is not None:
            array = pgImagePlot2d.slicedArray[pgImagePlot2d.crossPlotRow, :]
        else:
            array = pgImagePlot2d.slicedArray # fall back on complete sliced array

    elif crossPlot == 'vertical':
        if pgImagePlot2d.crossPlotCol is not None:
            array = pgImagePlot2d.slicedArray[:, pgImagePlot2d.crossPlotCol]
        else:
            array = pgImagePlot2d.slicedArray # fall back on complete sliced array
    else:
        raise ValueError("crossPlot must be: None, 'horizontal' or 'vertical', got: {}"
                         .format(crossPlot))

    return nanPercentileOfSubsampledArrayWithMask(array, (percentage, 100 - percentage), subsample)


def crossPlotAutoRangeMethods(pgImagePlot2d, crossPlot, intialItems=None):
    """ Creates an ordered dict with autorange methods for an PgImagePlot2d inspector.

        :param pgImagePlot2d: the range methods will work on (the sliced array) of this inspector.
        :param crossPlot: if None, the range will be calculated from the entire sliced array,
            if "horizontal" or "vertical" the range will be calculated from the data under the
            horizontal or vertical cross hairs
        :param intialItems: will be passed on to the  OrderedDict constructor.
    """
    rangeFunctions = OrderedDict({} if intialItems is None else intialItems)

    # If crossPlot is "horizontal" or "vertical" make functions that determine the range from the
    # data at the cross hair.
    if crossPlot:
        rangeFunctions['cross all data'] = partial(calcPgImagePlot2dDataRange, pgImagePlot2d,
                                                   0.0, crossPlot)
        for percentage in [0.1, 0.2, 0.5, 1, 2, 5, 10, 20]:
            label = "cross discard {}%".format(percentage)
            rangeFunctions[label] = partial(calcPgImagePlot2dDataRange, pgImagePlot2d,
                                            percentage, crossPlot)

    # Always add functions that determine the data from the complete sliced array.
    for percentage in [0.1, 0.2, 0.5, 1, 2, 5, 10, 20]:
        rangeFunctions['image all data'] = partial(calcPgImagePlot2dDataRange, pgImagePlot2d,
                                                   0.0, None)

        label = "image discard {}%".format(percentage)
        rangeFunctions[label] = partial(calcPgImagePlot2dDataRange, pgImagePlot2d,
                                        percentage, None)
    return rangeFunctions



class PgImagePlot2dCti(MainGroupCti):
    """ Configuration tree item for a PgImagePlot2dCti inspector
    """
    def __init__(self, pgImagePlot2d, nodeName):
        """ Constructor

            Maintains a link to the target pgImagePlot2d inspector, so that changes in the
            configuration can be applied to the target by simply calling the apply method.
            Vice versa, it can connect signals to the target.
        """
        super(PgImagePlot2dCti, self).__init__(nodeName)
        check_class(pgImagePlot2d, PgImagePlot2d)
        self.pgImagePlot2d = pgImagePlot2d
        imagePlotItem = self.pgImagePlot2d.imagePlotItem
        viewBox = imagePlotItem.getViewBox()

        self.insertChild(
            ChoiceCti('title', 0, editable=True,
                      configValues=["{base-name} -- {name} {slices}",
                                    "{name} {slices}", "{path} {slices}"]))

        #### Axes ####

        self.aspectLockedCti = self.insertChild(PgAspectRatioCti(viewBox))

        self.xAxisCti = self.insertChild(PgAxisCti('x-axis'))
        self.xAxisCti.insertChild(
            PgAxisLabelCti(imagePlotItem, 'bottom', self.pgImagePlot2d.collector,
                           defaultData=1,
                           configValues=[NO_LABEL_STR, "{x-dim} [index]"]))
        self.xFlippedCti = self.xAxisCti.insertChild(PgAxisFlipCti(viewBox, X_AXIS))
        self.xAxisRangeCti = self.xAxisCti.insertChild(PgAxisRangeCti(viewBox, X_AXIS))

        self.yAxisCti = self.insertChild(PgAxisCti('y-axis'))
        self.yAxisCti.insertChild(
            PgAxisLabelCti(imagePlotItem, 'left', self.pgImagePlot2d.collector,
                           defaultData=1,
                           configValues=[NO_LABEL_STR, "{y-dim} [index]"]))
        self.yFlippedCti = self.yAxisCti.insertChild(

            PgAxisFlipCti(viewBox, Y_AXIS, defaultData=True))