def __init__(self, parent=None, *args, **kwargs):
        super().__init__(parent=parent, *args, **kwargs)

        self._property_map = QQmlPropertyMap(self)

        self._stack_id = ""
        self._stack = None
        self._key = ""
        self._relations = set()
        self._watched_properties = []
        self._store_index = 0
        self._value_used = None
        self._stack_levels = []
        self._remove_unused_value = True
        self._validator = None
    def __init__(self, parent=None) -> None:
        super().__init__(parent=parent)

        self._property_map = QQmlPropertyMap(self)

        self._stack = None  # type: Optional[ContainerStack]
        self._key = ""
        self._relations = set()  # type: Set[str]
        self._watched_properties = []  # type: List[str]
        self._store_index = 0
        self._value_used = None  # type: Optional[bool]
        self._stack_levels = []  # type: List[int]
        self._remove_unused_value = True
        self._validator = None  # type: Optional[Validator]

        self._update_timer = QTimer(self)
        self._update_timer.setInterval(100)
        self._update_timer.setSingleShot(True)
        self._update_timer.timeout.connect(self._update)

        self.storeIndexChanged.connect(self._storeIndexChanged)
示例#3
0
    def __init__(self, parent=None, *args, **kwargs):
        super().__init__(parent=parent, *args, **kwargs)

        self._property_map = QQmlPropertyMap(self)

        self._stack = None
        self._key = ""
        self._relations = set()
        self._watched_properties = []
        self._store_index = 0
        self._value_used = None
        self._stack_levels = []
        self._remove_unused_value = True
        self._validator = None

        self._update_timer = QTimer()
        self._update_timer.setInterval(100)
        self._update_timer.setSingleShot(True)
        self._update_timer.timeout.connect(self._update)

        self.storeIndexChanged.connect(self._storeIndexChanged)
    def _update(self, container=None):
        if not self._stack or not self._watched_properties or not self._key:
            return

        self._updateStackLevels()
        relations = self._stack.getProperty(self._key, "relations")
        if relations:  # If the setting doesn't have the property relations, None is returned
            for relation in filter(
                    lambda r: r.type == RelationType.RequiredByTarget and r.
                    role == "value", relations):
                self._relations.add(relation.target.key)

        self._property_map = QQmlPropertyMap(self)

        for property_name in self._watched_properties:
            self._property_map.insert(property_name,
                                      self._getPropertyValue(property_name))

        self.propertiesChanged.emit()

        # Force update of value_used
        self._value_used = None
        self.isValueUsedChanged.emit()
    def __init__(self, parent = None) -> None:
        super().__init__(parent = parent)

        self._property_map = QQmlPropertyMap(self)

        self._stack = None  # type: Optional[ContainerStack]
        self._key = ""
        self._relations = set()  # type: Set[str]
        self._watched_properties = []  # type: List[str]
        self._store_index = 0
        self._value_used = None  # type: Optional[bool]
        self._stack_levels = []  # type: List[int]
        self._remove_unused_value = True
        self._validator = None  # type: Optional[Validator]

        self._update_timer = QTimer(self)
        self._update_timer.setInterval(100)
        self._update_timer.setSingleShot(True)
        self._update_timer.timeout.connect(self._update)

        self.storeIndexChanged.connect(self._storeIndexChanged)
