1# Copyright (c) 2018 Ultimaker B.V.
2# Cura is released under the terms of the LGPLv3 or higher.
3
4from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
5from typing import Iterable, TYPE_CHECKING
6
7from UM.i18n import i18nCatalog
8from UM.Qt.ListModel import ListModel
9from UM.Application import Application
10import UM.FlameProfiler
11
12if TYPE_CHECKING:
13    from cura.Settings.ExtruderStack import ExtruderStack  # To listen to changes on the extruders.
14
15catalog = i18nCatalog("cura")
16
17
18class ExtrudersModel(ListModel):
19    """Model that holds extruders.
20
21    This model is designed for use by any list of extruders, but specifically intended for drop-down lists of the
22    current machine's extruders in place of settings.
23    """
24
25    # The ID of the container stack for the extruder.
26    IdRole = Qt.UserRole + 1
27
28    NameRole = Qt.UserRole + 2
29    """Human-readable name of the extruder."""
30
31    ColorRole = Qt.UserRole + 3
32    """Colour of the material loaded in the extruder."""
33
34    IndexRole = Qt.UserRole + 4
35    """Index of the extruder, which is also the value of the setting itself.
36
37    An index of 0 indicates the first extruder, an index of 1 the second one, and so on. This is the value that will
38    be saved in instance containers. """
39
40    # The ID of the definition of the extruder.
41    DefinitionRole = Qt.UserRole + 5
42
43    # The material of the extruder.
44    MaterialRole = Qt.UserRole + 6
45
46    # The variant of the extruder.
47    VariantRole = Qt.UserRole + 7
48    StackRole = Qt.UserRole + 8
49
50    MaterialBrandRole = Qt.UserRole + 9
51    ColorNameRole = Qt.UserRole + 10
52
53    EnabledRole = Qt.UserRole + 11
54    """Is the extruder enabled?"""
55
56    defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
57    """List of colours to display if there is no material or the material has no known colour. """
58
59    def __init__(self, parent = None):
60        """Initialises the extruders model, defining the roles and listening for changes in the data.
61
62        :param parent: Parent QtObject of this list.
63        """
64
65        super().__init__(parent)
66
67        self.addRoleName(self.IdRole, "id")
68        self.addRoleName(self.NameRole, "name")
69        self.addRoleName(self.EnabledRole, "enabled")
70        self.addRoleName(self.ColorRole, "color")
71        self.addRoleName(self.IndexRole, "index")
72        self.addRoleName(self.DefinitionRole, "definition")
73        self.addRoleName(self.MaterialRole, "material")
74        self.addRoleName(self.VariantRole, "variant")
75        self.addRoleName(self.StackRole, "stack")
76        self.addRoleName(self.MaterialBrandRole, "material_brand")
77        self.addRoleName(self.ColorNameRole, "color_name")
78        self._update_extruder_timer = QTimer()
79        self._update_extruder_timer.setInterval(100)
80        self._update_extruder_timer.setSingleShot(True)
81        self._update_extruder_timer.timeout.connect(self.__updateExtruders)
82
83        self._active_machine_extruders = []  # type: Iterable[ExtruderStack]
84        self._add_optional_extruder = False
85
86        # Listen to changes
87        Application.getInstance().globalContainerStackChanged.connect(self._extrudersChanged)  # When the machine is swapped we must update the active machine extruders
88        Application.getInstance().getExtruderManager().extrudersChanged.connect(self._extrudersChanged)  # When the extruders change we must link to the stack-changed signal of the new extruder
89        Application.getInstance().getContainerRegistry().containerMetaDataChanged.connect(self._onExtruderStackContainersChanged)  # When meta data from a material container changes we must update
90        self._extrudersChanged()  # Also calls _updateExtruders
91
92    addOptionalExtruderChanged = pyqtSignal()
93
94    def setAddOptionalExtruder(self, add_optional_extruder):
95        if add_optional_extruder != self._add_optional_extruder:
96            self._add_optional_extruder = add_optional_extruder
97            self.addOptionalExtruderChanged.emit()
98            self._updateExtruders()
99
100    @pyqtProperty(bool, fset = setAddOptionalExtruder, notify = addOptionalExtruderChanged)
101    def addOptionalExtruder(self):
102        return self._add_optional_extruder
103
104    def _extrudersChanged(self, machine_id = None):
105        """Links to the stack-changed signal of the new extruders when an extruder is swapped out or added in the
106         current machine.
107
108        :param machine_id: The machine for which the extruders changed. This is filled by the
109        ExtruderManager.extrudersChanged signal when coming from that signal. Application.globalContainerStackChanged
110        doesn't fill this signal; it's assumed to be the current printer in that case.
111        """
112
113        machine_manager = Application.getInstance().getMachineManager()
114        if machine_id is not None:
115            if machine_manager.activeMachine is None:
116                # No machine, don't need to update the current machine's extruders
117                return
118            if machine_id != machine_manager.activeMachine.getId():
119                # Not the current machine
120                return
121
122        # Unlink from old extruders
123        for extruder in self._active_machine_extruders:
124            extruder.containersChanged.disconnect(self._onExtruderStackContainersChanged)
125            extruder.enabledChanged.disconnect(self._updateExtruders)
126
127        # Link to new extruders
128        self._active_machine_extruders = []
129        extruder_manager = Application.getInstance().getExtruderManager()
130        for extruder in extruder_manager.getActiveExtruderStacks():
131            if extruder is None: #This extruder wasn't loaded yet. This happens asynchronously while this model is constructed from QML.
132                continue
133            extruder.containersChanged.connect(self._onExtruderStackContainersChanged)
134            extruder.enabledChanged.connect(self._updateExtruders)
135            self._active_machine_extruders.append(extruder)
136
137        self._updateExtruders()  # Since the new extruders may have different properties, update our own model.
138
139    def _onExtruderStackContainersChanged(self, container):
140        # Update when there is an empty container or material or variant change
141        if container.getMetaDataEntry("type") in ["material", "variant", None]:
142            # The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name
143            self._updateExtruders()
144
145    modelChanged = pyqtSignal()
146
147    def _updateExtruders(self):
148        self._update_extruder_timer.start()
149
150    @UM.FlameProfiler.profile
151    def __updateExtruders(self):
152        """Update the list of extruders.
153
154        This should be called whenever the list of extruders changes.
155        """
156
157        extruders_changed = False
158
159        if self.count != 0:
160            extruders_changed = True
161
162        items = []
163
164        global_container_stack = Application.getInstance().getGlobalContainerStack()
165        if global_container_stack:
166
167            # get machine extruder count for verification
168            machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
169
170            for extruder in Application.getInstance().getExtruderManager().getActiveExtruderStacks():
171                position = extruder.getMetaDataEntry("position", default = "0")
172                try:
173                    position = int(position)
174                except ValueError:
175                    # Not a proper int.
176                    position = -1
177                if position >= machine_extruder_count:
178                    continue
179
180                default_color = self.defaultColors[position] if 0 <= position < len(self.defaultColors) else self.defaultColors[0]
181                color = extruder.material.getMetaDataEntry("color_code", default = default_color) if extruder.material else default_color
182                material_brand = extruder.material.getMetaDataEntry("brand", default = "generic")
183                color_name = extruder.material.getMetaDataEntry("color_name")
184                # construct an item with only the relevant information
185                item = {
186                    "id": extruder.getId(),
187                    "name": extruder.getName(),
188                    "enabled": extruder.isEnabled,
189                    "color": color,
190                    "index": position,
191                    "definition": extruder.getBottom().getId(),
192                    "material": extruder.material.getName() if extruder.material else "",
193                    "variant": extruder.variant.getName() if extruder.variant else "",  # e.g. print core
194                    "stack": extruder,
195                    "material_brand": material_brand,
196                    "color_name": color_name
197                }
198
199                items.append(item)
200                extruders_changed = True
201
202        if extruders_changed:
203            # sort by extruder index
204            items.sort(key = lambda i: i["index"])
205
206            # We need optional extruder to be last, so add it after we do sorting.
207            # This way we can simply interpret the -1 of the index as the last item (which it now always is)
208            if self._add_optional_extruder:
209                item = {
210                    "id": "",
211                    "name": catalog.i18nc("@menuitem", "Not overridden"),
212                    "enabled": True,
213                    "color": "#ffffff",
214                    "index": -1,
215                    "definition": "",
216                    "material": "",
217                    "variant": "",
218                    "stack": None,
219                    "material_brand": "",
220                    "color_name": "",
221                }
222                items.append(item)
223            if self._items != items:
224                self.setItems(items)
225                self.modelChanged.emit()
226