class Collector(BasePanel):
    """ Widget for collecting the selected data.
        Consists of a table to collect the RtiItems

        The CollectorTree only stores the items, the intelligence is located in the Collector
        itself.
    """
    sigContentsChanged = QtSignal(str) # one of the UpdateReason values.
    sigShowMessage = QtSignal(str)

    def __init__(self, windowNumber):
        """ Constructor
        """
        super(Collector, self).__init__()

        self._windowNumber = windowNumber
        self._rti = None
        self._rtiInfo = None

        self._signalsBlocked = False
        self.COL_FIRST_COMBO = 1     # Column that contains the first (left most) combobox
        self.AXIS_POST_FIX = "-axis" # Added to the axis label to give the combobox labels.
        self._axisNames = []         # Axis names. Correspond to the independent variables
        self._fullAxisNames = []     # Will be set in clearAndSetComboBoxes
        self._comboBoxes = []        # Will be set in clearAndSetComboBoxes
        self._spinBoxes = []         # Will be set in createSpinBoxes
        self._spinSliders = []       # Will be set in createSpinBoxes

        self.layout = QtWidgets.QHBoxLayout(self)
        self.layout.setSpacing(DOCK_SPACING)
        self.layout.setContentsMargins(DOCK_MARGIN, DOCK_MARGIN, DOCK_MARGIN, DOCK_MARGIN)
        self.buttonLayout = QtWidgets.QVBoxLayout()
        # self.layout.setContentsMargins(2, 0, 2, 0)

        # Add tree
        self.tree = CollectorTree(self)
        self.layout.addWidget(self.tree)

        # Add buttons (not yet implemented)
        # self.addVisItemButton = QtWidgets.QPushButton("Add")
        # self.addVisItemButton.setEnabled(False) # not yet implemented
        # self.buttonLayout.addWidget(self.addVisItemButton, stretch=0)
        # self.removeVisItemButton = QtWidgets.QPushButton("Remove")
        # self.removeVisItemButton.setEnabled(False) # not yet implemented
        # self.buttonLayout.addWidget(self.removeVisItemButton, stretch=0)
        # self.buttonLayout.addStretch(stretch=1)
        # self.layout.addLayout(self.buttonLayout, stretch=0)

        self._updateRtiInfo()


    def sizeHint(self):
        """ The recommended size for the widget."""
        return QtCore.QSize(300, TOP_DOCK_HEIGHT)

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

    @property
    def rti(self):
        """ The current repository tree item. Can be None.
        """
        return self._rti


    @property
    def rtiIsSliceable(self):
        """ Returns true if the RTI is not None and is sliceable
        """
        return self._rti and self._rti.isSliceable


    @property
    def axisNames(self):
        """ Returns a the axis names.
            The axis names indicate the independent dimensions of the visualization.
        """
        return self._axisNames


    @property
    def fullAxisNames(self):
        """ Returns the of the combobox labels.
            The combobox labes the axisNames followed by '-axis'
        """
        return self._fullAxisNames


    @property
    def maxCombos(self):
        """ Returns the maximum number of combo boxes """
        return len(self._fullAxisNames)


    # @property
    # def numCombos(self):
    #     """ Returns the number of combo boxes """
    #     return len(self._comboBoxes)


    def blockChildrenSignals(self, block):
        """ If block equals True, the signals of the combo boxes and spin boxes are blocked
            Returns the old blocking state.
        """
        logger.debug("Blocking collector signals")
        for spinBox in self._spinBoxes:
            spinBox.blockSignals(block)
        for comboBox in self._comboBoxes:
            comboBox.blockSignals(block)
        result = self._signalsBlocked
        self._signalsBlocked = block
        return result


    def _setColumnCountForContents(self):
        """ Sets the column count given the current axes and selected RTI.
            Returns the newly set column count.
        """
        numRtiDims = self.rti.nDims if self.rti and self.rti.isSliceable else 0

        colCount = self.COL_FIRST_COMBO + max(numRtiDims, len(self.axisNames))
        self.tree.model().setColumnCount(colCount)
        return colCount


    def clear(self):
        """ Removes all widgets
        """
        model = self.tree.model()
        # Don't use model.clear(). it will delete the column sizes
        model.removeRows(0, 1)
        model.setRowCount(1)
        self._setColumnCountForContents()


    def clearAndSetComboBoxes(self, axesNames):
        """ Removes all comboboxes.
        """
        logger.debug("Collector clearAndSetComboBoxes: {}".format(axesNames))
        check_is_a_sequence(axesNames)
        row = 0
        self._deleteComboBoxes(row)
        self.clear()
        self._setAxesNames(axesNames)
        self._createComboBoxes(row)
        self._updateWidgets()


    def _setAxesNames(self, axisNames):
        """ Sets the axesnames, combobox lables and updates the headers. Removes old values first.
            The comboLables is the axes name + '-axis'
        """
        for col, _ in enumerate(self._fullAxisNames, self.COL_FIRST_COMBO):
            self._setHeaderLabel(col, '')

        self._axisNames = tuple(axisNames)
        self._fullAxisNames = tuple([axName + self.AXIS_POST_FIX for axName in axisNames])

        for col, label in enumerate(self._fullAxisNames, self.COL_FIRST_COMBO):
            self._setHeaderLabel(col, label)


    def _setHeaderLabel(self, col, text):
        """ Sets the header of column col to text.
            Will increase the number of columns if col is larger than the current number.
        """
        model = self.tree.model()
        item = model.horizontalHeaderItem(col)
        if item:
            item.setText(text)
        else:
            model.setHorizontalHeaderItem(col, QtGui.QStandardItem(text))


    @QtSlot(BaseRti)
    def setRti(self, rti):
        """ Updates the current VisItem from the contents of the repo tree item.

            Is a slot but the signal is usually connected to the Collector, which then calls
            this function directly.
        """
        check_class(rti, BaseRti)
        #assert rti.isSliceable, "RTI must be sliceable" # TODO: maybe later

        self._rti = rti
        self._updateWidgets()
        self._updateRtiInfo()


    def _updateWidgets(self):
        """ Updates the combo and spin boxes given the new rti or axes.
            Emits the sigContentsChanged signal.
        """
        row = 0
        model = self.tree.model()

        # Create path label
        nodePath = '' if self.rti is None else self.rti.nodePath
        pathItem = model.item(row, 0)
        if pathItem:
            # Reusing path item. If we remove all items the column size will be lost.
            pathItem.setText(nodePath)
        else:
            pathItem = QtGui.QStandardItem(nodePath)
            pathItem.setEditable(False)
            model.setItem(row, 0, pathItem)

        pathItem.setToolTip(nodePath)
        if self.rti is not None:
            pathItem.setIcon(self.rti.decoration)

        self._deleteSpinBoxes(row)
        self._populateComboBoxes(row)
        self._createSpinBoxes(row)
        self._updateRtiInfo()

        self.tree.resizeColumnsFromContents(startCol=self.COL_FIRST_COMBO)

        logger.debug("{} sigContentsChanged signal (_updateWidgets)"
                      .format("Blocked" if self.signalsBlocked() else "Emitting"))
        self.sigContentsChanged.emit(UpdateReason.RTI_CHANGED)


    def _createComboBoxes(self, row):
        """ Creates a combo box for each of the fullAxisNames
        """
        tree = self.tree
        model = self.tree.model()
        self._setColumnCountForContents()

        for col, _ in enumerate(self._axisNames, self.COL_FIRST_COMBO):
            logger.debug("Adding combobox at ({}, {})".format(row, col))
            comboBox = QtWidgets.QComboBox()
            comboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
            comboBox.setMinimumContentsLength(3) # So that collector table header always readable
            comboBox.activated.connect(self._comboBoxActivated)
            self._comboBoxes.append(comboBox)

            #editor = LabeledWidget(QtWidgets.QLabel(comboLabel), comboBox)
            tree.setIndexWidget(model.index(row, col), comboBox)


    def _deleteComboBoxes(self, row):
        """ Deletes all comboboxes of a row
        """
        tree = self.tree
        model = self.tree.model()

        for col in range(self.COL_FIRST_COMBO, self.maxCombos):
            logger.debug("Removing combobox at: ({}, {})".format(row, col))
            tree.setIndexWidget(model.index(row, col), None)

        self._comboBoxes = []


    @property
    def _hasFakeDimension(self):
        """ Returns True if the comboboxes have a fake dimension.

            We add a fake dimension (of length 1) only if there are fewer dimensions in the RTI
            than the inspector can show.
        """
        return self._rti.nDims < len(self._comboBoxes)


    # TODO: why again don't we create the comboxes when a new RTI is selected (just like the spins)?
    def _populateComboBoxes(self, row):
        """ Populates the combo boxes with values of the repo tree item
        """
        logger.debug("_populateComboBoxes")
        for comboBox in self._comboBoxes:
            comboBox.clear()

        if not self.rtiIsSliceable:
            # Add an empty item to the combo boxes so that resize to contents works.
            for comboBoxNr, comboBox in enumerate(self._comboBoxes):
                comboBox.addItem('', userData=None)
                comboBox.setEnabled(False)
            return

        nDims = self._rti.nDims
        nCombos = len(self._comboBoxes)

        for comboBoxNr, comboBox in enumerate(self._comboBoxes):

            # Only add a fake dimension of length 1 if there are fewer dimensions in the RTI than
            # the inspector can show.

            if self._hasFakeDimension:
                comboBox.addItem(FAKE_DIM_NAME, userData = FAKE_DIM_OFFSET + comboBoxNr)

            for dimNr in range(nDims):
                comboBox.addItem(self._rti.dimensionNames[dimNr], userData=dimNr)

            comboBox.setEnabled(True)
            #comboBox.adjustSize() # necessary?

        # Set combo-boxes current index
        if nDims >= nCombos:
            # If there are more dimensions in the RTI than the inspector can show, we use the
            # last couple of RTI dimensions. This because the NetCDF-CF conventions have the
            # preferred dimension order of T, Z, Y, X. On top of that, dimensions with length <= 1
            # are skipped if possible (see Github issue #9).

            curDim = nDims

            for comboBoxNr, comboBox in reversed(list(enumerate(self._comboBoxes))):
                while(True):
                    curDim -= 1
                    if curDim <= comboBoxNr or self.rti.arrayShape[curDim] > 1:
                        break

                curIdx = curDim
                assert 0 <= curIdx <= nDims, \
                    "curIdx should be <= {}, got {}".format(nDims, curIdx)

                comboBox.setCurrentIndex(curIdx)
        else:
            # If there are less dimensions in the RTI than the inspector can show, we fill
            # the combo-boxes starting at the leftmost and set the remaining comboboxes to the
            # fake dimension. This means that a table inspector will have one column and many
            # rows, which is the most convenient.

            for comboBoxNr, comboBox in enumerate(self._comboBoxes):
                curIdx = comboBoxNr + 1 if comboBoxNr < nDims else 0

                assert 0 <= curIdx <= nDims + 1, \
                    "curIdx should be <= {}, got {}".format(nDims + 1, curIdx)

                comboBox.setCurrentIndex(curIdx)



    # def getComboBoxDimensionName(self, comboBoxNr):
    #     """ Returns the currently selected dimension index of combo box nr comboBoxNr
    #     """
    #     self._comboBoxes[comboBoxNr].currentText()
    #
    #
    # def getComboBoxDimensionIndex(self, comboBoxNr):
    #     """ Returns the currently selected dimension index of combo box nr comboBoxNr
    #     """
    #     return self._comboBoxDimensionIndex(self._comboBoxes[comboBoxNr])


    def _comboBoxDimensionIndex(self, comboBox):
        """ Returns the dimension index (from the user data) of the current item of the combo box.
        """
        return comboBox.itemData(comboBox.currentIndex())


    def _dimensionSelectedInComboBox(self, dimNr):
        """ Returns True if the dimension is selected in one of the combo boxes.
        """
        for combobox in self._comboBoxes:
            if self._comboBoxDimensionIndex(combobox) == dimNr:
                return True
        return False


    def _createSpinBoxes(self, row):
        """ Creates a spinBox for each dimension that is not selected in a combo box.
        """
        assert len(self._spinBoxes) == 0, "Spinbox list not empty. Call _deleteSpinBoxes first"

        if not self.rtiIsSliceable:
            return

        logger.debug("_createSpinBoxes, array shape: {}".format(self._rti.arrayShape))

        self._setColumnCountForContents()

        tree = self.tree
        model = self.tree.model()
        col = self.COL_FIRST_COMBO + self.maxCombos

        for dimNr, dimSize in enumerate(self._rti.arrayShape):

            if self._dimensionSelectedInComboBox(dimNr):
                continue

            self._setHeaderLabel(col, '')

            spinBox = CollectorSpinBox()
            self._spinBoxes.append(spinBox)

            spinBox.setKeyboardTracking(False)
            spinBox.setCorrectionMode(QtWidgets.QAbstractSpinBox.CorrectToNearestValue)

            spinBox.setMinimum(0)
            spinBox.setMaximum(dimSize - 1)
            spinBox.setSingleStep(1)
            spinBox.setValue(dimSize // 2) # select the middle of the slice
            spinBox.setPrefix("{} = ".format(self._rti.dimensionNames[dimNr]))
            spinBox.setProperty("dim_nr", dimNr)
            #spinBox.adjustSize() # necessary?

            if not USE_SLIDER:
                spinBox.setSuffix("/{}".format(spinBox.maximum()))

            if dimSize == 1:
                spinBox.setEnabled(False)

            # This must be done after setValue to prevent emitting too many signals
            spinBox.valueChanged[int].connect(self._spinboxValueChanged)

            if USE_SLIDER:
                spinSlider = SpinSlider(spinBox, layoutContentsMargins = (0, 0, 5, 0))
                self._spinSliders.append(spinSlider)
                tree.setIndexWidget(model.index(row, col), spinSlider)
            else:
                tree.setIndexWidget(model.index(row, col), spinBox)

            col += 1

        # Resize the spinbox columns to their new contents
        self.tree.resizeColumnsFromContents(startCol=self.COL_FIRST_COMBO + self.maxCombos)


    def _deleteSpinBoxes(self, row):
        """ Removes all spinboxes
        """
        tree = self.tree
        model = self.tree.model()

        for col, spinBox in enumerate(self._spinBoxes, self.COL_FIRST_COMBO + self.maxCombos):
            spinBox.valueChanged[int].disconnect(self._spinboxValueChanged)
            tree.setIndexWidget(model.index(row, col), None)
        self._spinBoxes = []

        self._setColumnCountForContents()


    @QtSlot(int)
    def _comboBoxActivated(self, index, comboBox=None):
        """ Is called when a combo box value was changed by the user.

            Updates the spin boxes and sets other combo boxes having the same index to
            the fake dimension of length 1.
        """
        if comboBox is None:
            comboBox = self.sender()
        assert comboBox, "comboBox not defined and not the sender"

        blocked = self.blockChildrenSignals(True)

        curDimIdx = self._comboBoxDimensionIndex(comboBox)

        # if curDimIdx < FAKE_DIM_OFFSET:

        # Find any dimension that is not used
        unusedDimensions = list(range(self.rti.nDims + int(self._hasFakeDimension)))
        otherComboBoxes = [cb for cb in self._comboBoxes if cb is not comboBox]
        for otherComboBox in otherComboBoxes:
            if otherComboBox.currentIndex() in unusedDimensions:
                unusedDimensions.remove(otherComboBox.currentIndex())

        logger.info("Unused dimensions: {}".format(unusedDimensions))
        for otherComboBox in otherComboBoxes:
            if otherComboBox.currentIndex() == comboBox.currentIndex():
                otherComboBox.setCurrentIndex(unusedDimensions.pop())

        # Show only spin boxes that are not selected
        row = 0
        self._deleteSpinBoxes(row)
        self._createSpinBoxes(row)
        self._updateRtiInfo()

        self.blockChildrenSignals(blocked)

        logger.debug("{} sigContentsChanged signal (comboBox)"
                      .format("Blocked" if self.signalsBlocked() else "Emitting"))
        self.sigContentsChanged.emit(UpdateReason.COLLECTOR_COMBO_BOX)


    @QtSlot(int)
    def _spinboxValueChanged(self, index, spinBox=None):
        """ Is called when a spin box value was changed.
        """
        if spinBox is None:
            spinBox = self.sender()
        assert spinBox, "spinBox not defined and not the sender"

        self._updateRtiInfo()

        logger.debug("{} sigContentsChanged signal (spinBox)"
                      .format("Blocked" if self.signalsBlocked() else "Emitting"))
        self.sigContentsChanged.emit(UpdateReason.COLLECTOR_SPIN_BOX)


    def getSlicedArray(self, copy=True):
        """ Slice the rti using a tuple of slices made from the values of the combo and spin boxes.

            :param copy: If True (the default), a copy is made so that inspectors cannot
                accidentally modify the underlying of the RTIs. You can set copy=False as a
                potential optimization, but only if you are absolutely sure that you don't modify
                the the slicedArray in your inspector! Note that this function calls transpose,
                which can still make a copy of the array for certain permutations.

            :return: ArrayWithMask array with the same number of dimension as the number of
                comboboxes (this can be zero!).

                Returns None if no slice can be made (i.e. the RTI is not sliceable).
            :rtype ArrayWithMask:
        """
        #logger.debug("getSlicedArray() called")

        if not self._rti:
            self.sigShowMessage.emit("No item selected.")
            return None

        if not self.rtiIsSliceable:
            # This is very common so we don't show a message so the user isn't flooded.
            # Also this can have many different causes (compound data, lists, etc_ so it's
            # difficult to show a good, descriptive message.
            return None

        if np.prod(self._rti.arrayShape) == 0:
            self.sigShowMessage.emit("Selected item has zero array elements.")
            return None

        # The dimensions that are selected in the combo boxes will be set to slice(None),
        # the values from the spin boxes will be set as a single integer value
        nDims = self.rti.nDims
        sliceList = [slice(None)] * nDims

        for spinBox in self._spinBoxes:
            dimNr = spinBox.property("dim_nr")
            sliceList[dimNr] = spinBox.value()

        # Make the array slicer. It needs to be a tuple, a list of only integers will be
        # interpreted as an index. With a tuple, array[(exp1, exp2, ..., expN)] is equivalent to
        # array[exp1, exp2, ..., expN].
        # See: http://docs.scipy.org/doc/numpy/reference/arrays.indexing.html
        slicedArray = self.rti[tuple(sliceList)]

        # Make a copy to prevent inspectors from modifying the underlying array.
        if copy:
            if versionStrToTuple(np.__version__) >= (1,19,0):
                slicedArray = np.copy(slicedArray, subok=True)  # Fixes issue #8
            else:
                slicedArray = ma.copy(slicedArray)

        # If there are no comboboxes the sliceList will contain no Slices objects, only ints. Then
        # the resulting slicedArray will be a usually a scalar (only structured fields may yield an
        # array). We convert this scalar to a zero-dimensional Numpy array so that inspectors
        # always get an array (having the same number of dimensions as the dimensionality of the
        # inspector, i.e. the number of comboboxes).
        # Also scalar RTIs, which have nDim == 0, can return a scalar which must be converted.
        # TODO: perhaps always convert to array.
        if self.maxCombos == 0 or self.rti.nDims == 0:
            slicedArray = ma.MaskedArray(slicedArray)

        # Post-condition type check
        check_is_an_array(slicedArray, np.ndarray)

        # Enforce the return type to be a masked array.
        if not isinstance(slicedArray, ma.MaskedArray):
            slicedArray = ma.MaskedArray(slicedArray)

        # Add fake dimensions of length 1 so that result.ndim will equal the number of combo boxes
        # TODO: Perhaps get rid of this because it fails with masked arrays with fill values.
        # The less we do here, the less chance an error occurs. See development/todo.txt
        # 2022-02-21: this seems no longer a problem (numpy 1.22.2)
        for dimNr in range(slicedArray.ndim, self.maxCombos):
            #logger.debug("Adding fake dimension: {}".format(dimNr))
            slicedArray = ma.expand_dims(slicedArray, dimNr)

        # Post-condition dimension check
        assert slicedArray.ndim == self.maxCombos, \
            "Bug: getSlicedArray should return a {:d}D array, got: {}D" \
            .format(self.maxCombos, slicedArray.ndim)

        # Convert to ArrayWithMask class for working around issues with the numpy maskedarray
        awm = ArrayWithMask.createFromMaskedArray(slicedArray)
        del slicedArray

        # Shuffle the dimensions to be in the order as specified by the combo boxes
        comboDims = [self._comboBoxDimensionIndex(cb) for cb in self._comboBoxes]
        permutations = np.argsort(comboDims)
        logger.debug("slicedArray.shape: {}".format(awm.data.shape))
        logger.debug("Transposing dimensions: {}".format(permutations))
        awm = awm.transpose(permutations)

        awm.checkIsConsistent()

        return awm


    def getSlicesString(self):
        """ Returns a string representation of the slices that are used to get the sliced array.
            For example returns '[:, 5]' if the combo box selects dimension 0 and the spin box 5.
        """
        if not self.rtiIsSliceable:
            return ''

        # The dimensions that are selected in the combo boxes will be set to slice(None),
        # the values from the spin boxes will be set as a single integer value
        nDims = self.rti.nDims
        sliceList = [':'] * nDims

        for spinBox in self._spinBoxes:
            dimNr = spinBox.property("dim_nr")
            sliceList[dimNr] = str(spinBox.value())

        # No need to shuffle combobox dimensions like in getSlicedArray; all combobox dimensions
        # yield a colon.

        return "[" + ", ".join(sliceList) + "]"


    # def independentDimensionNames(self):
    #     """ Returns list of the names of the independent dimensions, which have been selected in
    #         the combo boxes.
    #     """
    #     return [combobox.currentText() for combobox in self._comboBoxes]
    #
    #
    # def independentDimensionUnits(self):
    #     """ Returns list of the units of the independent dimensions, which have been selected in
    #         the combo boxes. At the moment return '' for each independent dimension.
    #     """
    #     return ['' for _combobox in self._comboBoxes]
    #
    #
    # def dependentDimensionName(self):
    #     """ Returns the name of the dependent dimension.
    #         A good default is the name of the RTI.
    #     """
    #     return self.rti.nodeName if self.rti else ''


    @property
    def rtiInfo(self):
        """ Returns a dictionary with information on the selected RTI (repo tree item).
            This can be used in string formatting of config options. For instance: the plot title
            can be specified as: '{path} {slices}', which will be expanded with the actual nodePath
            and slices-string of the RTI.

            If no RTI is selected the applicable values will be empty strings.

            The dictionary has the following contents:
                slices : a string representation of the selected slice indices.
                name        : nodeName of the RTI
                path        : nodePath of the RTI
                unit        : unit of the RTI in parentheses
                raw-unit    : unit of the RTI without parentheses (empty string if no unit given)
                n-dim       : dimension selected in the combobox for axis n. The axis name will be
                              in lower case (so e.g. x-dim, y-dim, etc)
        """
        return self._rtiInfo


    def _updateRtiInfo(self):
        """ Updates the _rtiInfo property when a new RTI is set or spin/comboboxes change value .
        """
        logger.debug("Updating self._rtiInfo")
        # Info about the dependent dimension
        rti = self.rti
        if rti is None:
            info = {'slices': '',
                    'name': '',
                    'path': '',
                    'file-name': '',
                    'dir-name': '',
                    'base-name': '',
                    'unit': '',
                    'raw-unit': ''}
        else:
            dirName, baseName = os.path.split(rti.fileName)
            info = {'slices': self.getSlicesString(),
                    'name': rti.nodeName,
                    'path': rti.nodePath,
                    'file-name': rti.fileName,
                    'dir-name': dirName,
                    'base-name': baseName,
                    'unit': '({})'.format(rti.unit) if rti.unit else '',
                    'raw-unit': rti.unit}

        # Add the info of the independent dimensions (appended with the axis name of that dim).
        for axisName, comboBox in zip(self._axisNames, self._comboBoxes):
            dimName = comboBox.currentText()
            key = '{}-dim'.format(axisName.lower())
            info[key] = dimName

        self._rtiInfo = info class AbstractCti(BaseTreeItem):
    """ TreeItem for use in a ConfigTreeModel. (CTI = Config Tree Item)

        Abstract class. You must implement the _enforceDataType method that ensures the the data
        is stored internally in the correct type. Also, implement the createEditor method if you
        want you CTI to be editable (which is usually the case). If your CTI is not editable, make
        sure that valueColumnItemFlags does not return Qt.ItemIsEditable to prevent createEditor
        from being called by the delegate.

        Just like the BaseTreeItem every node has a name and a full path that is a slash-separated
        string of the full path leading from the root node to the current node. For instance, the
        path may be '/scale/x', the nodeName in that case is 'x'.

        CTIs are used to store a certain configuration value. It can be queried by the configValue
        property. The type of this value differs between descendants of AbstractCti, but a sub class
        should always return the same type. For example, the ColorCti.value always returns a
        QColor object. The displayValue returns the string representation for use in the tables;
        by default this returns: str(self.value)

        The underlying data is usually stored in that type as well but this is not necessarily so.
        A ChoiceCti, which represents a combo box, stores a list of choices and an index that is
        the actual choice made by the user. ChoiceCti.data contains the index while
        ChoiceCti.configValue returns: choices[index]. Note that the constructor expects the data
        as input parameter. The constructor calls the _enforceDataType method to convert the data
        to the correct type.

        The purpose of the defaultData is to reset a config data to its initial value when the
        user clicks on the reset button during editing.
    """
    def __init__(self, nodeName, defaultData, enabled=True, expanded=True):
        """ Constructor
            :param nodeName: name of this node (used to construct the node path).
            :param defaultData: default data to which the data can be reset by the reset button.
            :param enabled: True if the item is enabled
            :param expanded: True if the item is expanded
        """
        super(AbstractCti, self).__init__(nodeName=nodeName)

        self._defaultData = self._enforceDataType(defaultData)
        self._data = self.defaultData
        self._enabled = enabled
        self._expanded = expanded
        self._blockRefresh = False


    def finalize(self):
        """ Can be used to cleanup resources. Should be called explicitly.
            Recursively calls the _closeRecources method on all children and then on itself.
            Descendants should override _closeRecources to do the actual closing of resources.
        """
        for child in self.childItems:
            child.finalize()

        # In contrast to the BaseRti there is no close() method, _closeResources is directly called
        # because it only is needed when the config is deleted and catching exceptions makes no
        # sense.
        self._closeResources()


    def _closeResources(self):
        """ Can be overridden to close the underlying resources or disconnect signals.
            The default implementation does nothing.
            Is called by self.finalize when the cti is deleted. There is no corresponding
            _openResources; all resources are claimed in the constructor.
        """
        pass


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


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

    @property
    def displayValue(self):
        """ Returns the string representation of data for use in the tree view.
            If a descendant overrides this, it should probably also override displayDefaultValue.
        """
        return self._dataToString(self.data)

    @property
    def displayDefaultValue(self):
        """ Returns the string representation of default data for use in the tree view.
        """
        return self._dataToString(self.defaultData)

    @property
    def configValue(self):
        """ Returns the configuration value of this item.
            By default this is the same as the underlying data but it can be overridden,
        """
        return self.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)

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

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

    def _enforceDataType(self, value):
        """ Converts data to the type of this CTI.
            Used by the setter to ensure that the data and defaultData have the correct type
        """
        raise NotImplementedError()

    @property
    def checkState(self):
        """ Returns how the checkbox for this cti should look like. Returns None for no checkbox.
            :rtype: Qt.CheckState or None
        """
        return None

    @checkState.setter
    def checkState(self, checkState):
        """ Allows the data to be set given a Qt.CheckState.
            Abstract method; you must override it if valueColumnItemFlags returns
            Qt.ItemIsUserCheckable
        """
        raise NotImplementedError()

    @property
    def enabled(self):
        """ Returns the enabled flag which determines if the user can interact with this item.
        """
        return self._enabled

    @enabled.setter
    def enabled(self, enabled):
        """ Sets the enabled flag which determines if the user can interact with this item.
        """
        #logger.debug("Setting enabled = {:5} for {}".format(enabled, self))
        self._enabled = enabled


    @property
    def expanded(self):
        """ Returns the expanded flag which keeps
        """
        return self._expanded

    @expanded.setter
    def expanded(self, expanded):
        """ Keeps track if the config item is expanded.
            Needed to keep state between changing inspectors.
            Note, this can only be used as an initial expanded value but does not update the tree
            when it's constructed. Use ConfigTree.expand for that.
        """
        #logger.debug("Setting expanded = {!r:5} for {}".format(expanded, self))
        self._expanded = expanded


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


    def resetToDefault(self, resetChildren=True):
        """ Resets the data to the default data. By default the children will be reset as well
        """
        self.data = self.defaultData
        if resetChildren:
            for child in self.childItems:
                child.resetToDefault(resetChildren=True)


    def getRefreshBlocked(self):
        """ If True the calls to _refreshNodeFromTarget are blocked.
            This calls self.model.getRefreshBlocked(), so this value is always equal for all
            config tree items in the model
        """
        return self.model.getRefreshBlocked()


    def refreshFromTarget(self, level=0):
        """ Refreshes the configuration tree from the target it monitors (if present).
            Recursively call _refreshNodeFromTarget for itself and all children. Subclasses should
            typically override _refreshNodeFromTarget instead of this function.
            During updateTarget's execution refreshFromTarget is blocked to avoid loops.
        """
        if self.getRefreshBlocked():
            logger.debug("_refreshNodeFromTarget blocked")
            return

        if False and level == 0:
            logger.debug("refreshFromTarget: {}".format(self.nodePath))

        self._refreshNodeFromTarget()
        for child in self.childItems:
            child.refreshFromTarget(level=level + 1)


    def _refreshNodeFromTarget(self):
        """ Refreshes the configuration from the target it monitors (if present).
            The default implementation does nothing; subclasses can override it.
            During updateTarget's execution refreshFromTarget is blocked to avoid loops.
            Typically called from inspector.drawTarget
        """
        pass


    def updateTarget(self, level=0):
        """ Applies the configuration to the target it monitors (if present).
            Recursively call _updateTargetFromNode for itself and all children. Subclasses should
            typically override _updateTargetFromNode instead of this function.

            :param level: the level of recursion.
        """
        #if level == 0:
        #    logger.debug("updateTarget: {}".format(self.nodePath))

        self._updateTargetFromNode()
        for child in self.childItems:
            child.updateTarget(level = level + 1)


    def _updateTargetFromNode(self):
        """ Applies the configuration to the target it monitors (if present).
            The default implementation does nothing; subclasses can override it.
            During updateTarget's execution refreshFromTarget is blocked to avoid loops.
        """
        pass


    def logBranch(self, indent=0, level=logging.DEBUG):
        """ Logs the item and all descendants, one line per child
        """
        line = "{} ({})".format(self, self.configValue)
        if 0:
            print(indent * "    " + line)
        else:
            logger.log(level, indent * "    " + line)
        for childItems in self.childItems:
            childItems.logBranch(indent + 1, level=level)


    #################
    # serialization #
    #################


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


    def marshall(self):
        """ Recursively retrieves values as a dictionary to be used for persistence.

            Typically descendants should override _nodeMarshall instead of this function.
        """
        nodeMar = self._nodeMarshall()
        assert not isinstance(nodeMar, dict), "Node marshall returns dict: {}".format(self)

        childDct = {}
        for childCti in self.childItems:
            childDct[childCti.nodeName] = childCti.marshall()

        if nodeMar is not None and childDct:
            res = {'_data': nodeMar, '_sub': childDct}
            return res
        elif childDct:
            return childDct
        else:
            return nodeMar


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


    def unmarshall(self, cfg):
        """ Initializes itself recursively from a config dict form the persistent settings.

            Typically descendants should override _nodeMarshall instead of this function.
        """
        if isinstance(cfg, dict):
            if '_data' in cfg and '_sub' in cfg:
                self._nodeUnmarshall(cfg['_data'])
                self.unmarshall(cfg['_sub'])
            else:
                for childName, childCfg in cfg.items():
                    try:
                        childCti = self.childByNodeName(childName)
                    except IndexError as _ex:
                        logger.warning("Unable to set values for: {}".format(childName))
                    else:
                        childCti.unmarshall(childCfg)
        else:
            self._nodeUnmarshall(cfg)



    # def marshall(self):
    #     """ Recursively retrieves values as a dictionary to be used for persistence.
    #
    #         Typically descendants should override _nodeMarshall instead of this function.
    #     """
    #
    #     if self.childItems:
    #         dct = {'_data': self._nodeMarshall()}
    #         dct['_sub'] = childDct = {}
    #         for childCti in self.childItems:
    #             childRes = childCti.marshall()
    #             childDct[childCti.nodeName] = childRes
    #     else:
    #         return self._nodeMarshall()
    #
    #     return dct

    ########################
    # Editor look and feel #
    ########################

    @property
    def valueColumnItemFlags(self):
        """ Returns Qt.ItemFlag enum that will be used for the value column in the config tree.
            These flags determine how the user can interact with the value column (e.g. can edit).

            Note that the ConfigTreeModel may override them: it will add the Qt.ItemIsEnabled and
            Qt.ItemIsSelectable to the flags.

            The base implementation of valueColumnItemFlags returns Qt.ItemIsEditable. Make sure to
            implement the createEditor abstract method if Qt.ItemIsEditable is included in the
            result.
        """
        return Qt.ItemIsEditable


    def createEditor(self, delegate, parent, option):
        """ Creates an editor (QWidget) for editing.
            It's parent will be set by the ConfigItemDelegate class that calls this method.

            :param delegate: the delegate that called this function
            :type  delegate: ConfigItemDelegate
            :param parent: The parent widget for the editor
            :type  parent: QWidget
            :param option: describes the parameters used to draw an item in a view widget.
            :type  option: QStyleOptionViewItem
        """
        raise NotImplementedError("createEditor not implemented while Qt.ItemIsEditable is set") class ConfigTreeModel(BaseTreeModel):
    """ An implementation QAbstractItemModel that offers access to configuration data for QTreeViews.
        The underlying data is stored as config tree items (AbstractCti descendants)
    """
    HEADERS = ["Name", "Path", "Value", "Default value", "Item class", "Debug info"]
    (COL_NODE_NAME, COL_NODE_PATH,
     COL_VALUE, COL_DEF_VALUE,
     COL_CTI_TYPE, COL_DEBUG) = range(len(HEADERS))

    COL_DECORATION = COL_VALUE   # Column number that contains the decoration.

    INVISIBLE_ROOT_NAME = '<invisible-root>'

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

        self._autoReset = True
        self._resetMode = DEFAULT_RESET_MODE

        self._invisibleRootTreeItem = MainGroupCti(self.INVISIBLE_ROOT_NAME)
        self._invisibleRootTreeItem.model = self
        #self.dataChanged.connect(self.debug)
        self._refreshBlocked = False


    def flags(self, index):
        """ Returns the item flags for the given index.
        """
        if not index.isValid():
            return 0

        cti = self.getItem(index)

        result = Qt.ItemIsSelectable

        if cti.enabled:
            result |= Qt.ItemIsEnabled

        if index.column() == self.COL_VALUE:
            result |= cti.valueColumnItemFlags

        return result


    def itemData(self, treeItem, column, role=Qt.DisplayRole):
        """ Returns the data stored under the given role for the item.
        """
        if role == Qt.DisplayRole:
            if column == self.COL_NODE_NAME:
                return treeItem.nodeName
            elif column == self.COL_NODE_PATH:
                return treeItem.nodePath
            elif column == self.COL_VALUE:
                return treeItem.displayValue
            elif column == self.COL_DEF_VALUE:
                return treeItem.displayDefaultValue
            elif column == self.COL_CTI_TYPE:
                return type_name(treeItem)
            elif column == self.COL_DEBUG:
                return treeItem.debugInfo
            else:
                raise ValueError("Invalid column: {}".format(column))

        elif role == Qt.EditRole:
            if column == self.COL_VALUE:
                return treeItem.data
            else:
                raise ValueError("Invalid column: {}".format(column))

        elif role == Qt.ToolTipRole:
            if column == self.COL_NODE_NAME or column == self.COL_NODE_PATH:
                return treeItem.nodePath
            elif column == self.COL_VALUE:
                # Give Access to exact values. In particular in scientific-notation spin boxes
                return repr(treeItem.configValue)
            elif column == self.COL_DEF_VALUE:
                return treeItem.displayDefaultValue
            elif column == self.COL_CTI_TYPE:
                return type_name(treeItem)
            elif column == self.COL_DEBUG:
                return treeItem.debugInfo
            else:
                return None

        elif role == Qt.CheckStateRole:
            if column != self.COL_VALUE:
                # The CheckStateRole is called for each cell so return None here.
                return None
            else:
                return treeItem.checkState
        else:
            return super(ConfigTreeModel, self).itemData(treeItem, column, role=role)


    def setItemData(self, treeItem, column, value, role=Qt.EditRole):
        """ Sets the role data for the item at index to value.
        """
        if role == Qt.CheckStateRole:
            if column != self.COL_VALUE:
                return False
            else:
                logger.debug("Setting check state (col={}): {!r}".format(column, value))
                treeItem.checkState = value
                return True

        elif role == Qt.EditRole:
            if column != self.COL_VALUE:
                return False
            else:
                logger.debug("Set Edit value (col={}): {!r}".format(column, value))
                treeItem.data = value
                return True
        else:
            raise ValueError("Unexpected edit role: {}".format(role))


    def setExpanded(self, index, expanded):
        """ Expands the model item specified by the index.
            Overridden from QTreeView to make it persistent (between inspector changes).
        """
        if index.isValid():
            item = self.getItem(index)
            item.expanded = expanded
            #logger.debug("Setting expanded = {} for {}".format(expanded, item))


    def expand(self, index):
        """ Expands the model item specified by the index.
            Overridden from QTreeView to make it persistent (between inspector changes).
        """
        self.setExpanded(index, True)


    def collapse(self, index):
        """ Expands the model item specified by the index.
            Overridden from QTreeView to make it persistent (between inspector changes).
        """
        self.setExpanded(index, False)


    def indexTupleFromItem(self, treeItem): # TODO: move to BaseTreeItem?
        """ Return (first column model index, last column model index) tuple for a configTreeItem
        """
        if not treeItem:
            return (QtCore.QModelIndex(), QtCore.QModelIndex())

        if not treeItem.parentItem: # TODO: only necessary because of childNumber?
            return (QtCore.QModelIndex(), QtCore.QModelIndex())

        # Is there a bug in Qt in QStandardItemModel::indexFromItem?
        # It passes the parent in createIndex. TODO: investigate

        row =  treeItem.childNumber()
        return (self.createIndex(row, 0, treeItem),
                self.createIndex(row, self.columnCount() - 1, treeItem))


    def emitDataChanged(self, treeItem):  # TODO: move to BaseTreeItem?
        """ Emits the data changed for the model indices (all columns) for this treeItem
        """
        indexLeft, indexRight = self.indexTupleFromItem(treeItem)
        self.dataChanged.emit(indexLeft, indexRight)


    def getRefreshBlocked(self):
        """ If set the configuration should not be updated.
            This setting is part of the model so that is shared by all CTIs.
        """
        return self._refreshBlocked


    def setRefreshBlocked(self, blocked):
        """ Set to True to indicate that set the configuration should not be updated.
            This setting is part of the model so that is shared by all CTIs.
            Returns the old value.
        """
        wasBlocked = self._refreshBlocked
        logger.debug("Setting refreshBlocked from {} to {}".format(wasBlocked, blocked))
        self._refreshBlocked = blocked
        return wasBlocked


    @property
    def autoReset(self):
        """ Indicates that the model will be (oartially) reset when the RTI or combo change
        """
        return self._autoReset


    @autoReset.setter
    def autoReset(self, value):
        """ Indicates that the model will be (oartially) reset when the RTI or combo change
        """
        self._autoReset = value


    @property
    def resetMode(self):
        """ Determines what is reset if autoReset is True (either axes or all settings)
        """
        return self._resetMode


    @resetMode.setter
    def resetMode(self, value):
        """ Determines what is reset if autoReset is True (either axes or all settings)
        """
        check_class(value, ResetMode)
        self._resetMode = value


    def resetAllSettings(self):
        """ Resets all items in the tree to their default
        """
        logger.debug("Resetting all settings")
        self.invisibleRootTreeItem.resetToDefault(resetChildren=True)
        self.emitDataChanged(self.invisibleRootTreeItem)     # Updates the tree
        self.sigItemChanged.emit(self.invisibleRootTreeItem) # Updates the inspector


    def resetAllRanges(self):
        """ Resets all (axit/color/ect) range items in the tree to their default
        """
        logger.debug("Resetting all settings")
        self.invisibleRootTreeItem.resetRangesToDefault()
        self.emitDataChanged(self.invisibleRootTreeItem)     # Updates the tree
        self.sigItemChanged.emit(self.invisibleRootTreeItem) # Updates the inspector class ConfigWidget(BasePanel):
    """ Shows the configuration. At the moment only the confg tree view.
    """

    def __init__(self, configTreeModel, parent=None):
        """ Constructor.
            :param parent:
        """
        super(ConfigWidget, self).__init__(parent=parent)

        # Actions that change the reset mode of the reset button
        self.modeActionGroup = QtWidgets.QActionGroup(self)
        self.modeActionGroup.setExclusive(True)

        self.modeAllAction = QtWidgets.QAction("Reset All", self.modeActionGroup)
        self.modeAllAction.setToolTip("Changes button reset mode to reset all settings")
        self.modeAllAction.setCheckable(True)
        self.modeAllAction.triggered.connect(lambda : self.setResetMode(ResetMode.All))


        self.modeRangeAction = QtWidgets.QAction("Reset Ranges", self.modeActionGroup)
        self.modeRangeAction.setToolTip("Changes button reset mode to reset axes")
        self.modeRangeAction.setCheckable(True)
        self.modeRangeAction.triggered.connect(lambda : self.setResetMode(ResetMode.Ranges))

        # Sanity check that actions have been added to action group
        assert self.modeActionGroup.actions(), "Sanity check. resetActionGroup is empty"

        # Actions that actually reset the settings

        self.resetAllAction = QtWidgets.QAction("Reset All", self)
        self.resetAllAction.setToolTip("Resets all settings.")
        self.resetAllAction.setIcon(QtGui.QIcon(os.path.join(icons_directory(), 'reset-l.svg')))
        self.resetAllAction.setShortcut("Ctrl+=")

        self.resetRangesAction = QtWidgets.QAction("Reset Ranges", self)
        self.resetRangesAction.setToolTip(
            "Resets range of all plots, color scales, table column/row sizes etc.")
        self.resetRangesAction.setIcon(QtGui.QIcon(os.path.join(icons_directory(), 'reset-l.svg')))
        self.resetRangesAction.setShortcut("Ctrl+0")

        self.resetButtonMenu = QtWidgets.QMenu()
        self.resetButtonMenu.addAction(self.resetAllAction)
        self.resetButtonMenu.addAction(self.resetRangesAction)
        self.resetButtonMenu.addSection("Default")
        self.resetButtonMenu.addAction(self.modeAllAction)
        self.resetButtonMenu.addAction(self.modeRangeAction)

        # Widgets

        self.mainLayout = QtWidgets.QVBoxLayout(self)
        self.mainLayout.setSpacing(5)
        self.mainLayout.setContentsMargins(DOCK_MARGIN, DOCK_MARGIN, DOCK_MARGIN, DOCK_MARGIN)

        self.configTreeView = ConfigTreeView(configTreeModel, parent=self)
        self.mainLayout.addWidget(self.configTreeView)

        self.buttonLayout = QtWidgets.QHBoxLayout()
        self.mainLayout.addLayout(self.buttonLayout)

        self.autoCheckBox = QtWidgets.QCheckBox("Auto")
        self.autoCheckBox.setToolTip("Auto reset when a new item or axis is selected.")
        self.autoCheckBox.setChecked(True)

        self.resetButton = QtWidgets.QToolButton()
        self.resetButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        self.resetButton.setDefaultAction(self.resetButtonMenu.defaultAction())
        self.resetButton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
        self.resetButton.setMenu(self.resetButtonMenu)

        # Set font size to the same as used for push buttons
        dummyButton = QtWidgets.QPushButton("dummy")
        fontSize = dummyButton.font().pointSize()
        del dummyButton

        logger.debug("Setting QToolButtons font size to: {} point".format(fontSize))
        font = self.resetButton.font()
        font.setPointSizeF(fontSize)
        self.resetButton.setFont(font)

        self.buttonLayout.addStretch()
        self.buttonLayout.addWidget(self.autoCheckBox)
        self.buttonLayout.addWidget(self.resetButton)
        self.buttonLayout.addStretch()

        self.autoCheckBox.stateChanged.connect(self.setAutoReset)
        self.resetRangesAction.triggered.connect(self.configTreeView.resetAllRanges)
        self.resetAllAction.triggered.connect(self.configTreeView.resetAllSettings)

        self.setResetMode(self.configTreeView.resetMode)


    def setAutoReset(self, value):
        """ Sets config tree widget auto reset
        """
        self.configTreeView.autoReset = value


    def setResetMode(self, resetMode):
        """ Sets the reset mode of the reset button reset

            :param resetMode: 'Ranges' all 'All'
        """
        check_class(resetMode, ResetMode)
        if resetMode == ResetMode.All:
            self.resetButton.setDefaultAction(self.resetAllAction)
            self.modeAllAction.setChecked(True)
        elif resetMode == ResetMode.Ranges:
            self.resetButton.setDefaultAction(self.resetRangesAction)
            self.modeRangeAction.setChecked(True)
        else:
            raise ValueError("Unexpected resetMode: {}".format(resetMode))

        self.configTreeView.resetMode = resetMode


    def marshall(self):
        """ Returns a dictionary to save in the persistent settings
        """
        cfg = dict(
            autoRange = self.autoCheckBox.isChecked(),
            resetMode = self.configTreeView.resetMode.value,
        )
        return cfg


    def unmarshall(self, cfg):
        """ Initializes itself from a config dict form the persistent settings.
        """
        if 'autoRange' in cfg:
            self.autoCheckBox.setChecked(cfg['autoRange'])

        if 'resetMode' in cfg:
            self.setResetMode(ResetMode(cfg['resetMode'])) DEBUGGING = ('-d' in sys.argv or '--debug' in sys.argv) PROJECT_NAME = "Argos" PROFILING = False# and DEBUGGING EXIT_CODE_RESTART = 66 # Indicates the program is being 'restarted' 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 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 ErrorMsgInspector(AbstractInspector):
    """ Inspector that shows an error message

        :param str msg: the string that will be
    """
    def __init__(self, collector, msg, parent=None):

        super(ErrorMsgInspector, self).__init__(collector, parent=parent)

        check_is_a_string(msg)
        self.msg = msg

        self._config = self._createConfig()

        self.setCurrentIndex(self.ERROR_PAGE_IDX)


    @classmethod
    def axesNames(cls):
        """ The names of the axes that this inspector visualizes.
            See the parent class documentation for a more detailed explanation.
        """
        return tuple()


    def _createConfig(self):
        """ Creates a config tree item (CTI) hierarchy containing default children.
        """
        rootItem = MainGroupCti('message inspector')
        return rootItem


    def updateContents(self, reason=None, initiator=None):
        """ Override updateContents. Shows the error message
        """
        self.setCurrentIndex(self.ERROR_PAGE_IDX)
        self._showError(msg=self.msg, title="Error") class InspectorSelectionPane(QtWidgets.QFrame):
    """ Shows the attributes of the selected repo tree item
    """

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

        # self.setFrameShape(QtWidgets.QFrame.Box)
        self.menuButton = QtWidgets.QPushButton(NO_INSPECTOR_LABEL)
        self.menuButton.setMinimumWidth(10)

        inspectorMenu = QtWidgets.QMenu("Change Inspector", parent=self.menuButton)
        for action in inspectorActionGroup.actions():
            inspectorMenu.addAction(action)
        self.menuButton.setMenu(inspectorMenu)

        self.messageLabel = QtWidgets.QLabel("")
        self.messageLabel.setObjectName("message_label")
        self.messageLabel.setFrameStyle(QtWidgets.QFrame.Panel)
        self.messageLabel.setFrameShadow(QtWidgets.QFrame.Sunken)
        self.messageLabel.setTextInteractionFlags(Qt.TextSelectableByMouse)

        self.mainLayout = QtWidgets.QHBoxLayout()
        self.mainLayout.setContentsMargins(DOCK_MARGIN, DOCK_MARGIN, DOCK_MARGIN, DOCK_MARGIN)
        self.setLayout(self.mainLayout)

        self.mainLayout.addWidget(self.menuButton, stretch=0)
        self.mainLayout.addWidget(self.messageLabel, stretch=1)

        setWidgetSizePolicy(self.menuButton, hor=QtWidgets.QSizePolicy.Minimum)
        setWidgetSizePolicy(self.messageLabel, hor=QtWidgets.QSizePolicy.Ignored)
        setWidgetSizePolicy(
            self, hor=QtWidgets.QSizePolicy.MinimumExpanding, ver=QtWidgets.QSizePolicy.Fixed)


    def showMessage(self, msg):
        """ Sets the message label to a message text
        """
        logger.debug("Show message to user: '{}'".format(msg))
        self.messageLabel.setText(msg)
        self.messageLabel.setToolTip(msg)


    @QtSlot(InspectorRegItem)
    def updateFromInspectorRegItem(self, inspectorRegItem):
        """ Updates the label from the full name of the InspectorRegItem
        """
        label = NO_INSPECTOR_LABEL if inspectorRegItem is None else inspectorRegItem.name
        self.menuButton.setText(label) 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 getWidgetGeom(widget):
    """ Gets the QWindow or QWidget geometry as a QByteArray.

        Since Qt does not provide this directly we hack this by saving it to the QSettings
        in a temporary location and then reading it from the QSettings.

        :param widget: A QWidget that has a saveGeometry() methods
    """
    settings = QtCore.QSettings()
    settings.beginGroup('temp_conversion')
    try:
        settings.setValue("winGeom", widget.saveGeometry())
        return bytes(settings.value("winGeom"))
    finally:
        settings.endGroup() def getWidgetState(qWindow):
    """ Gets the QWindow or QWidget state as a QByteArray.

        Since Qt does not provide this directly we hack this by saving it to the QSettings
        in a temporary location and then reading it from the QSettings.

        :param widget: A QWidget that has a saveState() methods
    """
    settings = QtCore.QSettings()
    settings.beginGroup('temp_conversion')
    try:
        settings.setValue("winState", qWindow.saveState())
        return bytes(settings.value("winState"))
    finally:
        settings.endGroup() def nameToIdentifier(fullName):
    """ Constructs the regItem identifier given its full name
    """
    return string_to_identifier(fullName, white_space_becomes='') class PluginsDialog(QtWidgets.QDialog):
    """ Dialog window that allows users to configure the installed plugins.
    """

    def __init__(self, label, registry,  parent=None):
        """ Constructor
        """
        super(PluginsDialog, self).__init__(parent=parent)
        check_class(registry, BaseRegistry)

        self._label = label
        self._orgRegistry = registry
        self._registry = copy.deepcopy(registry)  # make copy so changes can be canceled
        self._tableModel = self._registry.createTableModel(parent=self)
        self.mapper = QtWidgets.QDataWidgetMapper(parent=self)
        self.mapper.setModel(self._tableModel)

        self.setWindowTitle("Argos {} Plugins".format(label))

        layout = QtWidgets.QVBoxLayout(self)

        self.verSplitter = QtWidgets.QSplitter(Qt.Vertical)
        #self.verSplitter.setCollapsible(1, False)
        self.verSplitter.setChildrenCollapsible(False)
        layout.addWidget(self.verSplitter)

        self.tableWidget = TableEditWidget(self._tableModel)
        self.verSplitter.addWidget(self.tableWidget)

        self._tableView = self.tableWidget.tableView
        self._tableView.installEventFilter(self)

        # Form
        self.horSplitter = QtWidgets.QSplitter(Qt.Horizontal)
        self.horSplitter.setChildrenCollapsible(False)

        self.verSplitter.addWidget(self.horSplitter)

        self.formWidget = QtWidgets.QWidget()
        self.formLayout = QtWidgets.QFormLayout()
        self.formLayout.setContentsMargins(0, 0, 0, 0)
        self.formWidget.setLayout(self.formLayout)
        self.horSplitter.addWidget(self.formWidget)

        self._editWidgets = []
        itemCls = registry.ITEM_CLASS
        assert len(itemCls.LABELS) == len(itemCls.TYPES), \
            "Regtype Mismatch: {} != {}".format(len(itemCls.LABELS), len(itemCls.TYPES))
        for col, (label, regType) in enumerate(zip(itemCls.LABELS, itemCls.TYPES)):

            if regType == RegType.String:
                editWidget = QtWidgets.QLineEdit()
                self.mapper.addMapping(editWidget, col)
            elif regType == RegType.ShortCut:
                editWidget = ShortCutEditor()
                self.mapper.addMapping(editWidget, col)
            elif regType == RegType.ColorStr:
                editWidget = ColorSelectWidget()
                self.mapper.addMapping(editWidget.lineEditor, col)
            else:
                raise AssertionError("Unexpected regType: {}".format(regType))
            editWidget.installEventFilter(self)
            self.formLayout.addRow(label, editWidget)
            self._editWidgets.append(editWidget)

        # Detail info widget
        font = QtGui.QFont()
        font.setFamily(MONO_FONT)
        font.setFixedPitch(True)
        font.setPointSize(FONT_SIZE)

        self.editor = QtWidgets.QTextEdit()
        self.editor.setReadOnly(True)
        #self.editor.setFocusPolicy(Qt.NoFocus) # Allow focus so that user can copy text from it.
        #self.editor.setFont(font)
        self.editor.setWordWrapMode(QtGui.QTextOption.WordWrap)
        self.editor.clear()
        self.horSplitter.addWidget(self.editor)

        self.horSplitter.setStretchFactor(0, 2)
        self.horSplitter.setStretchFactor(1, 3)
        self.verSplitter.setSizes([300, 150])

        # Reset/Cancel/Save Buttons

        self.saveButton = QtWidgets.QPushButton("Save")
        self.saveButton.clicked.connect(self.accept)

        self.cancelButton = QtWidgets.QPushButton("Cancel")
        self.cancelButton.clicked.connect(self.reject)

        self.resetButton = QtWidgets.QPushButton("Reset Table to Defaults...")
        self.resetButton.clicked.connect(self.resetToDefaults)
        self.resetButton.setIcon(QtGui.QIcon(os.path.join(icons_directory(), 'reset-l.svg')))

        # We use a button layout instead of a QButtonBox because there always will be a default
        # button (e.g. the Save button) that will light up, even if another widget has the focus.
        # From https://doc.qt.io/archives/qt-4.8/qdialogbuttonbox.html#details
        #   However, if there is no default button set and to preserve which button is the default
        #   button across platforms when using the QPushButton::autoDefault property, the first
        #   push button with the accept role is made the default button when the QDialogButtonBox
        #   is shown,

        self.buttonLayout = QtWidgets.QHBoxLayout()
        self.buttonLayout.addWidget(self.resetButton)
        self.buttonLayout.addStretch()
        if sys.platform == 'darwin':
            self.buttonLayout.addWidget(self.cancelButton)
            self.buttonLayout.addWidget(self.saveButton)
        else:
            self.buttonLayout.addWidget(self.saveButton)
            self.buttonLayout.addWidget(self.cancelButton)
        layout.addLayout(self.buttonLayout)

        # Connect signals and populate

        self.tableWidget.tableView.selectionModel().currentChanged.connect(self.currentItemChanged)
        self.tableWidget.tableView.model().sigItemChanged.connect(self._updateEditor)

        self.resize(QtCore.QSize(1100, 700))
        self.tableWidget.tableView.setFocus(Qt.NoFocusReason)

        self.tryImportAllPlugins()



    def eventFilter(self, targetWidget, event):
        """ Toggles the focus between table and edit form when the return key is pressed.
        """
        if event.type() == QtCore.QEvent.KeyPress and event.key() in (Qt.Key_Enter, Qt.Key_Return):

            if (event.modifiers() & Qt.ControlModifier):
                self.accept()
                return True

            if targetWidget in self._editWidgets:
                self._tableView.setFocus()
                return True

            elif targetWidget == self._tableView:
                curIdx = self._tableView.currentIndex()
                col = curIdx.column() if curIdx.isValid() else 0
                self._editWidgets[col].setFocus()
                return True

        # Give parent event filter chance to filter the event. Otherwise targetWidget will get it.
        return super(PluginsDialog, self).eventFilter(targetWidget, event)


    def accept(self):
        """ Saves registry.

            After saving the application may be in an inconsistent state. For instance, files
            may be opened with plugins that no longer exist. Therefore the caller must 'restart'
            the application if the changes were accepted.
        """
        logger.debug("Updating registry from dialog")

        # Copy contents from copy back to the original registry
        self._orgRegistry.clear()
        self._orgRegistry.unmarshall(self._registry.marshall())
        super(PluginsDialog, self).accept()


    def resetToDefaults(self):
        """ Resets the registry to it's default values
        """
        button = QtWidgets.QMessageBox.question(
            self, "Reset {}".format(self._label), "Reset plugins to the default set?")

        if button != QtWidgets.QMessageBox.Yes:
            logger.info("Resetting registry canceled.")
            return None

        logger.info("Resetting {} registry to its default.".format(self._label))
        self._tableModel.beginResetModel()
        try:
            self._registry.unmarshall(cfg={}) #  Empty config will cause the defaults to be used.
            self.tryImportAllPlugins()
        finally:
            self._tableModel.endResetModel()


    def tryImportAllPlugins(self):
        """ Tries to import all underlying plugin classes
        """
        logger.debug("Importing all plugins.")

        for regItem in self._registry.items:
            if not regItem.triedImport:
                self._tableModel.tryImportRegItem(regItem)

        logger.debug("Importing finished.")


    def getCurrentRegItem(self):
        """ Returns the item that is currently selected in the table.
            Can return None if there is no data in the table
        """
        return self.tableWidget.tableView.getCurrentItem()


    @QtSlot(QtCore.QModelIndex, QtCore.QModelIndex)
    def currentItemChanged(self, currentIndex=None, _previousIndex=None):
        """ Updates the description text widget when the user clicks on a selector in the table.
            The _currentIndex and _previousIndex parameters are ignored.
        """
        self.mapper.setCurrentModelIndex(currentIndex)
        regItem = self.getCurrentRegItem()
        self._updateEditor(regItem)


    def _updateEditor(self, regItem):
        """ Updates the editor with contents of the currently selected regItem
        """
        self.editor.clear()

        if regItem is None:
            return

        header = "<h2>{}</h2>".format(regItem.name)

        if regItem.successfullyImported is None:
            html = header + 'Plugin not yet imported!'
            self.editor.setHtml(wrapHtmlColor(html, COLOR_ERROR))
        elif regItem.successfullyImported is False:
            html = "{}Unable to import plugin.\n\nError: {}".format(header, regItem.exception)
            if hasattr(regItem.exception, "traceBackString"):
                html += "<pre>{}</pre>".format(regItem.exception.traceBackString)
            self.editor.setHtml(wrapHtmlColor(html, COLOR_ERROR))
        elif regItem.descriptionHtml:
            self.editor.setHtml(header.replace('\n', '<br>') + regItem.descriptionHtml)
        else:
            self.editor.setHtml(header + regItem.docString.replace('\n', '<br>')) class RtiIconFactory(object):
    """ A factory class that generates QIcons for use in the Repository Tree.

        Typically the RepoTreeItems have a reference to a (singleton) RtiIconFactory and
        call getIcon to get an icon of a certain glyph, color and open/close state.

        The factory contains a icon registry that contains for registered (glyph, open/close state)
        combinations the location of an SVG file. The SVG files originate from the snip-icon
        library. See http://www.snipicons.com/

        The getIcon method can optionally set the fill color of the icons. It also caches the
        generated QItem objects to that they only have to be created once.
    """

    ICONS_DIRECTORY = os.path.join(program_directory(), 'img/snipicons')
    #ICON_SIZE = 32 # Render in this size

    # File state
    OPEN = "open"
    CLOSED = "closed"

    # Registered glyph names
    TRANSPARENT = "transparent"  # Transparent icon to reserve space
    ERROR = "error"
    FOLDER = "folder"
    FILE = "file"
    ARRAY = "array"
    FIELD = "field"
    DIMENSION = "dimension"
    SEQUENCE = "sequence"
    SCALAR = "scalar"

    # Icon colors from constants defined at the top of this module
    COLOR_UNDEF      = ICON_COLOR_UNDEF
    COLOR_UNKNOWN    = ICON_COLOR_UNKNOWN
    COLOR_ERROR      = ICON_COLOR_ERROR
    COLOR_MEMORY     = ICON_COLOR_MEMORY

    _singleInstance = None

    def __init__(self):
        """ Constructor
        """
        self._icons = {}
        self._registry = {}
        self.colorsToBeReplaced = ('#008BFF', '#00AAFF')
        self.renderSizes = [16, 24, 32, 64]

        self.registerIcon(None, None) # no icon
        self.registerIcon("",   None) # no icon
        self.registerIcon("transparent_1x1.svg", self.TRANSPARENT)
        self.registerIcon("warning-sign.svg", self.ERROR)
        self.registerIcon("folder-open.svg",  self.FOLDER, True)
        self.registerIcon("folder-close.svg", self.FOLDER, False)
        self.registerIcon("file.svg",         self.FILE, True)
        self.registerIcon("file-inverse.svg", self.FILE, False)
        self.registerIcon("th-large.svg",     self.ARRAY)
        self.registerIcon("asterisk.svg",     self.FIELD)
        self.registerIcon("move.svg",         self.DIMENSION)
        self.registerIcon("align-left.svg",   self.SEQUENCE)
        self.registerIcon("leaf.svg",         self.SCALAR)


    @classmethod
    def singleton(cls):
        """ Returns the RtiIconFactory singleton.
        """
        if cls._singleInstance is None:
            cls._singleInstance = cls()
        return cls._singleInstance


    def registerIcon(self, fileName, glyph, isOpen=None):
        """ Register an icon SVG file given a glyph, and optionally the open/close state.

            :param fileName: filename to the SVG file.
                If the filename is a relative path, the ICONS_DIRECTORY will be prepended.
            :param glyph: a string describing the glyph (e.g. 'file', 'array')
            :param isOpen: boolean that indicates if the RTI is open or closed.
                If None, the icon will be registered for open is both True and False
            :return: QIcon
        """
        check_class(isOpen, bool, allow_none=True)

        if fileName and not os.path.isabs(fileName):
            fileName = os.path.join(self.ICONS_DIRECTORY, fileName)

        if isOpen is None:
            # Register both opened and closed variants
            self._registry[(glyph, True)] = fileName
            self._registry[(glyph, False)] = fileName
        else:
            self._registry[(glyph, isOpen)] = fileName


    def getIcon(self, glyph, isOpen, color=None):
        """ Returns a QIcon given a glyph name, open/closed state and color.

            The reslulting icon is cached so that it only needs to be rendered once.

            :param glyph: name of a registered glyph (e.g. 'file', 'array')
            :param isOpen: boolean that indicates if the RTI is open or closed.
            :param color: '#RRGGBB' string (e.g. '#FF0000' for red)
            :return: QtGui.QIcon
        """
        try:
            fileName = self._registry[(glyph, isOpen)]
        except KeyError:
            logger.warning("Unregistered icon glyph: {} (open={})".format(glyph, isOpen))
            from argos.utils.logs import log_dictionary
            log_dictionary(self._registry, "registry", logger=logger)
            raise

        return self.loadIcon(fileName, color=color)


    def loadIcon(self, fileName, color=None):
        """ Reads SVG from a file name and creates an QIcon from it.

            Optionally replaces the color. Caches the created icons.

            :param fileName: absolute path to an icon file.
                If False/empty/None, None returned, which yields no icon.
            :param color: '#RRGGBB' string (e.g. '#FF0000' for red)
            :return: QtGui.QIcon
        """
        if not fileName:
            return None

        key = (fileName, color)
        if key not in self._icons:
            try:
                with open(fileName, 'r') as input:
                    svg = input.read()

                self._icons[key] = self.createIconFromSvg(svg, color=color)
            except Exception as ex:
                # It's preferable to show no icon in case of an error rather than letting
                # the application fail. Icons are a (very) nice to have.
                logger.warning("Unable to read icon: {}".format(ex))
                if DEBUGGING:
                    raise
                else:
                    return None

        return self._icons[key]


    def createIconFromSvg(self, svg, color=None, colorsToBeReplaced=None):
        """ Creates a QIcon given an SVG string.

            Optionally replaces the colors in colorsToBeReplaced by color.

            :param svg: string containing Scalable Vector Graphics XML
            :param color: '#RRGGBB' string (e.g. '#FF0000' for red)
            :param colorsToBeReplaced: optional list of colors to be replaced by color
                If None, it will be set to the fill colors of the snip-icon libary
            :return: QtGui.QIcon
        """
        if colorsToBeReplaced is None:
            colorsToBeReplaced = self.colorsToBeReplaced

        if color:
            for oldColor in colorsToBeReplaced:
                svg = svg.replace(oldColor, color)

        # From http://stackoverflow.com/questions/15123544/change-the-color-of-an-svg-in-qt
        qByteArray = QtCore.QByteArray(svg.encode('utf-8'))
        # qByteArray = QtCore.QByteArray() # the old PyQt way
        # qByteArray.append(svg)

        svgRenderer = QtSvg.QSvgRenderer(qByteArray)
        icon = QtGui.QIcon()
        for size in self.renderSizes:
            pixMap = QtGui.QPixmap(QtCore.QSize(size, size))
            pixMap.fill(Qt.transparent)
            pixPainter = QtGui.QPainter(pixMap)
            pixPainter.setRenderHint(QtGui.QPainter.TextAntialiasing, True)
            pixPainter.setRenderHint(QtGui.QPainter.Antialiasing, True)
            svgRenderer.render(pixPainter)
            pixPainter.end()
            icon.addPixmap(pixMap)

        return icon



    def createIconFromSvg(self, svg, color=None, colorsToBeReplaced=None):
        """ Creates a QIcon given an SVG string.

            Optionally replaces the colors in colorsToBeReplaced by color.

            :param svg: string containing Scalable Vector Graphics XML
            :param color: '#RRGGBB' string (e.g. '#FF0000' for red)
            :param colorsToBeReplaced: optional list of colors to be replaced by color
                If None, it will be set to the fill colors of the snip-icon libary
            :return: QtGui.QIcon
        """
        if colorsToBeReplaced is None:
            colorsToBeReplaced = self.colorsToBeReplaced

        if color:
            for oldColor in colorsToBeReplaced:
                svg = svg.replace(oldColor, color)

        # From http://stackoverflow.com/questions/15123544/change-the-color-of-an-svg-in-qt
        qByteArray = QtCore.QByteArray(svg.encode('utf-8'))
        # qByteArray = QtCore.QByteArray() # the old PyQt way
        # qByteArray.append(svg)

        svgRenderer = QtSvg.QSvgRenderer(qByteArray)
        icon = QtGui.QIcon()
        for size in self.renderSizes:
            pixMap = QtGui.QPixmap(QtCore.QSize(size, size))
            pixMap.fill(Qt.transparent)
            pixPainter = QtGui.QPainter(pixMap)
            pixPainter.setRenderHint(QtGui.QPainter.TextAntialiasing, True)
            pixPainter.setRenderHint(QtGui.QPainter.Antialiasing, True)
            svgRenderer.render(pixPainter)
            pixPainter.end()
            icon.addPixmap(pixMap)

        return icon class RtiRegistry(BaseRegistry):
    """ Class that can be used to register repository tree items (RTIs).

        Maintains a name to RtiClass mapping and an extension to RtiClass mapping.
        The extension in the extensionToRti assure that a unique RTI is used as default mapping,
        the extensions in the RtiRegItem class do not have to be unique and are used in the
        filter in the getFileDialogFilter function.
    """
    ITEM_CLASS = RtiRegItem

    DIRECTORY_REG_ITEM = RtiRegItem('Directory', 'argos.repo.filesytemrtis.DirectoryRti',
                                    iconColor=ICON_COLOR_UNKNOWN)

    def __init__(self):
        """ Constructor
        """
        super(RtiRegistry, self).__init__()
        self._extensionMap = {}


    def getRtiRegItemByExtension(self, filePath):
        """ Returns the first RtiRegItem class where filePath matches one of the globs patherns.
            Returns None if no class registered for the extension.
        """
        # Current implementation just returns the first rtiRegItem that contains the extension.
        logger.debug("{} getRtiRegItemByExtension, filePath: {}".format(self, filePath))
        for rtiRegItem in self._items:
            if rtiRegItem.pathNameMatchesGlobs(filePath):
                return rtiRegItem

        return None


    def getFileDialogFilter(self):
        """ Returns a filter that can be used in open file dialogs,
            for example: 'All files (*);;Txt (*.txt;*.text);;netCDF(*.nc;*.nc4)'
        """
        filters = []
        for regRti in self.items:
            filters.append(regRti.getFileDialogFilter())
        return ';;'.join(filters)


    def extraItemsForOpenAsMenu(self):
        """ Creates list of RtiRegItem to append to the 'open-as' and 'reload-as menus
        """
        # Add directory to the context menu so a an Exdir 'file' can be re-opened as a directory
        return [self.DIRECTORY_REG_ITEM]


    def getDefaultItems(self):
        """ Returns a list with the default plugins in the repo tree item registry.
        """

        # Note that when finding a plugin by extension, Argos uses the first one that matches.
        # Therefore put the defaults at the top of the list. The user can changed the order in the
        # plugin configuration dialog.

        hdfGlobs = '*.hdf5;*.h5;*.h5e;*.he5' # hdf extension is for HDF-4
        return [
            RtiRegItem('HDF-5 file',
                       'argos.repo.rtiplugins.hdf5.H5pyFileRti',
                       iconColor=ICON_COLOR_H5PY,
                       globs=hdfGlobs),

            RtiRegItem('Exdir file',
                       'argos.repo.rtiplugins.exdir.ExdirFileRti',
                       iconColor=ICON_COLOR_EXDIR,
                       globs='*.exdir'),

            RtiRegItem('NetCDF file',
                       'argos.repo.rtiplugins.ncdf.NcdfFileRti',
                       iconColor=ICON_COLOR_NCDF4,
                       globs='*.nc;*.nc4'),

            RtiRegItem('Pandas HDF file',
                       'argos.repo.rtiplugins.pandasio.PandasHdfFileRti',
                       iconColor=ICON_COLOR_PANDAS,
                       globs=hdfGlobs),

            RtiRegItem('Pandas CSV file',
                       'argos.repo.rtiplugins.pandasio.PandasCsvFileRti',
                       iconColor=ICON_COLOR_PANDAS,
                       globs='*.csv'),

            RtiRegItem('NumPy binary file',
                       'argos.repo.rtiplugins.numpyio.NumpyBinaryFileRti',
                       iconColor=ICON_COLOR_NUMPY,
                       globs='*.npy'),

            RtiRegItem('NumPy compressed file',
                       'argos.repo.rtiplugins.numpyio.NumpyCompressedFileRti',
                       iconColor=ICON_COLOR_NUMPY,
                       globs='*.npz'),

            RtiRegItem('NumPy text file',
                       'argos.repo.rtiplugins.numpyio.NumpyTextFileRti',
                       iconColor=ICON_COLOR_NUMPY,
                       #globs=['*.txt;*.text'),
                       globs='*.dat'),

            RtiRegItem('IDL save file',
                       'argos.repo.rtiplugins.scipyio.IdlSaveFileRti',
                       iconColor=ICON_COLOR_SCIPY,
                       globs='*.sav'),

            RtiRegItem('MATLAB file',
                       'argos.repo.rtiplugins.scipyio.MatlabFileRti',
                       iconColor=ICON_COLOR_SCIPY,
                       globs='*.mat'),

            RtiRegItem('Wav file',
                       'argos.repo.rtiplugins.scipyio.WavFileRti',
                       iconColor=ICON_COLOR_SCIPY,
                       globs='*.wav'),

            RtiRegItem('Pillow image',
                       'argos.repo.rtiplugins.pillowio.PillowFileRti',
                       iconColor=ICON_COLOR_PILLOW,
                       globs='*.bmp;*.eps;*.im;*.gif;*.jpg;*.jpeg;*.msp;*.pcx;*.png;*.ppm;*.spi;'
                             '*.tif;*.tiff;*.xbm;*.xv'),
        ] class RepoWidget(BasePanel):
    """ Groups the repository tree plus the details dock widgets.
    """
    def __init__(self, repoTreeModel, collector, parent=None):
        """ Constructor.
            :param parent:
        """
        super(RepoWidget, self).__init__(parent=parent)

        self.detailDockPanes = []

        self.mainLayout = QtWidgets.QVBoxLayout()
        self.mainLayout.setSpacing(DOCK_SPACING)
        self.mainLayout.setContentsMargins(DOCK_MARGIN, DOCK_MARGIN, DOCK_MARGIN, DOCK_MARGIN)
        self.setLayout(self.mainLayout)

        self.mainSplitter = QtWidgets.QSplitter(orientation=Qt.Vertical)
        self.mainLayout.addWidget(self.mainSplitter)

        self.repoTreeView = RepoTreeView(repoTreeModel, collector)
        self.mainSplitter.addWidget(self.repoTreeView)
        self.mainSplitter.setCollapsible(0, False)

        self.tabWidget = QtWidgets.QTabWidget()
        self.mainSplitter.addWidget(self.tabWidget)
        self.mainSplitter.setCollapsible(1, True)

        self.propertiesPane = self.addDetailsPane(PropertiesPane(self.repoTreeView))
        self.attributesPane = self.addDetailsPane(AttributesPane(self.repoTreeView))
        self.quickLookPane = self.addDetailsPane(QuickLookPane(self.repoTreeView))

        self.repoTreeView.sigRepoItemChanged.connect(self.repoItemChanged)
        self.tabWidget.currentChanged.connect(self.tabChanged)

        self.tabWidget.setCurrentIndex(2) # Show quick look tab the first time the program runs



    def addDetailsPane(self, detailPane):
        """ Adds a pane widget as a tab.

            Returns: detailPane so calls can be chained.
        """
        self.tabWidget.addTab(detailPane, detailPane.classLabel())
        self.detailDockPanes.append(detailPane)
        return detailPane


    def marshall(self):
        """ Returns a dictionary to save in the persistent settings
        """
        cfg = dict(
            tabIndex = self.tabWidget.currentIndex(),
            splitterSizes = self.mainSplitter.sizes(),
            treeHeaders = self.repoTreeView.marshall(),
            propertiesPane = self.propertiesPane.marshall(),
            attributesPane = self.attributesPane.marshall(),
        )

        return cfg


    def unmarshall(self, cfg):
        """ Initializes itself from a config dict form the persistent settings.
        """
        if 'tabIndex' in cfg:
            self.tabWidget.setCurrentIndex(cfg['tabIndex'])

        if 'splitterSizes' in cfg:
            self.mainSplitter.setSizes(cfg['splitterSizes'])

        self.repoTreeView.unmarshall(cfg.get('treeHeaders'))

        self.propertiesPane.unmarshall(cfg.get('propertiesPane', {}))
        self.attributesPane.unmarshall(cfg.get('attributesPane', {}))


    def repoItemChanged(self, rti):
        """ Updates the current tab with the newly selected repoWidget

            The rti parameter can be None when no RTI is selected in the repository tree.
        """
        logger.debug("RepoWidget.repoItemChanged: {}".format(rti))

        curPanel = self.tabWidget.currentWidget() # can be None according to docs
        if curPanel:
            curPanel.repoItemChanged(rti)
        else:
            logger.debug("No panel selected")


    def tabChanged(self, _tabIndex):
        """ Updates the tab from the currently selected repo item in the tree.
        """
        currentRepoItem, _currentIndex = self.repoTreeView.getCurrentItem()
        self.repoItemChanged(currentRepoItem) def createArgosTestData():
    """ Makes various test data sets for debugging
    """

    myDict = {}
    myDict['name'] = 'Pac Man'
    myDict['age'] = 34
    myDict['ghosts'] = ['Inky', 'Blinky', 'Pinky', 'Clyde']
    myDict['none'] = None
    myDict['numbers'] = {'int': 5, 'float': -6.6, 'large float': 7e77,
                         '-inf': np.NINF, 'nan': np.nan, 'complex': 8-9j, 'decimal': Decimal(4.444)}

    myDict['random'] = np.random.randn(8, 5)
    array = np.arange(240, dtype=np.float64).reshape(8, 30).transpose()
    #array[10, 4] = 7e77
    myDict['array'] = array

    myDict['ones'] = np.ones_like(array)

    x = np.arange(60)
    parabola = 0.02 * np.square(x)
    myDict['parabola'] = parabola

    myDict['array_1K'] = np.random.randn(1000)
    myDict['array_10K'] = np.random.randn(10 * 1000)
    myDict['array_100K'] = np.random.randn(100 * 1000)
    myDict['array_200K'] = np.random.randn(200 * 1000)
    myDict['array_500K'] = np.random.randn(500 * 1000)
    myDict['array_1000K'] = np.random.randn(1000 * 1000)

    #masked_array = ma.arange(2400, dtype=np.float16).reshape(60, 40)
    masked_array = np.transpose(ma.ones(shape=(40, 60)) * parabola)
    masked_array[:, 0:7] = ma.masked
    masked_array[15:45, 10:17] = ma.masked
    myDict['array_masked'] = masked_array

    nan_array = np.arange(2400, dtype=np.float16).reshape(60, 40)
    nan_array[:, 0:7] = np.nan
    nan_array[15:45, 10:17] = np.nan
    myDict['array_with_nans'] = nan_array

    inf_array = np.arange(2400, dtype=np.float16).reshape(60, 40)
    inf_array[:, 0:7] = np.inf
    inf_array[15:45, 10:17] = np.NINF
    myDict['array_with_infs'] = inf_array

    myDict['structured_arr1'] = np.array([(1,2.,'Hello'), (2,3.,"World")],
                                          dtype=[('foo', 'i4'),('bar', 'f4'), ('baz', 'S10')])

    myDict['structured_arr2'] = np.array([(1.5,2.5,(1.0,2.0)),(3.,4.,(4.,5.)),(1.,3.,(2.,6.))],
                                         dtype=[('x','f4'),('y',np.float32),('value','f4',(2,2))])
    myDict['structured_arr3'] = np.array([(1.5,2.5,(2.0, )),(3.,4.,(5., )),(1.,3.,(2.,))],
                                         dtype=[('1st','f4'),('2nd',np.float32),('3rd','f4',(2,))])

    myDict['empty_array1D'] = np.ones(shape=(0, ))
    myDict['empty_array2D'] = np.ones(shape=(0, 6))
    myDict['empty_array3D'] = np.ones(shape=(10, 0, 6))

    # A structured array with offsets and titles
    dt4 = np.dtype({'names': ['r','b'], 'formats': ['u1', 'u1'], 'offsets': [0, 2]})
                    #'titles': ['Red pixel', 'Blue pixel']})
    myDict['structured_arr4'] = np.array([(255, 11), (1, 50)], dtype=dt4)

    myDict['structured_masked_arr2'] = ma.MaskedArray(myDict['structured_arr2'], fill_value=-99999)
    myDict['structured_masked_arr2'].mask[0][0] = True
    myDict['structured_masked_arr2'].mask[0][2][0,1] = True # doesn't seem to work

    myDict['numpy string array']  = np.array(['Yackity', 'Smackity'])
    myDict['numpy unicode array'] = np.array(['Table', u'ταБЬℓσ'])
    myDict['byte array'] = bytearray(range(256))
    myDict['bytes'] = bytes(bytearray(range(0, 256)))
    myDict['string'] = "The quick brown fox jumps over the lazy dog!"

    mappingRti = MappingRti(myDict, nodeName="myDict", iconColor=ICON_COLOR_MEMORY)

    # New axis at the beginning and end.
    mappingRti.insertChild(SyntheticArrayRti(
        'newaxis heading 2D', fun=lambda: makeConcentricCircles()[np.newaxis, np.newaxis, ...]))
    mappingRti.insertChild(SyntheticArrayRti(
        'newaxis trailing 2D', fun=lambda: makeConcentricCircles()[..., np.newaxis, np.newaxis]))

    mappingRti.insertChild(SyntheticArrayRti(
        'newaxis heading 1D', fun=lambda: makeConcentricCircles()[50,:][np.newaxis, np.newaxis, :]))

    mappingRti.insertChild(SyntheticArrayRti(
        'newaxis trailing 1D', fun=lambda: makeConcentricCircles()[50,:][:, np.newaxis, np.newaxis]))

    # Synthetic images for testing color maps.
    colorMapRti = mappingRti.insertChild(MappingRti({}, nodeName="test color maps"))
    colorMapRti.insertChild(SyntheticArrayRti('concentric circles', fun=makeConcentricCircles))
    colorMapRti.insertChild(SyntheticArrayRti('ramp', fun=makeRamp))
    colorMapRti.insertChild(SyntheticArrayRti('arctan2', fun=makeArcTan2))
    colorMapRti.insertChild(SyntheticArrayRti('spiral', fun=makeSpiral))
    colorMapRti.insertChild(SyntheticArrayRti('sine product', fun=makeSineProduct))
    colorMapRti.insertChild(SyntheticArrayRti('uniform noise', fun=makeUniformNoise))

    with warnings.catch_warnings():
        # Ignore future warnins about Panel
        warnings.simplefilter(action='ignore', category=FutureWarning)
        addPandasTestData(mappingRti)

    return mappingRti 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 check_is_a_sequence(var, allow_none=False):
    """ Calls is_a_sequence and raises a type error if the check fails.
    """
    if not is_a_sequence(var, allow_none=allow_none):
        raise TypeError("var must be a list or tuple, however type(var) is {}"
                        .format(type(var))) def argosConfigDirectory():
    r""" Gets the Argos configuration directory.

        The config directory is platform dependent. (See the module doc string at the top).
    """
    return os.path.join(baseConfigLocation(), ORGANIZATION_NAME, SCRIPT_NAME) def argosLogDirectory():
    r""" Returns the directory where Argos can store its log files.

        This is the 'logs' subdirectory of the argosLocalDataDirectory()
    """
    return os.path.join(argosLocalDataDirectory(), 'logs') def string_to_identifier(s, white_space_becomes='_'):
    """ Takes a string and makes it suitable for use as an identifier

        Translates to lower case
        Replaces white space by the white_space_becomes character (default=underscore).
        Removes and punctuation.
    """
    import re
    s = s.lower()
    s = re.sub(r"\s+", white_space_becomes, s) # replace whitespace with underscores
    s = re.sub(r"-", "_", s) # replace hyphens with underscores
    s = re.sub(r"[^A-Za-z0-9_]", "", s) # remove everything that's not a character, a digit or a _
    return s def versionStrToTuple(versionStr):
    """ Converts a version string to tuple

        Version string has format <major>.<minor>.<patch><postfix> where postfix can be empty.
            E.g. versionStrToTuple('0.3.1rc1') returns (0, 3, 1, 'rc1')
    """
    matchObj = re.match(r"(\d+)\.(\d+)\.(\d+)(\w*)", versionStr)

    major = int(matchObj.group(1))
    minor = int(matchObj.group(2))
    patch = int(matchObj.group(3))
    postfix = matchObj.group(4)

    return (major, minor, patch, postfix) class AboutDialog(QtWidgets.QDialog):
    """ Dialog window that shows dependency information.
    """
    def __init__(self, parent=None):
        """ Constructor
        """
        super(AboutDialog, self).__init__(parent=parent)
        self.setModal(True)

        mainLayout = QtWidgets.QVBoxLayout()
        self.setLayout(mainLayout)

        progVersionLabel = QtWidgets.QLabel()
        progVersionLabel.setText("{} {}{}".format(PROJECT_NAME, VERSION,
                                                  ' (debugging-mode)' if DEBUGGING else ''))
        progVersionLabel.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
        mainLayout.addWidget(progVersionLabel)

        font = QtGui.QFont()
        font.setFamily(MONO_FONT)
        font.setFixedPitch(True)
        font.setPointSize(FONT_SIZE)

        self.editor = QtWidgets.QPlainTextEdit()
        self.editor.setReadOnly(True)
        self.editor.setFont(font)
        self.editor.setWordWrapMode(QtGui.QTextOption.NoWrap)
        self.editor.clear()
        self.editor.setPlainText("Retrieving package info...")
        mainLayout.addWidget(self.editor)

        self.progressLabel = QtWidgets.QLabel()
        mainLayout.addWidget(self.progressLabel)

        buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok)
        buttonBox.accepted.connect(self.accept)
        mainLayout.addWidget(buttonBox)

        self.resize(QtCore.QSize(800, 400))


    def _addModuleInfo(self, moduleInfo):
        """ Adds a line with module info to the editor
            :param moduleInfo: can either be a string or a module info class.
                In the first case, an object is instantiated as ImportedModuleInfo(moduleInfo).
        """
        if is_a_string(moduleInfo):
            moduleInfo = mi.ImportedModuleInfo(moduleInfo)

        line = "{:15s}: {}".format(moduleInfo.name, moduleInfo.verboseVersion)
        logger.info("  {}".format(line))
        self.editor.appendPlainText(line)
        QtWidgets.QApplication.instance().processEvents()


    def addDependencyInfo(self):
        """ Adds version info about the installed dependencies
        """
        logger.debug("Adding dependency info to the AboutDialog")
        self.progressLabel.setText("Retrieving package info...")
        self.editor.clear()

        logger.info("Used packages:")
        self._addModuleInfo(mi.PythonModuleInfo())
        self._addModuleInfo(mi.QtModuleInfo())

        modules = ['numpy', 'scipy', 'pandas', 'pyqtgraph', 'exdir', 'pgcolorbar', 'cmlib']
        for module in modules:
            self._addModuleInfo(module)

        self._addModuleInfo(mi.H5pyModuleInfo())
        self._addModuleInfo(mi.NetCDF4ModuleInfo())
        self._addModuleInfo(mi.PillowInfo())

        self._addModuleInfo(mi.ArgosModuleInfo())

        self.progressLabel.setText("")
        logger.debug("Finished adding dependency info to the AboutDialog") CENTRAL_MARGIN  = 0 CENTRAL_SPACING = 0 def processEvents():
    """ Processes all pending events for the calling thread until there are no more events to
        process.
    """
    QtWidgets.QApplication.instance().processEvents()
