CuraContainerStack.py 19 KB

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