示例#6
0
class SettingPropertyProvider(QObject):
    def __init__(self, parent=None, *args, **kwargs):
        super().__init__(parent=parent, *args, **kwargs)

        self._property_map = None

        self._stack_id = ""
        self._stack = None
        self._key = ""
        self._relations = set()
        self._watched_properties = []
        self._store_index = 0
        self._value_used = None
        self._stack_levels = []
        self._remove_unused_value = True
        self._validator = None

    ##  Set the containerStackId property.
    def setContainerStackId(self, stack_id):
        if stack_id == self._stack_id:
            return  # No change.

        self._stack_id = stack_id

        if self._stack:
            self._stack.propertiesChanged.disconnect(self._onPropertiesChanged)
            self._stack.containersChanged.disconnect(self._update)

        if self._stack_id:
            if self._stack_id == "global":
                self._stack = UM.Application.getInstance(
                ).getGlobalContainerStack()
            else:
                stacks = UM.Settings.ContainerRegistry.getInstance(
                ).findContainerStacks(id=self._stack_id)
                if stacks:
                    self._stack = stacks[0]

            if self._stack:
                self._stack.propertiesChanged.connect(
                    self._onPropertiesChanged)
                self._stack.containersChanged.connect(self._update)
        else:
            self._stack = None
        self._validator = None
        self._update()
        self.containerStackIdChanged.emit()

    ##  Emitted when the containerStackId property changes.
    containerStackIdChanged = pyqtSignal()
    ##  The ID of the container stack we should query for property values.
    @pyqtProperty(str,
                  fset=setContainerStackId,
                  notify=containerStackIdChanged)
    def containerStackId(self):
        return self._stack_id

    removeUnusedValueChanged = pyqtSignal()

    def setRemoveUnusedValue(self, remove_unused_value):
        self._remove_unused_value = remove_unused_value
        self.removeUnusedValueChanged.emit()

    @pyqtProperty(bool,
                  fset=setRemoveUnusedValue,
                  notify=removeUnusedValueChanged)
    def removeUnusedValue(self):
        return self._remove_unused_value

    ##  Set the watchedProperties property.
    def setWatchedProperties(self, properties):
        if properties != self._watched_properties:
            self._watched_properties = properties
            self._update()
            self.watchedPropertiesChanged.emit()

    ##  Emitted when the watchedProperties property changes.
    watchedPropertiesChanged = pyqtSignal()
    ##  A list of property names that should be watched for changes.
    @pyqtProperty("QVariantList",
                  fset=setWatchedProperties,
                  notify=watchedPropertiesChanged)
    def watchedProperties(self):
        return self._watched_properties

    ##  Set the key property.
    def setKey(self, key):
        if key != self._key:
            self._key = key
            self._validator = None
            self._update()
            self.keyChanged.emit()

    ##  Emitted when the key property changes.
    keyChanged = pyqtSignal()
    ##  The key of the setting that we should provide property values for.
    @pyqtProperty(str, fset=setKey, notify=keyChanged)
    def key(self):
        return self._key

    propertiesChanged = pyqtSignal()

    @pyqtProperty(QQmlPropertyMap, notify=propertiesChanged)
    def properties(self):
        return self._property_map

    @pyqtSlot()
    def forcePropertiesChanged(self):
        self._onPropertiesChanged(self._key, self._watched_properties)

    def setStoreIndex(self, index):
        if index != self._store_index:
            self._store_index = index
            self.storeIndexChanged.emit()

    storeIndexChanged = pyqtSignal()

    @pyqtProperty(int, fset=setStoreIndex, notify=storeIndexChanged)
    def storeIndex(self):
        return self._store_index

    stackLevelChanged = pyqtSignal()

    ##  At what levels in the stack does the value(s) for this setting occur?
    @pyqtProperty("QVariantList", notify=stackLevelChanged)
    def stackLevels(self):
        if not self._stack:
            return -1
        return self._stack_levels

    ##  Set the value of a property.
    #
    #   \param stack_index At which level in the stack should this property be set?
    #   \param property_name The name of the property to set.
    #   \param property_value The value of the property to set.
    @pyqtSlot(str, "QVariant")
    def setPropertyValue(self, property_name, property_value):
        if not self._stack or not self._key:
            return

        if property_name not in self._watched_properties:
            Logger.log("w",
                       "Tried to set a property that is not being watched")
            return

        container = self._stack.getContainer(self._store_index)
        if isinstance(container, UM.Settings.DefinitionContainer):
            return

        # In some cases we clean some stuff and the result is as when nothing as been changed manually.
        if property_name == "value" and self._remove_unused_value:
            for index in self._stack_levels:
                if index > self._store_index:
                    old_value = self.getPropertyValue(property_name, index)

                    key_state = str(
                        self._stack.getContainer(
                            self._store_index).getProperty(self._key, "state"))
                    # sometimes: old value is int, property_value is float
                    # (and the container is not removed, so the revert button appears)
                    if str(old_value) == str(
                            property_value
                    ) and key_state != "InstanceState.Calculated":
                        # If we change the setting so that it would be the same as a deeper setting, we can just remove
                        # the value. Note that we only do this when this is not caused by the calculated state
                        # In this case the setting does need to be set, as it needs to be stored in the user settings.
                        self.removeFromContainer(self._store_index)
                        return
                    else:  # First value that we encountered was different, stop looking & continue as normal.
                        break

        # _remove_unused_value is used when the stack value differs from the effective value
        # i.e. there is a resolve function
        if self._property_map.value(
                property_name) == property_value and self._remove_unused_value:
            return

        container.setProperty(self._key, property_name, property_value)

    ##  Manually request the value of a property.
    #   The most notable difference with the properties is that you have more control over at what point in the stack
    #   you want the setting to be retrieved (instead of always taking the top one)
    #   \param property_name The name of the property to get the value from.
    #   \param stack_level the index of the container to get the value from.
    @pyqtSlot(str, int, result="QVariant")
    def getPropertyValue(self, property_name, stack_level):
        try:
            # Because we continue to count if there are multiple linked stacks, we need to check what stack is targeted
            current_stack = self._stack
            while current_stack:
                num_containers = len(current_stack.getContainers())
                if stack_level >= num_containers:
                    stack_level -= num_containers
                    current_stack = self._stack.getNextStack()
                else:
                    break  # Found the right stack

            if not current_stack:
                Logger.log(
                    "w",
                    "Could not find the right stack for setting %s at stack level %d while trying to get property %s",
                    self._key, stack_level, property_name)
                return None

            value = current_stack.getContainers()[stack_level].getProperty(
                self._key, property_name)
        except IndexError:  # Requested stack level does not exist
            Logger.log(
                "w",
                "Tried to get property of type %s from %s but it did not exist on requested index %d",
                property_name, self._key, stack_level)
            return None
        return value

    @pyqtSlot(int)
    def removeFromContainer(self, index):
        current_stack = self._stack
        while current_stack:
            num_containers = len(current_stack.getContainers())
            if index >= num_containers:
                index -= num_containers
                current_stack = self._stack.getNextStack()
            else:
                break  # Found the right stack

        if not current_stack:
            Logger.log(
                "w",
                "Unable to remove instance from container because the right stack at stack level %d could not be found",
                stack_level)
            return

        container = current_stack.getContainer(index)
        if not container or not isinstance(container,
                                           UM.Settings.InstanceContainer):
            Logger.log(
                "w",
                "Unable to remove instance from container as it was either not found or not an instance container"
            )
            return

        container.removeInstance(self._key)

    isValueUsedChanged = pyqtSignal()

    @pyqtProperty(bool, notify=isValueUsedChanged)
    def isValueUsed(self):
        if self._value_used is not None:
            return self._value_used
        if not self._stack:
            return False
        definition = self._stack.getSettingDefinition(self._key)
        if not definition:
            return False

        relation_count = 0
        value_used_count = 0
        for key in self._relations:
            # If the setting is not a descendant of this setting, ignore it.
            if not definition.isDescendant(key):
                continue

            relation_count += 1

            if self._stack.getProperty(
                    key, "state") != UM.Settings.InstanceState.User:
                value_used_count += 1

            # If the setting has a formula the value is still used.
            if isinstance(self._stack.getRawProperty(key, "value"),
                          SettingFunction):
                value_used_count += 1

        self._value_used = relation_count == 0 or (relation_count > 0
                                                   and value_used_count != 0)
        return self._value_used

    # protected:

    def _onPropertiesChanged(self, key, property_names):
        if key != self._key:
            if key in self._relations:
                self._value_used = None
                self.isValueUsedChanged.emit()
            return

        for property_name in property_names:
            if property_name not in self._watched_properties:
                continue

            self._property_map.insert(property_name,
                                      self._getPropertyValue(property_name))

        self._updateStackLevels()

    def _update(self, container=None):
        if not self._stack or not self._watched_properties or not self._key:
            return

        self._updateStackLevels()
        relations = self._stack.getProperty(self._key, "relations")
        if relations:  # If the setting doesn't have the property relations, None is returned
            for relation in filter(
                    lambda r: r.type == UM.Settings.SettingRelation.
                    RelationType.RequiredByTarget and r.role == "value",
                    relations):
                self._relations.add(relation.target.key)

        self._property_map = QQmlPropertyMap(self)

        for property_name in self._watched_properties:
            self._property_map.insert(property_name,
                                      self._getPropertyValue(property_name))

        self.propertiesChanged.emit()

        # Force update of value_used
        self._value_used = None
        self.isValueUsedChanged.emit()

    def _updateStackLevels(self):
        levels = []
        # Start looking at the stack this provider is attached to.
        current_stack = self._stack
        index = 0
        while current_stack:
            for container in current_stack.getContainers():
                try:
                    if container.getProperty(self._key, "value") is not None:
                        levels.append(index)
                except AttributeError:
                    pass
                index += 1
            # If there is a next stack, check that one as well.
            current_stack = current_stack.getNextStack()
        if levels != self._stack_levels:
            self._stack_levels = levels
            self.stackLevelChanged.emit()

    def _getPropertyValue(self, property_name):
        property_value = self._stack.getProperty(self._key, property_name)
        if isinstance(property_value, UM.Settings.SettingFunction):
            property_value = property_value(self._stack)

        if property_name == "value":
            property_value = UM.Settings.SettingDefinition.settingValueToString(
                self._stack.getProperty(self._key, "type"), property_value)
        elif property_name == "validationState":
            # Setting is not validated. This can happen if there is only a setting definition.
            # We do need to validate it, because a setting defintions value can be set by a function, which could
            # be an invalid setting.
            if property_value is None:
                if not self._validator:
                    definition = self._stack.getSettingDefinition(self._key)
                    validator_type = UM.Settings.SettingDefinition.getValidatorForType(
                        definition.type)
                    if validator_type:
                        self._validator = validator_type(self._key)
                if self._validator:
                    property_value = self._validator(self._stack)
        return str(property_value)
