1# Copyright (c) 2019 Ultimaker B.V.
2# Cura is released under the terms of the LGPLv3 or higher.
3
4from typing import Optional, TYPE_CHECKING
5
6from PyQt5.QtCore import pyqtProperty
7
8import UM.i18n
9from UM.FlameProfiler import pyqtSlot
10from UM.Settings.ContainerRegistry import ContainerRegistry
11from UM.Settings.DefinitionContainer import DefinitionContainer
12from UM.Util import parseBool
13
14import cura.CuraApplication  # Imported like this to prevent circular dependencies.
15from cura.MachineAction import MachineAction
16from cura.Machines.ContainerTree import ContainerTree  # To re-build the machine node when hasMaterials changes.
17from cura.Settings.CuraStackBuilder import CuraStackBuilder
18from cura.Settings.cura_empty_instance_containers import isEmptyContainer
19
20if TYPE_CHECKING:
21    from PyQt5.QtCore import QObject
22
23catalog = UM.i18n.i18nCatalog("cura")
24
25
26class MachineSettingsAction(MachineAction):
27    """This action allows for certain settings that are "machine only") to be modified.
28
29    It automatically detects machine definitions that it knows how to change and attaches itself to those.
30    """
31    def __init__(self, parent: Optional["QObject"] = None) -> None:
32        super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
33        self._qml_url = "MachineSettingsAction.qml"
34
35        from cura.CuraApplication import CuraApplication
36        self._application = CuraApplication.getInstance()
37
38        from cura.Settings.CuraContainerStack import _ContainerIndexes
39        self._store_container_index = _ContainerIndexes.DefinitionChanges
40
41        self._container_registry = ContainerRegistry.getInstance()
42        self._container_registry.containerAdded.connect(self._onContainerAdded)
43
44        # The machine settings dialog blocks auto-slicing when it's shown, and re-enables it when it's finished.
45        self._backend = self._application.getBackend()
46        self.onFinished.connect(self._onFinished)
47
48        # If the g-code flavour changes between UltiGCode and another flavour, we need to update the container tree.
49        self._application.globalContainerStackChanged.connect(self._updateHasMaterialsInContainerTree)
50
51    # Which container index in a stack to store machine setting changes.
52    @pyqtProperty(int, constant = True)
53    def storeContainerIndex(self) -> int:
54        return self._store_container_index
55
56    def _onContainerAdded(self, container):
57        # Add this action as a supported action to all machine definitions
58        if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
59            self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
60
61    def _updateHasMaterialsInContainerTree(self) -> None:
62        """Triggered when the global container stack changes or when the g-code
63
64        flavour setting is changed.
65        """
66        global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
67        if global_stack is None:
68            return
69        machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
70
71        if machine_node.has_materials != parseBool(global_stack.getMetaDataEntry("has_materials")):  # May have changed due to the g-code flavour.
72            machine_node.has_materials = parseBool(global_stack.getMetaDataEntry("has_materials"))
73            machine_node._loadAll()
74
75    def _reset(self):
76        global_stack = self._application.getMachineManager().activeMachine
77        if not global_stack:
78            return
79
80        # Make sure there is a definition_changes container to store the machine settings
81        definition_changes_id = global_stack.definitionChanges.getId()
82        if isEmptyContainer(definition_changes_id):
83            CuraStackBuilder.createDefinitionChangesContainer(global_stack,
84                                                              global_stack.getName() + "_settings")
85
86        # Disable auto-slicing while the MachineAction is showing
87        if self._backend:  # This sometimes triggers before backend is loaded.
88            self._backend.disableTimer()
89
90    def _onFinished(self):
91        # Restore auto-slicing when the machine action is dismissed
92        if self._backend and self._backend.determineAutoSlicing():
93            self._backend.enableTimer()
94            self._backend.tickle()
95
96    @pyqtSlot(int)
97    def setMachineExtruderCount(self, extruder_count: int) -> None:
98        # Note: this method was in this class before, but since it's quite generic and other plugins also need it
99        # it was moved to the machine manager instead. Now this method just calls the machine manager.
100        self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count)
101
102    @pyqtSlot()
103    def forceUpdate(self) -> None:
104        # Force rebuilding the build volume by reloading the global container stack.
105        # This is a bit of a hack, but it seems quick enough.
106        self._application.getMachineManager().globalContainerChanged.emit()
107        self._application.getMachineManager().forceUpdateAllSettings()
108
109    @pyqtSlot()
110    def updateHasMaterialsMetadata(self) -> None:
111        global_stack = self._application.getMachineManager().activeMachine
112
113        # Updates the has_materials metadata flag after switching gcode flavor
114        if not global_stack:
115            return
116
117        definition = global_stack.getDefinition()
118        if definition.getProperty("machine_gcode_flavor", "value") != "UltiGCode" or parseBool(definition.getMetaDataEntry("has_materials", False)):
119            # In other words: only continue for the UM2 (extended), but not for the UM2+
120            return
121
122        machine_manager = self._application.getMachineManager()
123        has_materials = global_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
124
125        if has_materials:
126            global_stack.setMetaDataEntry("has_materials", True)
127        else:
128            # The metadata entry is stored in an ini, and ini files are parsed as strings only.
129            # Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
130            if "has_materials" in global_stack.getMetaData():
131                global_stack.removeMetaDataEntry("has_materials")
132
133        self._updateHasMaterialsInContainerTree()
134
135        # set materials
136        machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
137        for position, extruder in enumerate(global_stack.extruderList):
138            #Find out what material we need to default to.
139            approximate_diameter = round(extruder.getProperty("material_diameter", "value"))
140            material_node = machine_node.variants[extruder.variant.getName()].preferredMaterial(approximate_diameter)
141            machine_manager.setMaterial(str(position), material_node)
142
143        self._application.globalContainerStackChanged.emit()
144
145    @pyqtSlot(int)
146    def updateMaterialForDiameter(self, extruder_position: int) -> None:
147        # Updates the material container to a material that matches the material diameter set for the printer
148        self._application.getMachineManager().updateMaterialWithVariant(str(extruder_position))
149