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