1# Copyright (c) 2019 Ultimaker B.V.
2# Cura is released under the terms of the LGPLv3 or higher.
3
4from UM.Job import Job  # For our background task of loading MachineNodes lazily.
5from UM.JobQueue import JobQueue  # For our background task of loading MachineNodes lazily.
6from UM.Logger import Logger
7from UM.Settings.ContainerRegistry import ContainerRegistry  # To listen to containers being added.
8from UM.Signal import Signal
9import cura.CuraApplication  # Imported like this to prevent circular dependencies.
10from cura.Machines.MachineNode import MachineNode
11from cura.Settings.GlobalStack import GlobalStack  # To listen only to global stacks being added.
12
13from typing import Dict, List, Optional, TYPE_CHECKING
14import time
15
16if TYPE_CHECKING:
17    from cura.Machines.QualityGroup import QualityGroup
18    from cura.Machines.QualityChangesGroup import QualityChangesGroup
19    from UM.Settings.ContainerStack import ContainerStack
20
21
22class ContainerTree:
23    """This class contains a look-up tree for which containers are available at which stages of configuration.
24
25    The tree starts at the machine definitions. For every distinct definition there will be one machine node here.
26
27    All of the fallbacks for material choices, quality choices, etc. should be encoded in this tree. There must
28    always be at least one child node (for nodes that have children) but that child node may be a node representing
29    the empty instance container.
30    """
31
32    __instance = None  # type: Optional["ContainerTree"]
33
34    @classmethod
35    def getInstance(cls):
36        if cls.__instance is None:
37            cls.__instance = ContainerTree()
38        return cls.__instance
39
40    def __init__(self) -> None:
41        self.machines = self._MachineNodeMap()  # Mapping from definition ID to machine nodes with lazy loading.
42        self.materialsChanged = Signal()  # Emitted when any of the material nodes in the tree got changed.
43        cura.CuraApplication.CuraApplication.getInstance().initializationFinished.connect(self._onStartupFinished)  # Start the background task to load more machine nodes after start-up is completed.
44
45    def getCurrentQualityGroups(self) -> Dict[str, "QualityGroup"]:
46        """Get the quality groups available for the currently activated printer.
47
48        This contains all quality groups, enabled or disabled. To check whether the quality group can be activated,
49        test for the ``QualityGroup.is_available`` property.
50
51        :return: For every quality type, one quality group.
52        """
53
54        global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
55        if global_stack is None:
56            return {}
57        variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList]
58        material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList]
59        extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
60        return self.machines[global_stack.definition.getId()].getQualityGroups(variant_names, material_bases, extruder_enabled)
61
62    def getCurrentQualityChangesGroups(self) -> List["QualityChangesGroup"]:
63        """Get the quality changes groups available for the currently activated printer.
64
65        This contains all quality changes groups, enabled or disabled. To check whether the quality changes group can
66        be activated, test for the ``QualityChangesGroup.is_available`` property.
67
68        :return: A list of all quality changes groups.
69        """
70
71        global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
72        if global_stack is None:
73            return []
74        variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList]
75        material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList]
76        extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
77        return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
78
79    def _onStartupFinished(self) -> None:
80        """Ran after completely starting up the application."""
81
82        currently_added = ContainerRegistry.getInstance().findContainerStacks()  # Find all currently added global stacks.
83        JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added))
84
85    class _MachineNodeMap:
86        """Dictionary-like object that contains the machines.
87
88        This handles the lazy loading of MachineNodes.
89        """
90
91        def __init__(self) -> None:
92            self._machines = {}  # type: Dict[str, MachineNode]
93
94        def __contains__(self, definition_id: str) -> bool:
95            """Returns whether a printer with a certain definition ID exists.
96
97            This is regardless of whether or not the printer is loaded yet.
98
99            :param definition_id: The definition to look for.
100
101            :return: Whether or not a printer definition exists with that name.
102            """
103
104            return len(ContainerRegistry.getInstance().findContainersMetadata(id = definition_id)) > 0
105
106        def __getitem__(self, definition_id: str) -> MachineNode:
107            """Returns a machine node for the specified definition ID.
108
109            If the machine node wasn't loaded yet, this will load it lazily.
110
111            :param definition_id: The definition to look for.
112
113            :return: A machine node for that definition.
114            """
115
116            if definition_id not in self._machines:
117                start_time = time.time()
118                self._machines[definition_id] = MachineNode(definition_id)
119                self._machines[definition_id].materialsChanged.connect(ContainerTree.getInstance().materialsChanged)
120                Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - start_time))
121            return self._machines[definition_id]
122
123        def get(self, definition_id: str, default: Optional[MachineNode] = None) -> Optional[MachineNode]:
124            """Gets a machine node for the specified definition ID, with default.
125
126            The default is returned if there is no definition with the specified ID. If the machine node wasn't
127            loaded yet, this will load it lazily.
128
129            :param definition_id: The definition to look for.
130            :param default: The machine node to return if there is no machine with that definition (can be ``None``
131            optionally or if not provided).
132
133            :return: A machine node for that definition, or the default if there is no definition with the provided
134            definition_id.
135            """
136
137            if definition_id not in self:
138                return default
139            return self[definition_id]
140
141        def is_loaded(self, definition_id: str) -> bool:
142            """Returns whether we've already cached this definition's node.
143
144            :param definition_id: The definition that we may have cached.
145
146            :return: ``True`` if it's cached.
147            """
148
149            return definition_id in self._machines
150
151    class _MachineNodeLoadJob(Job):
152        """Pre-loads all currently added printers as a background task so that switching printers in the interface is
153        faster.
154        """
155
156        def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]) -> None:
157            """Creates a new background task.
158
159            :param tree_root: The container tree instance. This cannot be obtained through the singleton static
160            function since the instance may not yet be constructed completely.
161            :param container_stacks: All of the stacks to pre-load the container trees for. This needs to be provided
162            from here because the stacks need to be constructed on the main thread because they are QObject.
163            """
164
165            self.tree_root = tree_root
166            self.container_stacks = container_stacks
167            super().__init__()
168
169        def run(self) -> None:
170            """Starts the background task.
171
172            The ``JobQueue`` will schedule this on a different thread.
173            """
174            Logger.log("d", "Started background loading of MachineNodes")
175            for stack in self.container_stacks:  # Load all currently-added containers.
176                if not isinstance(stack, GlobalStack):
177                    continue
178                # Allow a thread switch after every container.
179                # Experimentally, sleep(0) didn't allow switching. sleep(0.1) or sleep(0.2) neither.
180                # We're in no hurry though. Half a second is fine.
181                time.sleep(0.5)
182                definition_id = stack.definition.getId()
183                if not self.tree_root.machines.is_loaded(definition_id):
184                    _ = self.tree_root.machines[definition_id]
185            Logger.log("d", "All MachineNode loading completed")