class SettingPropertyProvider(QObject):
    def __init__(self, parent=None) -> None:
        super().__init__(parent=parent)

        self._property_map = QQmlPropertyMap(self)

        self._stack = None  # type: Optional[ContainerStack]
        self._key = ""
        self._relations = set()  # type: Set[str]
        self._watched_properties = []  # type: List[str]
        self._store_index = 0
        self._value_used = None  # type: Optional[bool]
        self._stack_levels = []  # type: List[int]
        self._remove_unused_value = True
        self._validator = None  # type: Optional[Validator]

        self._update_timer = QTimer(self)
        self._update_timer.setInterval(100)
        self._update_timer.setSingleShot(True)
        self._update_timer.timeout.connect(self._update)

        self.storeIndexChanged.connect(self._storeIndexChanged)

    def setContainerStack(self, stack: Optional[ContainerStack]) -> None:
        if self._stack == stack:
            return  # Nothing to do, attempting to set stack to the same value.

        if self._stack:
            self._stack.propertiesChanged.disconnect(self._onPropertiesChanged)
            self._stack.containersChanged.disconnect(self._containersChanged)

        self._stack = stack

        if self._stack:
            self._stack.propertiesChanged.connect(self._onPropertiesChanged)
            self._stack.containersChanged.connect(self._containersChanged)

        self._validator = None
        self._updateDelayed()
        self.containerStackChanged.emit()

    ##  Set the containerStackId property.
    def setContainerStackId(self, stack_id: str) -> None:
        if stack_id == self.containerStackId:
            return  # No change.

        if stack_id:
            if stack_id == "global":
                self.setContainerStack(
                    Application.getInstance().getGlobalContainerStack())
            else:
                stacks = ContainerRegistry.getInstance().findContainerStacks(
                    id=stack_id)
                if stacks:
                    self.setContainerStack(stacks[0])
        else:
            self.setContainerStack(None)

    ##  Emitted when the containerStackId property changes.
    containerStackIdChanged = pyqtSignal()

    ##  The ID of the container stack we should query for property values.
    @pyqtProperty(str,
                  fset=setContainerStackId,
                  notify=containerStackIdChanged)
    def containerStackId(self) -> str:
        if self._stack:
            return self._stack.id

        return ""

    containerStackChanged = pyqtSignal()

    @pyqtProperty(QObject,
                  fset=setContainerStack,
                  notify=containerStackChanged)
    def containerStack(self) -> Optional[ContainerInterface]:
        return self._stack

    removeUnusedValueChanged = pyqtSignal()

    def setRemoveUnusedValue(self, remove_unused_value: bool) -> None:
        if self._remove_unused_value != remove_unused_value:
            self._remove_unused_value = remove_unused_value
            self.removeUnusedValueChanged.emit()

    @pyqtProperty(bool,
                  fset=setRemoveUnusedValue,
                  notify=removeUnusedValueChanged)
    def removeUnusedValue(self) -> bool:
        return self._remove_unused_value

    ##  Set the watchedProperties property.
    def setWatchedProperties(self, properties: List[str]) -> None:
        if properties != self._watched_properties:
            self._watched_properties = properties
            self._updateDelayed()
            self.watchedPropertiesChanged.emit()

    ##  Emitted when the watchedProperties property changes.
    watchedPropertiesChanged = pyqtSignal()
    ##  A list of property names that should be watched for changes.
    @pyqtProperty("QStringList",
                  fset=setWatchedProperties,
                  notify=watchedPropertiesChanged)
    def watchedProperties(self) -> List[str]:
        return self._watched_properties

    ##  Set the key property.
    def setKey(self, key: str) -> None:
        if key != self._key:
            self._key = key
            self._validator = None
            self._updateDelayed()
            self.keyChanged.emit()

    ##  Emitted when the key property changes.
    keyChanged = pyqtSignal()
    ##  The key of the setting that we should provide property values for.
    @pyqtProperty(str, fset=setKey, notify=keyChanged)
    def key(self):
        return self._key

    propertiesChanged = pyqtSignal()

    @pyqtProperty(QQmlPropertyMap, notify=propertiesChanged)
    def properties(self):
        return self._property_map

    @pyqtSlot()
    def forcePropertiesChanged(self):
        self._onPropertiesChanged(self._key, self._watched_properties)

    def setStoreIndex(self, index):
        if index != self._store_index:
            self._store_index = index
            self.storeIndexChanged.emit()

    storeIndexChanged = pyqtSignal()

    @pyqtProperty(int, fset=setStoreIndex, notify=storeIndexChanged)
    def storeIndex(self):
        return self._store_index

    stackLevelChanged = pyqtSignal()

    ##  At what levels in the stack does the value(s) for this setting occur?
    @pyqtProperty("QVariantList", notify=stackLevelChanged)
    def stackLevels(self):
        if not self._stack:
            return [-1]
        return self._stack_levels

    ##  Set the value of a property.
    #
    #   \param stack_index At which level in the stack should this property be set?
    #   \param property_name The name of the property to set.
    #   \param property_value The value of the property to set.
    @pyqtSlot(str, "QVariant")
    def setPropertyValue(self, property_name, property_value):
        if not self._stack or not self._key:
            return

        if property_name not in self._watched_properties:
            Logger.log("w",
                       "Tried to set a property that is not being watched")
            return

        container = self._stack.getContainer(self._store_index)
        if isinstance(container, DefinitionContainer):
            return

        # In some cases we clean some stuff and the result is as when nothing as been changed manually.
        if property_name == "value" and self._remove_unused_value:
            for index in self._stack_levels:
                if index > self._store_index:
                    old_value = self.getPropertyValue(property_name, index)

                    key_state = str(
                        self._stack.getContainer(
                            self._store_index).getProperty(self._key, "state"))

                    # The old_value might be a SettingFunction, like round(), sum(), etc.
                    #  In this case retrieve the value to compare
                    if isinstance(old_value, SettingFunction):
                        old_value = old_value(self._stack)

                    # sometimes: old value is int, property_value is float
                    # (and the container is not removed, so the revert button appears)
                    if str(old_value) == str(
                            property_value
                    ) and key_state != "InstanceState.Calculated":
                        # If we change the setting so that it would be the same as a deeper setting, we can just remove
                        # the value. Note that we only do this when this is not caused by the calculated state
                        # In this case the setting does need to be set, as it needs to be stored in the user settings.
                        self.removeFromContainer(self._store_index)
                        return
                    else:  # First value that we encountered was different, stop looking & continue as normal.
                        break

        # _remove_unused_value is used when the stack value differs from the effective value
        # i.e. there is a resolve function
        if self._property_map.value(
                property_name) == property_value and self._remove_unused_value:
            return

        container.setProperty(self._key, property_name, property_value)

    ##  Manually request the value of a property.
    #   The most notable difference with the properties is that you have more control over at what point in the stack
    #   you want the setting to be retrieved (instead of always taking the top one)
    #   \param property_name The name of the property to get the value from.
    #   \param stack_level the index of the container to get the value from.
    @pyqtSlot(str, int, result="QVariant")
    def getPropertyValue(self, property_name: str, stack_level: int) -> Any:
        try:
            # Because we continue to count if there are multiple linked stacks, we need to check what stack is targeted
            current_stack = self._stack
            while current_stack:
                num_containers = len(current_stack.getContainers())
                if stack_level >= num_containers:
                    stack_level -= num_containers
                    current_stack = current_stack.getNextStack()
                else:
                    break  # Found the right stack

            if not current_stack:
                Logger.log(
                    "w",
                    "Could not find the right stack for setting %s at stack level %d while trying to get property %s",
                    self._key, stack_level, property_name)
                return None

            value = current_stack.getContainers()[stack_level].getProperty(
                self._key, property_name)
        except IndexError:  # Requested stack level does not exist
            Logger.log(
                "w",
                "Tried to get property of type %s from %s but it did not exist on requested index %d",
                property_name, self._key, stack_level)
            return None
        return value

    @pyqtSlot(str, result=str)
    def getPropertyValueAsString(self, property_name: str) -> str:
        return self._getPropertyValue(property_name)

    @pyqtSlot(int)
    def removeFromContainer(self, index: int) -> None:
        current_stack = self._stack
        while current_stack:
            num_containers = len(current_stack.getContainers())
            if index >= num_containers:
                index -= num_containers
                current_stack = current_stack.getNextStack()
            else:
                break  # Found the right stack

        if not current_stack:
            Logger.log(
                "w",
                "Unable to remove instance from container because the right stack at stack level %d could not be found",
                index)
            return

        container = current_stack.getContainer(index)
        if not container or not isinstance(container, InstanceContainer):
            Logger.log(
                "w",
                "Unable to remove instance from container as it was either not found or not an instance container"
            )
            return

        container.removeInstance(self._key)

    isValueUsedChanged = pyqtSignal()

    @pyqtProperty(bool, notify=isValueUsedChanged)
    def isValueUsed(self) -> bool:
        if self._value_used is not None:
            return self._value_used
        if not self._stack:
            return False
        definition = self._stack.getSettingDefinition(self._key)
        if not definition:
            return False

        relation_count = 0
        value_used_count = 0
        for key in self._relations:
            # If the setting is not a descendant of this setting, ignore it.
            if not definition.isDescendant(key):
                continue

            relation_count += 1

            if self._stack.getProperty(key, "state") != InstanceState.User:
                value_used_count += 1
                break

            # If the setting has a formula the value is still used.
            if isinstance(self._stack.getRawProperty(key, "value"),
                          SettingFunction):
                value_used_count += 1
                break

        self._value_used = relation_count == 0 or (relation_count > 0
                                                   and value_used_count != 0)
        return self._value_used

    def _onPropertiesChanged(self, key: str,
                             property_names: List[str]) -> None:
        if key != self._key:
            if key in self._relations:
                self._value_used = None
                try:
                    self.isValueUsedChanged.emit()
                except RuntimeError:
                    # QtObject has been destroyed, no need to handle the signals anymore.
                    # This can happen when the QtObject in C++ has been destroyed, but the python object hasn't quite
                    # caught on yet. Once we call any signals, it will cause a runtimeError since all the underlying
                    # logic to emit pyqtSignals is gone.
                    return
            return

        has_values_changed = False
        for property_name in property_names:
            if property_name not in self._watched_properties:
                continue

            has_values_changed = True
            try:
                self._property_map.insert(
                    property_name, self._getPropertyValue(property_name))
            except RuntimeError:
                # QtObject has been destroyed, no need to handle the signals anymore.
                # This can happen when the QtObject in C++ has been destroyed, but the python object hasn't quite
                # caught on yet. Once we call any signals, it will cause a runtimeError since all the underlying
                # logic to emit pyqtSignals is gone.
                return

        self._updateStackLevels()
        if has_values_changed:
            try:
                self.propertiesChanged.emit()
            except RuntimeError:
                # QtObject has been destroyed, no need to handle the signals anymore.
                # This can happen when the QtObject in C++ has been destroyed, but the python object hasn't quite
                # caught on yet. Once we call any signals, it will cause a runtimeError since all the underlying
                # logic to emit pyqtSignals is gone.
                return

    def _update(self, container=None):
        if not self._stack or not self._watched_properties or not self._key:
            return

        self._updateStackLevels()
        relations = self._stack.getProperty(self._key, "relations")
        if relations:  # If the setting doesn't have the property relations, None is returned
            for relation in filter(
                    lambda r: r.type == RelationType.RequiredByTarget and r.
                    role == "value", relations):
                self._relations.add(relation.target.key)

        for property_name in self._watched_properties:
            self._property_map.insert(property_name,
                                      self._getPropertyValue(property_name))

        # Force update of value_used
        self._value_used = None
        self.isValueUsedChanged.emit()

    def _updateDelayed(self, container=None) -> None:
        try:
            self._update_timer.start()
        except RuntimeError:
            # Sometimes the python object is not yet deleted, but the wrapped part is already gone.
            # In that case there is nothing else to do but ignore this.
            pass

    def _containersChanged(self, container=None):
        self._updateDelayed(container=container)

    def _storeIndexChanged(self, container=None):
        self._updateDelayed(container=container)

    ##  Updates the self._stack_levels field, which indicates at which levels in
    #   the stack the property is set.
    def _updateStackLevels(self) -> None:
        levels = []
        # Start looking at the stack this provider is attached to.
        current_stack = self._stack
        index = 0
        while current_stack:
            for container in current_stack.getContainers():
                try:
                    if container.getProperty(self._key, "value") is not None:
                        levels.append(index)
                except AttributeError:
                    pass
                index += 1
            # If there is a next stack, check that one as well.
            current_stack = current_stack.getNextStack()

        if levels != self._stack_levels:
            self._stack_levels = levels
            self.stackLevelChanged.emit()

    def _getPropertyValue(self, property_name):
        # Use the evaluation context to skip certain containers
        context = PropertyEvaluationContext(self._stack)
        context.context["evaluate_from_container_index"] = self._store_index

        property_value = self._stack.getProperty(self._key,
                                                 property_name,
                                                 context=context)
        if isinstance(property_value, SettingFunction):
            property_value = property_value(self._stack)

        if property_name == "value":
            setting_type = self._stack.getProperty(self._key, "type")
            if setting_type is not None:
                property_value = SettingDefinition.settingValueToString(
                    setting_type, property_value)
            else:
                property_value = ""
        elif property_name == "validationState":
            # Setting is not validated. This can happen if there is only a setting definition.
            # We do need to validate it, because a setting defintions value can be set by a function, which could
            # be an invalid setting.
            if property_value is None:
                if not self._validator:
                    definition = self._stack.getSettingDefinition(self._key)
                    if definition:
                        validator_type = SettingDefinition.getValidatorForType(
                            definition.type)
                        if validator_type:
                            self._validator = validator_type(self._key)
                if self._validator:
                    property_value = self._validator(self._stack)
        return str(property_value)
class SettingPropertyProvider(QObject):
    def __init__(self, parent = None) -> None:
        super().__init__(parent = parent)

        self._property_map = QQmlPropertyMap(self)

        self._stack = None  # type: Optional[ContainerStack]
        self._key = ""
        self._relations = set()  # type: Set[str]
        self._watched_properties = []  # type: List[str]
        self._store_index = 0
        self._value_used = None  # type: Optional[bool]
        self._stack_levels = []  # type: List[int]
        self._remove_unused_value = True
        self._validator = None  # type: Optional[Validator]

        self._update_timer = QTimer(self)
        self._update_timer.setInterval(100)
        self._update_timer.setSingleShot(True)
        self._update_timer.timeout.connect(self._update)

        self.storeIndexChanged.connect(self._storeIndexChanged)

    def setContainerStack(self, stack: Optional[ContainerStack]) -> None:
        if self._stack == stack:
            return  # Nothing to do, attempting to set stack to the same value.

        if self._stack:
            self._stack.propertiesChanged.disconnect(self._onPropertiesChanged)
            self._stack.containersChanged.disconnect(self._containersChanged)

        self._stack = stack

        if self._stack:
            self._stack.propertiesChanged.connect(self._onPropertiesChanged)
            self._stack.containersChanged.connect(self._containersChanged)

        self._validator = None
        self._update()
        self.containerStackChanged.emit()

    ##  Set the containerStackId property.
    def setContainerStackId(self, stack_id: str) -> None:
        if stack_id == self.containerStackId:
            return  # No change.

        if stack_id:
            if stack_id == "global":
                self.setContainerStack(Application.getInstance().getGlobalContainerStack())
            else:
                stacks = ContainerRegistry.getInstance().findContainerStacks(id = stack_id)
                if stacks:
                    self.setContainerStack(stacks[0])
        else:
            self.setContainerStack(None)

    ##  Emitted when the containerStackId property changes.
    containerStackIdChanged = pyqtSignal()

    ##  The ID of the container stack we should query for property values.
    @pyqtProperty(str, fset = setContainerStackId, notify = containerStackIdChanged)
    def containerStackId(self) -> str:
        if self._stack:
            return self._stack.id

        return ""

    containerStackChanged = pyqtSignal()

    @pyqtProperty(QObject, fset=setContainerStack, notify=containerStackChanged)
    def containerStack(self) -> Optional[ContainerInterface]:
        return self._stack

    removeUnusedValueChanged = pyqtSignal()

    def setRemoveUnusedValue(self, remove_unused_value: bool) -> None:
        if self._remove_unused_value != remove_unused_value:
            self._remove_unused_value = remove_unused_value
            self.removeUnusedValueChanged.emit()

    @pyqtProperty(bool, fset = setRemoveUnusedValue, notify = removeUnusedValueChanged)
    def removeUnusedValue(self) -> bool:
        return self._remove_unused_value

    ##  Set the watchedProperties property.
    def setWatchedProperties(self, properties: List[str]) -> None:
        if properties != self._watched_properties:
            self._watched_properties = properties
            self._update()
            self.watchedPropertiesChanged.emit()

    ##  Emitted when the watchedProperties property changes.
    watchedPropertiesChanged = pyqtSignal()
    ##  A list of property names that should be watched for changes.
    @pyqtProperty("QStringList", fset = setWatchedProperties, notify = watchedPropertiesChanged)
    def watchedProperties(self) -> List[str]:
        return self._watched_properties

    ##  Set the key property.
    def setKey(self, key: str) -> None:
        if key != self._key:
            self._key = key
            self._validator = None
            self._update()
            self.keyChanged.emit()

    ##  Emitted when the key property changes.
    keyChanged = pyqtSignal()
    ##  The key of the setting that we should provide property values for.
    @pyqtProperty(str, fset = setKey, notify = keyChanged)
    def key(self):
        return self._key

    propertiesChanged = pyqtSignal()
    @pyqtProperty(QQmlPropertyMap, notify = propertiesChanged)
    def properties(self):
        return self._property_map

    @pyqtSlot()
    def forcePropertiesChanged(self):
        self._onPropertiesChanged(self._key, self._watched_properties)

    def setStoreIndex(self, index):
        if index != self._store_index:
            self._store_index = index
            self.storeIndexChanged.emit()

    storeIndexChanged = pyqtSignal()
    @pyqtProperty(int, fset = setStoreIndex, notify = storeIndexChanged)
    def storeIndex(self):
        return self._store_index

    stackLevelChanged = pyqtSignal()

    ##  At what levels in the stack does the value(s) for this setting occur?
    @pyqtProperty("QVariantList", notify = stackLevelChanged)
    def stackLevels(self):
        if not self._stack:
            return [-1]
        return self._stack_levels

    ##  Set the value of a property.
    #
    #   \param stack_index At which level in the stack should this property be set?
    #   \param property_name The name of the property to set.
    #   \param property_value The value of the property to set.
    @pyqtSlot(str, "QVariant")
    def setPropertyValue(self, property_name, property_value):
        if not self._stack or not self._key:
            return

        if property_name not in self._watched_properties:
            Logger.log("w", "Tried to set a property that is not being watched")
            return

        container = self._stack.getContainer(self._store_index)
        if isinstance(container, DefinitionContainer):
            return

        # In some cases we clean some stuff and the result is as when nothing as been changed manually.
        if property_name == "value" and self._remove_unused_value:
            for index in self._stack_levels:
                if index > self._store_index:
                    old_value = self.getPropertyValue(property_name, index)

                    key_state = str(self._stack.getContainer(self._store_index).getProperty(self._key, "state"))

                    # The old_value might be a SettingFunction, like round(), sum(), etc.
                    #  In this case retrieve the value to compare
                    if isinstance(old_value, SettingFunction):
                        old_value = old_value(self._stack)

                    # sometimes: old value is int, property_value is float
                    # (and the container is not removed, so the revert button appears)
                    if str(old_value) == str(property_value) and key_state != "InstanceState.Calculated":
                        # If we change the setting so that it would be the same as a deeper setting, we can just remove
                        # the value. Note that we only do this when this is not caused by the calculated state
                        # In this case the setting does need to be set, as it needs to be stored in the user settings.
                        self.removeFromContainer(self._store_index)
                        return
                    else:  # First value that we encountered was different, stop looking & continue as normal.
                        break

        # _remove_unused_value is used when the stack value differs from the effective value
        # i.e. there is a resolve function
        if self._property_map.value(property_name) == property_value and self._remove_unused_value:
            return

        container.setProperty(self._key, property_name, property_value)

    ##  Manually request the value of a property.
    #   The most notable difference with the properties is that you have more control over at what point in the stack
    #   you want the setting to be retrieved (instead of always taking the top one)
    #   \param property_name The name of the property to get the value from.
    #   \param stack_level the index of the container to get the value from.
    @pyqtSlot(str, int, result = "QVariant")
    def getPropertyValue(self, property_name: str, stack_level: int) -> Any:
        try:
            # Because we continue to count if there are multiple linked stacks, we need to check what stack is targeted
            current_stack = self._stack
            while current_stack:
                num_containers = len(current_stack.getContainers())
                if stack_level >= num_containers:
                    stack_level -= num_containers
                    current_stack = current_stack.getNextStack()
                else:
                    break  # Found the right stack

            if not current_stack:
                Logger.log("w", "Could not find the right stack for setting %s at stack level %d while trying to get property %s", self._key, stack_level, property_name)
                return None

            value = current_stack.getContainers()[stack_level].getProperty(self._key, property_name)
        except IndexError:  # Requested stack level does not exist
            Logger.log("w", "Tried to get property of type %s from %s but it did not exist on requested index %d", property_name, self._key, stack_level)
            return None
        return value

    @pyqtSlot(int)
    def removeFromContainer(self, index: int) -> None:
        current_stack = self._stack
        while current_stack:
            num_containers = len(current_stack.getContainers())
            if index >= num_containers:
                index -= num_containers
                current_stack = current_stack.getNextStack()
            else:
                break  # Found the right stack

        if not current_stack:
            Logger.log("w", "Unable to remove instance from container because the right stack at stack level %d could not be found", index)
            return

        container = current_stack.getContainer(index)
        if not container or not isinstance(container, InstanceContainer):
            Logger.log("w", "Unable to remove instance from container as it was either not found or not an instance container")
            return

        container.removeInstance(self._key)

    isValueUsedChanged = pyqtSignal()
    @pyqtProperty(bool, notify = isValueUsedChanged)
    def isValueUsed(self) -> bool:
        if self._value_used is not None:
            return self._value_used
        if not self._stack:
            return False
        definition = self._stack.getSettingDefinition(self._key)
        if not definition:
            return False

        relation_count = 0
        value_used_count = 0
        for key in self._relations:
            # If the setting is not a descendant of this setting, ignore it.
            if not definition.isDescendant(key):
                continue

            relation_count += 1

            if self._stack.getProperty(key, "state") != InstanceState.User:
                value_used_count += 1
                break

            # If the setting has a formula the value is still used.
            if isinstance(self._stack.getRawProperty(key, "value"), SettingFunction):
                value_used_count += 1
                break

        self._value_used = relation_count == 0 or (relation_count > 0 and value_used_count != 0)
        return self._value_used

    def _onPropertiesChanged(self, key: str, property_names: List[str]) -> None:
        if key != self._key:
            if key in self._relations:
                self._value_used = None
                try:
                    self.isValueUsedChanged.emit()
                except RuntimeError:
                    # QtObject has been destroyed, no need to handle the signals anymore.
                    # This can happen when the QtObject in C++ has been destroyed, but the python object hasn't quite
                    # caught on yet. Once we call any signals, it will cause a runtimeError since all the underlying
                    # logic to emit pyqtSignals is gone.
                    return
            return

        has_values_changed = False
        for property_name in property_names:
            if property_name not in self._watched_properties:
                continue

            has_values_changed = True
            try:
                self._property_map.insert(property_name, self._getPropertyValue(property_name))
            except RuntimeError:
                # QtObject has been destroyed, no need to handle the signals anymore.
                # This can happen when the QtObject in C++ has been destroyed, but the python object hasn't quite
                # caught on yet. Once we call any signals, it will cause a runtimeError since all the underlying
                # logic to emit pyqtSignals is gone.
                return

        self._updateStackLevels()
        if has_values_changed:
            try:
                self.propertiesChanged.emit()
            except RuntimeError:
                # QtObject has been destroyed, no need to handle the signals anymore.
                # This can happen when the QtObject in C++ has been destroyed, but the python object hasn't quite
                # caught on yet. Once we call any signals, it will cause a runtimeError since all the underlying
                # logic to emit pyqtSignals is gone.
                return

    def _update(self, container = None):
        if not self._stack or not self._watched_properties or not self._key:
            return

        self._updateStackLevels()
        relations = self._stack.getProperty(self._key, "relations")
        if relations:  # If the setting doesn't have the property relations, None is returned
            for relation in filter(lambda r: r.type == RelationType.RequiredByTarget and r.role == "value", relations):
                self._relations.add(relation.target.key)

        for property_name in self._watched_properties:
            self._property_map.insert(property_name, self._getPropertyValue(property_name))

        # Force update of value_used
        self._value_used = None
        self.isValueUsedChanged.emit()

    def _updateDelayed(self, container = None):
        self._update_timer.start()

    def _containersChanged(self, container = None):
        self._updateDelayed(container = container)

    def _storeIndexChanged(self, container = None):
        self._updateDelayed(container = container)

    ##  Updates the self._stack_levels field, which indicates at which levels in
    #   the stack the property is set.
    def _updateStackLevels(self) -> None:
        levels = []
        # Start looking at the stack this provider is attached to.
        current_stack = self._stack
        index = 0
        while current_stack:
            for container in current_stack.getContainers():
                try:
                    if container.getProperty(self._key, "value") is not None:
                        levels.append(index)
                except AttributeError:
                    pass
                index += 1
            # If there is a next stack, check that one as well.
            current_stack = current_stack.getNextStack()

        if levels != self._stack_levels:
            self._stack_levels = levels
            self.stackLevelChanged.emit()

    def _getPropertyValue(self, property_name):
        # Use the evaluation context to skip certain containers
        context = PropertyEvaluationContext(self._stack)
        context.context["evaluate_from_container_index"] = self._store_index

        property_value = self._stack.getProperty(self._key, property_name, context = context)
        if isinstance(property_value, SettingFunction):
            property_value = property_value(self._stack)

        if property_name == "value":
            setting_type = self._stack.getProperty(self._key, "type")
            if setting_type is not None:
                property_value = SettingDefinition.settingValueToString(setting_type, property_value)
            else:
                property_value = ""
        elif property_name == "validationState":
            # Setting is not validated. This can happen if there is only a setting definition.
            # We do need to validate it, because a setting defintions value can be set by a function, which could
            # be an invalid setting.
            if property_value is None:
                if not self._validator:
                    definition = self._stack.getSettingDefinition(self._key)
                    if definition:
                        validator_type = SettingDefinition.getValidatorForType(definition.type)
                        if validator_type:
                            self._validator = validator_type(self._key)
                if self._validator:
                    property_value = self._validator(self._stack)
        return str(property_value)