1# Copyright (c) 2018 Ultimaker B.V. 2# Cura is released under the terms of the LGPLv3 or higher. 3 4from typing import Any, cast, List, Optional, Dict 5from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject 6 7from UM.Application import Application 8from UM.Decorators import override 9from UM.FlameProfiler import pyqtSlot 10from UM.Logger import Logger 11from UM.Settings.ContainerStack import ContainerStack, InvalidContainerStackError 12from UM.Settings.InstanceContainer import InstanceContainer 13from UM.Settings.DefinitionContainer import DefinitionContainer 14from UM.Settings.ContainerRegistry import ContainerRegistry 15from UM.Settings.Interfaces import ContainerInterface, DefinitionContainerInterface 16from cura.Settings import cura_empty_instance_containers 17 18from . import Exceptions 19 20 21class CuraContainerStack(ContainerStack): 22 """Base class for Cura related stacks that want to enforce certain containers are available. 23 24 This class makes sure that the stack has the following containers set: user changes, quality 25 changes, quality, material, variant, definition changes and finally definition. Initially, 26 these will be equal to the empty instance container. 27 28 The container types are determined based on the following criteria: 29 - user: An InstanceContainer with the metadata entry "type" set to "user". 30 - quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes". 31 - quality: An InstanceContainer with the metadata entry "type" set to "quality". 32 - material: An InstanceContainer with the metadata entry "type" set to "material". 33 - variant: An InstanceContainer with the metadata entry "type" set to "variant". 34 - definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes". 35 - definition: A DefinitionContainer. 36 37 Internally, this class ensures the mentioned containers are always there and kept in a specific order. 38 This also means that operations on the stack that modifies the container ordering is prohibited and 39 will raise an exception. 40 """ 41 42 def __init__(self, container_id: str) -> None: 43 super().__init__(container_id) 44 45 self._empty_instance_container = cura_empty_instance_containers.empty_container #type: InstanceContainer 46 47 self._empty_quality_changes = cura_empty_instance_containers.empty_quality_changes_container #type: InstanceContainer 48 self._empty_quality = cura_empty_instance_containers.empty_quality_container #type: InstanceContainer 49 self._empty_material = cura_empty_instance_containers.empty_material_container #type: InstanceContainer 50 self._empty_variant = cura_empty_instance_containers.empty_variant_container #type: InstanceContainer 51 52 self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))] #type: List[ContainerInterface] 53 self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes 54 self._containers[_ContainerIndexes.Quality] = self._empty_quality 55 self._containers[_ContainerIndexes.Material] = self._empty_material 56 self._containers[_ContainerIndexes.Variant] = self._empty_variant 57 58 self.containersChanged.connect(self._onContainersChanged) 59 60 import cura.CuraApplication #Here to prevent circular imports. 61 self.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion) 62 63 self._settable_per_extruder_cache = {} # type: Dict[str, Any] 64 65 self.setDirty(False) 66 67 # This is emitted whenever the containersChanged signal from the ContainerStack base class is emitted. 68 pyqtContainersChanged = pyqtSignal() 69 70 def setUserChanges(self, new_user_changes: InstanceContainer) -> None: 71 """Set the user changes container. 72 73 :param new_user_changes: The new user changes container. It is expected to have a "type" metadata entry with the value "user". 74 """ 75 76 self.replaceContainer(_ContainerIndexes.UserChanges, new_user_changes) 77 78 @pyqtProperty(InstanceContainer, fset = setUserChanges, notify = pyqtContainersChanged) 79 def userChanges(self) -> InstanceContainer: 80 """Get the user changes container. 81 82 :return: The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. 83 """ 84 85 return cast(InstanceContainer, self._containers[_ContainerIndexes.UserChanges]) 86 87 def setQualityChanges(self, new_quality_changes: InstanceContainer, postpone_emit = False) -> None: 88 """Set the quality changes container. 89 90 :param new_quality_changes: The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes". 91 """ 92 93 self.replaceContainer(_ContainerIndexes.QualityChanges, new_quality_changes, postpone_emit = postpone_emit) 94 95 @pyqtProperty(InstanceContainer, fset = setQualityChanges, notify = pyqtContainersChanged) 96 def qualityChanges(self) -> InstanceContainer: 97 """Get the quality changes container. 98 99 :return: The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. 100 """ 101 102 return cast(InstanceContainer, self._containers[_ContainerIndexes.QualityChanges]) 103 104 def setIntent(self, new_intent: InstanceContainer, postpone_emit: bool = False) -> None: 105 """Set the intent container. 106 107 :param new_intent: The new intent container. It is expected to have a "type" metadata entry with the value "intent". 108 """ 109 110 self.replaceContainer(_ContainerIndexes.Intent, new_intent, postpone_emit = postpone_emit) 111 112 @pyqtProperty(InstanceContainer, fset = setIntent, notify = pyqtContainersChanged) 113 def intent(self) -> InstanceContainer: 114 """Get the quality container. 115 116 :return: The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer. 117 """ 118 119 return cast(InstanceContainer, self._containers[_ContainerIndexes.Intent]) 120 121 def setQuality(self, new_quality: InstanceContainer, postpone_emit: bool = False) -> None: 122 """Set the quality container. 123 124 :param new_quality: The new quality container. It is expected to have a "type" metadata entry with the value "quality". 125 """ 126 127 self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit) 128 129 @pyqtProperty(InstanceContainer, fset = setQuality, notify = pyqtContainersChanged) 130 def quality(self) -> InstanceContainer: 131 """Get the quality container. 132 133 :return: The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer. 134 """ 135 136 return cast(InstanceContainer, self._containers[_ContainerIndexes.Quality]) 137 138 def setMaterial(self, new_material: InstanceContainer, postpone_emit: bool = False) -> None: 139 """Set the material container. 140 141 :param new_material: The new material container. It is expected to have a "type" metadata entry with the value "material". 142 """ 143 144 self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit) 145 146 @pyqtProperty(InstanceContainer, fset = setMaterial, notify = pyqtContainersChanged) 147 def material(self) -> InstanceContainer: 148 """Get the material container. 149 150 :return: The material container. Should always be a valid container, but can be equal to the empty InstanceContainer. 151 """ 152 153 return cast(InstanceContainer, self._containers[_ContainerIndexes.Material]) 154 155 def setVariant(self, new_variant: InstanceContainer) -> None: 156 """Set the variant container. 157 158 :param new_variant: The new variant container. It is expected to have a "type" metadata entry with the value "variant". 159 """ 160 161 self.replaceContainer(_ContainerIndexes.Variant, new_variant) 162 163 @pyqtProperty(InstanceContainer, fset = setVariant, notify = pyqtContainersChanged) 164 def variant(self) -> InstanceContainer: 165 """Get the variant container. 166 167 :return: The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer. 168 """ 169 170 return cast(InstanceContainer, self._containers[_ContainerIndexes.Variant]) 171 172 def setDefinitionChanges(self, new_definition_changes: InstanceContainer) -> None: 173 """Set the definition changes container. 174 175 :param new_definition_changes: The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes". 176 """ 177 178 self.replaceContainer(_ContainerIndexes.DefinitionChanges, new_definition_changes) 179 180 @pyqtProperty(InstanceContainer, fset = setDefinitionChanges, notify = pyqtContainersChanged) 181 def definitionChanges(self) -> InstanceContainer: 182 """Get the definition changes container. 183 184 :return: The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. 185 """ 186 187 return cast(InstanceContainer, self._containers[_ContainerIndexes.DefinitionChanges]) 188 189 def setDefinition(self, new_definition: DefinitionContainerInterface) -> None: 190 """Set the definition container. 191 192 :param new_definition: The new definition container. It is expected to have a "type" metadata entry with the value "definition". 193 """ 194 195 self.replaceContainer(_ContainerIndexes.Definition, new_definition) 196 197 def getDefinition(self) -> "DefinitionContainer": 198 return cast(DefinitionContainer, self._containers[_ContainerIndexes.Definition]) 199 200 definition = pyqtProperty(QObject, fget = getDefinition, fset = setDefinition, notify = pyqtContainersChanged) 201 202 @override(ContainerStack) 203 def getBottom(self) -> "DefinitionContainer": 204 return self.definition 205 206 @override(ContainerStack) 207 def getTop(self) -> "InstanceContainer": 208 return self.userChanges 209 210 @pyqtSlot(str, result = bool) 211 def hasUserValue(self, key: str) -> bool: 212 """Check whether the specified setting has a 'user' value. 213 214 A user value here is defined as the setting having a value in either 215 the UserChanges or QualityChanges container. 216 217 :return: True if the setting has a user value, False if not. 218 """ 219 220 if self._containers[_ContainerIndexes.UserChanges].hasProperty(key, "value"): 221 return True 222 223 if self._containers[_ContainerIndexes.QualityChanges].hasProperty(key, "value"): 224 return True 225 226 return False 227 228 def setProperty(self, key: str, property_name: str, property_value: Any, container: "ContainerInterface" = None, set_from_cache: bool = False) -> None: 229 """Set a property of a setting. 230 231 This will set a property of a specified setting. Since the container stack does not contain 232 any settings itself, it is required to specify a container to set the property on. The target 233 container is matched by container type. 234 235 :param key: The key of the setting to set. 236 :param property_name: The name of the property to set. 237 :param new_value: The new value to set the property to. 238 """ 239 240 container_index = _ContainerIndexes.UserChanges 241 self._containers[container_index].setProperty(key, property_name, property_value, container, set_from_cache) 242 243 @override(ContainerStack) 244 def addContainer(self, container: ContainerInterface) -> None: 245 """Overridden from ContainerStack 246 247 Since we have a fixed order of containers in the stack and this method would modify the container 248 ordering, we disallow this operation. 249 """ 250 251 raise Exceptions.InvalidOperationError("Cannot add a container to Global stack") 252 253 @override(ContainerStack) 254 def insertContainer(self, index: int, container: ContainerInterface) -> None: 255 """Overridden from ContainerStack 256 257 Since we have a fixed order of containers in the stack and this method would modify the container 258 ordering, we disallow this operation. 259 """ 260 261 raise Exceptions.InvalidOperationError("Cannot insert a container into Global stack") 262 263 @override(ContainerStack) 264 def removeContainer(self, index: int = 0) -> None: 265 """Overridden from ContainerStack 266 267 Since we have a fixed order of containers in the stack and this method would modify the container 268 ordering, we disallow this operation. 269 """ 270 271 raise Exceptions.InvalidOperationError("Cannot remove a container from Global stack") 272 273 @override(ContainerStack) 274 def replaceContainer(self, index: int, container: ContainerInterface, postpone_emit: bool = False) -> None: 275 """Overridden from ContainerStack 276 277 Replaces the container at the specified index with another container. 278 This version performs checks to make sure the new container has the expected metadata and type. 279 280 :throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type. 281 """ 282 283 expected_type = _ContainerIndexes.IndexTypeMap[index] 284 if expected_type == "definition": 285 if not isinstance(container, DefinitionContainer): 286 raise Exceptions.InvalidContainerError("Cannot replace container at index {index} with a container that is not a DefinitionContainer".format(index = index)) 287 elif container != self._empty_instance_container and container.getMetaDataEntry("type") != expected_type: 288 raise Exceptions.InvalidContainerError("Cannot replace container at index {index} with a container that is not of {type} type, but {actual_type} type.".format(index = index, type = expected_type, actual_type = container.getMetaDataEntry("type"))) 289 290 current_container = self._containers[index] 291 if current_container.getId() == container.getId(): 292 return 293 294 super().replaceContainer(index, container, postpone_emit) 295 296 @override(ContainerStack) 297 def deserialize(self, serialized: str, file_name: Optional[str] = None) -> str: 298 """Overridden from ContainerStack 299 300 This deserialize will make sure the internal list of containers matches with what we expect. 301 It will first check to see if the container at a certain index already matches with what we 302 expect. If it does not, it will search for a matching container with the correct type. Should 303 no container with the correct type be found, it will use the empty container. 304 305 :raise InvalidContainerStackError: Raised when no definition can be found for the stack. 306 """ 307 308 # update the serialized data first 309 serialized = super().deserialize(serialized, file_name) 310 311 new_containers = self._containers.copy() 312 while len(new_containers) < len(_ContainerIndexes.IndexTypeMap): 313 new_containers.append(self._empty_instance_container) 314 315 # Validate and ensure the list of containers matches with what we expect 316 for index, type_name in _ContainerIndexes.IndexTypeMap.items(): 317 container = None 318 try: 319 container = new_containers[index] 320 except IndexError: 321 pass 322 323 if type_name == "definition": 324 if not container or not isinstance(container, DefinitionContainer): 325 definition = self.findContainer(container_type = DefinitionContainer) 326 if not definition: 327 raise InvalidContainerStackError("Stack {id} does not have a definition!".format(id = self.getId())) 328 329 new_containers[index] = definition 330 continue 331 332 if not container or container.getMetaDataEntry("type") != type_name: 333 actual_container = self.findContainer(type = type_name) 334 if actual_container: 335 new_containers[index] = actual_container 336 else: 337 new_containers[index] = self._empty_instance_container 338 339 self._containers = new_containers 340 341 # CURA-5281 342 # Some stacks can have empty definition_changes containers which will cause problems. 343 # Make sure that all stacks here have non-empty definition_changes containers. 344 if isinstance(new_containers[_ContainerIndexes.DefinitionChanges], type(self._empty_instance_container)): 345 from cura.Settings.CuraStackBuilder import CuraStackBuilder 346 CuraStackBuilder.createDefinitionChangesContainer(self, self.getId() + "_settings") 347 348 ## TODO; Deserialize the containers. 349 return serialized 350 351 def _onContainersChanged(self, container: Any) -> None: 352 """Helper to make sure we emit a PyQt signal on container changes.""" 353 354 Application.getInstance().callLater(self.pyqtContainersChanged.emit) 355 356 # Helper that can be overridden to get the "machine" definition, that is, the definition that defines the machine 357 # and its properties rather than, for example, the extruder. Defaults to simply returning the definition property. 358 def _getMachineDefinition(self) -> DefinitionContainer: 359 return self.definition 360 361 @classmethod 362 def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainerInterface) -> str: 363 """Find the ID that should be used when searching for instance containers for a specified definition. 364 365 This handles the situation where the definition specifies we should use a different definition when 366 searching for instance containers. 367 368 :param machine_definition: The definition to find the "quality definition" for. 369 370 :return: The ID of the definition container to use when searching for instance containers. 371 """ 372 373 quality_definition = machine_definition.getMetaDataEntry("quality_definition") 374 if not quality_definition: 375 return machine_definition.id #type: ignore 376 377 definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = quality_definition) 378 if not definitions: 379 Logger.log("w", "Unable to find parent definition {parent} for machine {machine}", parent = quality_definition, machine = machine_definition.id) #type: ignore 380 return machine_definition.id #type: ignore 381 382 return cls._findInstanceContainerDefinitionId(definitions[0]) 383 384 def getExtruderPositionValueWithDefault(self, key): 385 """getProperty for extruder positions, with translation from -1 to default extruder number""" 386 387 value = self.getProperty(key, "value") 388 if value == -1: 389 value = int(Application.getInstance().getMachineManager().defaultExtruderPosition) 390 return value 391 392 def getProperty(self, key: str, property_name: str, context = None) -> Any: 393 if property_name == "settable_per_extruder": 394 # Setable per extruder isn't a value that can ever change. So once we requested it once, we can just keep 395 # that in memory. 396 try: 397 return self._settable_per_extruder_cache[key] 398 except KeyError: 399 self._settable_per_extruder_cache[key] = super().getProperty(key, property_name, context) 400 return self._settable_per_extruder_cache[key] 401 402 return super().getProperty(key, property_name, context) 403 404 405class _ContainerIndexes: 406 """Private helper class to keep track of container positions and their types.""" 407 408 UserChanges = 0 409 QualityChanges = 1 410 Intent = 2 411 Quality = 3 412 Material = 4 413 Variant = 5 414 DefinitionChanges = 6 415 Definition = 7 416 417 # Simple hash map to map from index to "type" metadata entry 418 IndexTypeMap = { 419 UserChanges: "user", 420 QualityChanges: "quality_changes", 421 Intent: "intent", 422 Quality: "quality", 423 Material: "material", 424 Variant: "variant", 425 DefinitionChanges: "definition_changes", 426 Definition: "definition", 427 } 428 429 # Reverse lookup: type -> index 430 TypeIndexMap = dict([(v, k) for k, v in IndexTypeMap.items()]) 431