import base64
import cProfile
import os.path
import pstats
import time
import numpy as np
import logging
from functools import partial
from argos.collect.collector import Collector
from argos.config.abstractcti import AbstractCti
from argos.config.configtreemodel import ConfigTreeModel
from argos.config.configtreeview import ConfigWidget
from argos.info import DEBUGGING, PROJECT_NAME, PROFILING, EXIT_CODE_RESTART
from argos.inspector.abstract import AbstractInspector, UpdateReason
from argos.inspector.errormsg import ErrorMsgInspector
from argos.inspector.selectionpane import InspectorSelectionPane
from argos.qt import Qt, QUrl, QtCore, QtGui, QtWidgets, QtSignal, QtSlot
from argos.qt.misc import getWidgetGeom, getWidgetState
from argos.reg.basereg import nameToIdentifier
from argos.reg.dialog import PluginsDialog
from argos.repo.iconfactory import RtiIconFactory
from argos.repo.registry import RtiRegistry
from argos.repo.repotreeview import RepoWidget
from argos.repo.testdata import createArgosTestData
from argos.utils.cls import check_class, check_is_a_sequence
from argos.utils.dirs import argosConfigDirectory, argosLogDirectory
from argos.utils.misc import string_to_identifier
from argos.utils.moduleinfo import versionStrToTuple
from argos.widgets.aboutdialog import AboutDialog
from argos.widgets.constants import CENTRAL_MARGIN, CENTRAL_SPACING
from argos.widgets.misc import processEvents
            from argos.inspector.qtplugins.text import TextInspector
            from argos.inspector.pgplugins.imageplot2d import PgImagePlot2d

    def openInWebBrowser(self, url):
        """ Opens url or file in an external documentation.

            Regular URLs are opened in the web browser, Local URLs are opened in the application
            that is used to open that type of file by default.
        """
        try:
            logger.debug("Opening URL: {}".format(url))
            qUrl = QUrl(url)
            QtGui.QDesktopServices.openUrl(qUrl)
        except Exception as ex:
            msg = "Unable to open URL {}. \n\nDetails: {}".format(url, ex)
            QtWidgets.QMessageBox.warning(self, "Warning", msg)
            logger.error(msg.replace('\n', ' '))


    def openInExternalApp(self, fileName):
        """ Opens url or file in an external documentation.

            Regular URLs are opened in the web browser, Local URLs are opened in the application
            that is used to open that type of file by default.
        """
        try:
            logger.debug("Opening URL: {}".format(fileName))
            if not os.path.exists(fileName):
                raise OSError("File doesn't exist.")
            url = QUrl.fromLocalFile(fileName)
            QtGui.QDesktopServices.openUrl(url)
        except Exception as ex:
            msg = "Unable to open file '{}'. \n\nDetails: {}".format(fileName, ex)
            QtWidgets.QMessageBox.warning(self, "Warning", msg)
            logger.error(msg.strip('\n'))


    def marshall(self):
        """ Returns a dictionary to save in the persistent settings
        """
        self._storeInspectorState(self.inspectorRegItem, self.inspector)

        layoutCfg = dict(
            repoWidget = self.repoWidget.marshall(),
            configTreeHeaders =  self.configWidget.configTreeView.marshall(),
            collectorHeaders = self.collector.tree.marshall(),
            winGeom = base64.b64encode(getWidgetGeom(self)).decode('ascii'),
            winState = base64.b64encode(getWidgetState(self)).decode('ascii')
        )

        cfg = dict(
            configWidget = self.configWidget.marshall(),
            curInspector = self.inspectorRegItem.identifier if self.inspectorRegItem else '',
            inspectors = self._inspectorStates,
            layout = layoutCfg,
        )
        return cfg


    def unmarshall(self, cfg):
        """ Initializes itself from a config dict form the persistent settings.
        """
        self.configWidget.unmarshall(cfg.get('configWidget', {}))

        self._inspectorStates = cfg.get('inspectors', {})

        curInspector = cfg.get('curInspector')
        if curInspector:
            try:
                logger.debug("Setting inspector to: {}".format(curInspector))
                self.setInspectorById(curInspector)
            except KeyError as ex:
                logger.warning("No inspector with ID {!r}.: {}".format(curInspector, ex))

        layoutCfg = cfg.get('layout', {})

        self.repoWidget.unmarshall(layoutCfg.get('repoWidget', {}))
        self.configWidget.configTreeView.unmarshall(layoutCfg.get('configTreeHeaders', ''))
        self.collector.tree.unmarshall(layoutCfg.get('collectorHeaders', ''))

        if 'winGeom' in layoutCfg:
            self.restoreGeometry(base64.b64decode(layoutCfg['winGeom']))
        if 'winState' in layoutCfg:
            self.restoreState(base64.b64decode(layoutCfg['winState']))



    @QtSlot()
    def cloneWindow(self):
        """ Opens a new window with the same inspector as the current window.
        """
        newWindow = self.argosApplication.addNewMainWindow(
            cfg=self.marshall(), inspectorFullName=self.inspectorRegItem.name)

        # Select the current item in the new window.
        currentItem, _currentIndex = self.repoWidget.repoTreeView.getCurrentItem()
        if currentItem:
            newWindow.trySelectRtiByPath(currentItem.nodePath)

        # Move the new window 24 pixels to the bottom right and raise it to the front.
        newGeomRect = newWindow.geometry()
        logger.debug("newGeomRect: x={}".format(newGeomRect.x()))
        newGeomRect.moveTo(newGeomRect.x() + 24, newGeomRect.y() + 24)

        newWindow.setGeometry(newGeomRect)
        logger.debug("newGeomRect: x={}".format(newGeomRect.x()))

        newWindow.raise_()


    @QtSlot()
    def activateAndRaise(self):
        """ Activates and raises the window.
        """
        logger.debug("Activate and raising window: {}".format(self.windowNumber))
        self.activateWindow()
        self.raise_()


    def event(self, ev):
        """ Detects the WindowActivate event. Pass all event through to the super class.
        """

        if ev.type() == QtCore.QEvent.WindowActivate: