Browse Source

Merge branch 'feature_intent_container_tree' of github.com:Ultimaker/Cura into feature_intent_interface

Jaime van Kessel 5 years ago
parent
commit
b00b8c8c58

+ 18 - 4
cura/CuraApplication.py

@@ -85,6 +85,7 @@ from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachine
 from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
 from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel
 from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
+from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel
 from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
 from cura.Machines.Models.NozzleModel import NozzleModel
 from cura.Machines.Models.QualityManagementModel import QualityManagementModel
@@ -220,7 +221,9 @@ class CuraApplication(QtApplication):
         self._cura_scene_controller = None
         self._machine_error_checker = None
 
-        self._machine_settings_manager = MachineSettingsManager(self, parent = self)
+        self._machine_settings_manager = MachineSettingsManager(self, parent=self)
+        self._material_management_model = MaterialManagementModel()
+        self._quality_management_model = None
 
         self._discovered_printer_model = DiscoveredPrintersModel(self, parent = self)
         self._first_start_machine_actions_model = FirstStartMachineActionsModel(self, parent = self)
@@ -918,12 +921,12 @@ class CuraApplication(QtApplication):
 
     # Can't deprecate this function since the deprecation marker collides with pyqtSlot!
     @pyqtSlot(result = QObject)
-    def getMaterialManager(self, *args) -> "MaterialManager":
+    def getMaterialManager(self, *args) -> cura.Machines.MaterialManager.MaterialManager:
         return cura.Machines.MaterialManager.MaterialManager.getInstance()
 
     # Can't deprecate this function since the deprecation marker collides with pyqtSlot!
     @pyqtSlot(result = QObject)
-    def getQualityManager(self, *args) -> "QualityManager":
+    def getQualityManager(self, *args) -> cura.Machines.QualityManager.QualityManager:
         return cura.Machines.QualityManager.QualityManager.getInstance()
 
     def getIntentManager(self, *args) -> IntentManager:
@@ -975,6 +978,16 @@ class CuraApplication(QtApplication):
     def getMachineActionManager(self, *args):
         return self._machine_action_manager
 
+    @pyqtSlot(result = QObject)
+    def getMaterialManagementModel(self):
+        return self._material_management_model
+
+    @pyqtSlot(result=QObject)
+    def getQualityManagementModel(self):
+        if not self._quality_management_model:
+            self._quality_management_model = QualityManagementModel()
+        return self._quality_management_model
+
     def getSimpleModeSettingsManager(self, *args):
         if self._simple_mode_settings_manager is None:
             self._simple_mode_settings_manager = SimpleModeSettingsManager()
@@ -1053,7 +1066,8 @@ class CuraApplication(QtApplication):
         qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
         qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
         qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
-        qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
+        qmlRegisterSingletonType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel", self.getQualityManagementModel)
+        qmlRegisterSingletonType(MaterialManagementModel, "Cura", 1, 5, "MaterialManagementModel", self.getMaterialManagementModel)
 
         qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel")
 

+ 2 - 2
cura/Machines/IntentNode.py

@@ -4,6 +4,7 @@
 from typing import TYPE_CHECKING
 
 from UM.Settings.ContainerRegistry import ContainerRegistry
+
 from cura.Machines.ContainerNode import ContainerNode
 
 if TYPE_CHECKING:
@@ -16,5 +17,4 @@ class IntentNode(ContainerNode):
     def __init__(self, container_id: str, quality: "QualityNode") -> None:
         super().__init__(container_id)
         self.quality = quality
-        my_metadata = ContainerRegistry.getInstance().findContainersMetadata(id=container_id)[0]
-        self.intent_category = my_metadata.get("intent_category", "default")
+        self.intent_category = ContainerRegistry.getInstance().findContainersMetadata(id = container_id)[0].get("intent_category", "default")

+ 7 - 4
cura/Machines/MachineNode.py

@@ -36,7 +36,7 @@ class MachineNode(ContainerNode):
         self.has_variants = parseBool(my_metadata.get("has_variants", "false"))
         self.has_machine_materials = parseBool(my_metadata.get("has_machine_materials", "false"))
         self.has_machine_quality = parseBool(my_metadata.get("has_machine_quality", "false"))
-        self.quality_definition = my_metadata.get("quality_definition", container_id)
+        self.quality_definition = my_metadata.get("quality_definition", container_id) if self.has_machine_quality else "fdmprinter"
         self.exclude_materials = my_metadata.get("exclude_materials", [])
         self.preferred_variant_name = my_metadata.get("preferred_variant_name", "")
         self.preferred_material = my_metadata.get("preferred_material", "")
@@ -62,7 +62,7 @@ class MachineNode(ContainerNode):
             Logger.log("e", "The number of extruders in the list of variants (" + str(len(variant_names)) + ") is not equal to the number of extruders in the list of materials (" + str(len(material_bases)) + ") or the list of enabled extruders (" + str(len(extruder_enabled)) + ").")
             return {}
         # For each extruder, find which quality profiles are available. Later we'll intersect the quality types.
-        qualities_per_type_per_extruder = [{} for _ in range(len(variant_names))]  # type: List[Dict[str, QualityNode]]
+        qualities_per_type_per_extruder = [{}] * len(variant_names)  # type: List[Dict[str, QualityNode]]
         for extruder_nr, variant_name in enumerate(variant_names):
             if not extruder_enabled[extruder_nr]:
                 continue  # No qualities are available in this extruder. It'll get skipped when calculating the available quality types.
@@ -77,6 +77,9 @@ class MachineNode(ContainerNode):
         # Create the quality group for each available type.
         quality_groups = {}
         for quality_type, global_quality_node in self.global_qualities.items():
+            if not global_quality_node.container:
+                Logger.log("w", "Node {0} doesn't have a container.".format(global_quality_node.container_id))
+                continue
             quality_groups[quality_type] = QualityGroup(name = global_quality_node.container.getMetaDataEntry("name", "Unnamed profile"), quality_type = quality_type)
             quality_groups[quality_type].node_for_global = global_quality_node
             for extruder, qualities_per_type in enumerate(qualities_per_type_per_extruder):
@@ -116,7 +119,7 @@ class MachineNode(ContainerNode):
     def getQualityChangesGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> List[QualityChangesGroup]:
         machine_quality_changes = ContainerRegistry.getInstance().findContainersMetadata(type = "quality_changes", definition = self.quality_definition)  # All quality changes for each extruder.
 
-        groups_by_name = {}  # Group quality changes profiles by their display name. The display name must be unique for quality changes. This finds profiles that belong together in a group.
+        groups_by_name = {}  #type: Dict[str, QualityChangesGroup]  # Group quality changes profiles by their display name. The display name must be unique for quality changes. This finds profiles that belong together in a group.
         for quality_changes in machine_quality_changes:
             name = quality_changes["name"]
             if name not in groups_by_name:
@@ -143,7 +146,7 @@ class MachineNode(ContainerNode):
     #   quality is taken.
     #   If there are no global qualities, an empty quality is returned.
     def preferredGlobalQuality(self) -> QualityNode:
-        return self.global_qualities.get(self.preferred_quality_type, next(iter(self.global_qualities)))
+        return self.global_qualities.get(self.preferred_quality_type, next(iter(self.global_qualities.values())))
 
     ##  (Re)loads all variants under this printer.
     def _loadAll(self):

+ 20 - 11
cura/Machines/MaterialManager.py

@@ -1,5 +1,6 @@
 # Copyright (c) 2019 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
+
 from collections import defaultdict
 import copy
 import uuid
@@ -7,19 +8,16 @@ from typing import Dict, Optional, TYPE_CHECKING, Any, List, cast
 
 from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
 
-from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
 from UM.Decorators import deprecated
 from UM.Logger import Logger
 from UM.Settings.ContainerRegistry import ContainerRegistry
-from UM.Settings.SettingFunction import SettingFunction
 from UM.Util import parseBool
-import cura.CuraApplication #Imported like this to prevent circular imports.
+import cura.CuraApplication  # Imported like this to prevent circular imports.
 from cura.Machines.ContainerTree import ContainerTree
 from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
 
 from .MaterialNode import MaterialNode
 from .MaterialGroup import MaterialGroup
-from .VariantType import VariantType
 
 if TYPE_CHECKING:
     from UM.Settings.DefinitionContainer import DefinitionContainer
@@ -134,7 +132,7 @@ class MaterialManager(QObject):
         # Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
         materials = self.getAvailableMaterials(machine.definition.getId(), nozzle_name)
         compatible_material_diameter = str(round(extruder_stack.getCompatibleMaterialDiameter()))
-        result = {key: material for key, material in materials.items() if material.container.getMetaDataEntry("approximate_diameter") == compatible_material_diameter}
+        result = {key: material for key, material in materials.items() if material.container and material.container.getMetaDataEntry("approximate_diameter") == compatible_material_diameter}
         return result
 
     #
@@ -248,7 +246,7 @@ class MaterialManager(QObject):
 
     def removeMaterialByRootId(self, root_material_id: str):
         container_registry = CuraContainerRegistry.getInstance()
-        results = container_registry.findContainers(id=root_material_id)
+        results = container_registry.findContainers(id = root_material_id)
         if not results:
             container_registry.addWrongContainerId(root_material_id)
 
@@ -260,8 +258,8 @@ class MaterialManager(QObject):
         # Check if the material is active in any extruder train. In that case, the material shouldn't be removed!
         # In the future we might enable this again, but right now, it's causing a ton of issues if we do (since it
         # corrupts the configuration)
-        root_material_id = material_node.container.getMetaDataEntry("base_file")
-        ids_to_remove = [metadata.get("id", "") for metadata in CuraContainerRegistry.getInstance().findInstanceContainersMetadata(base_file=root_material_id)]
+        root_material_id = material_node.base_file
+        ids_to_remove = {metadata.get("id", "") for metadata in CuraContainerRegistry.getInstance().findInstanceContainersMetadata(base_file = root_material_id)}
 
         for extruder_stack in CuraContainerRegistry.getInstance().findContainerStacks(type = "extruder_train"):
             if extruder_stack.material.getId() in ids_to_remove:
@@ -270,6 +268,8 @@ class MaterialManager(QObject):
 
     @pyqtSlot("QVariant", str)
     def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
+        if material_node.container is None:
+            return
         root_material_id = material_node.container.getMetaDataEntry("base_file")
         if root_material_id is None:
             return
@@ -281,6 +281,8 @@ class MaterialManager(QObject):
 
     @pyqtSlot("QVariant")
     def removeMaterial(self, material_node: "MaterialNode") -> None:
+        if material_node.container is None:
+            return
         root_material_id = material_node.container.getMetaDataEntry("base_file")
         if root_material_id is not None:
             self.removeMaterialByRootId(root_material_id)
@@ -300,7 +302,6 @@ class MaterialManager(QObject):
 
         # Create a new ID & container to hold the data.
         new_containers = []
-        container_registry = CuraContainerRegistry.getInstance()
         if new_base_id is None:
             new_base_id = container_registry.uniqueName(base_container.getId())
         new_base_container = copy.deepcopy(base_container)
@@ -336,15 +337,19 @@ class MaterialManager(QObject):
 
         # if the duplicated material was favorite then the new material should also be added to favorite.
         if root_material_id in self.getFavorites():
-            self.addFavorite(new_base_id)
+            cura.CuraApplication.CuraApplication.getInstance().getMaterialManagementModel().addFavorite(new_base_id)
 
         return new_base_id
+
     #
     # Creates a duplicate of a material, which has the same GUID and base_file metadata.
     # Returns the root material ID of the duplicated material if successful.
     #
     @pyqtSlot("QVariant", result = str)
     def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
+        if material_node.container is None:
+            Logger.log("e", "Material node {0} doesn't have container.".format(material_node.container_id))
+            return "ERROR"
         root_material_id = cast(str, material_node.container.getMetaDataEntry("base_file", ""))
         return self.duplicateMaterialByRootId(root_material_id, new_base_id, new_metadata)
 
@@ -361,7 +366,11 @@ class MaterialManager(QObject):
         machine_manager = application.getMachineManager()
         extruder_stack = machine_manager.activeStack
 
-        machine_definition = application.getGlobalContainerStack().definition
+        global_stack = application.getGlobalContainerStack()
+        if global_stack is None:
+            Logger.log("e", "Global stack not present!")
+            return "ERROR"
+        machine_definition = global_stack.definition
         root_material_id = machine_definition.getMetaDataEntry("preferred_material", default = "generic_pla")
 
         approximate_diameter = str(extruder_stack.approximateMaterialDiameter)

+ 9 - 8
cura/Machines/MaterialNode.py

@@ -62,15 +62,16 @@ class MaterialNode(ContainerNode):
             qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter")
         else:
             # Need to find the qualities that specify a material profile with the same material type.
-            my_material_type = self.material_type
-            qualities = []
-            qualities_any_material = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.variant.machine.quality_definition, variant = self.variant.variant_name)
-            for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", material = my_material_type):
-                qualities.extend((quality for quality in qualities_any_material if quality["material"] == material_metadata["id"]))
-            if not qualities:  # No quality profiles found. Go by GUID then.
-                my_guid = self.guid
-                for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", guid = my_guid):
+            qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.variant.machine.quality_definition, variant = self.variant.variant_name, material = self.container_id)  # First try by exact material ID.
+            if not qualities:
+                my_material_type = self.material_type
+                qualities_any_material = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.variant.machine.quality_definition, variant = self.variant.variant_name)
+                for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", material = my_material_type):
                     qualities.extend((quality for quality in qualities_any_material if quality["material"] == material_metadata["id"]))
+                if not qualities:  # No quality profiles found. Go by GUID then.
+                    my_guid = self.guid
+                    for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", guid = my_guid):
+                        qualities.extend((quality for quality in qualities_any_material if quality["material"] == material_metadata["id"]))
 
         for quality in qualities:
             quality_id = quality["id"]

+ 10 - 7
cura/Machines/Models/BaseMaterialsModel.py

@@ -31,8 +31,13 @@ class BaseMaterialsModel(ListModel):
         self._container_registry = self._application.getInstance().getContainerRegistry()
         self._machine_manager = self._application.getMachineManager()
 
+        self._extruder_position = 0
+        self._extruder_stack = None
+        self._enabled = True
+
         # Update the stack and the model data when the machine changes
         self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
+        self._updateExtruderStack()
 
         # Update this model when switching machines, when adding materials or changing their metadata.
         self._machine_manager.activeStackChanged.connect(self._update)
@@ -55,12 +60,8 @@ class BaseMaterialsModel(ListModel):
         self.addRoleName(Qt.UserRole + 15, "container_node")
         self.addRoleName(Qt.UserRole + 16, "is_favorite")
 
-        self._extruder_position = 0
-        self._extruder_stack = None
-
         self._available_materials = None  # type: Optional[Dict[str, MaterialNode]]
         self._favorite_ids = set()  # type: Set[str]
-        self._enabled = True
 
     def _updateExtruderStack(self):
         global_stack = self._machine_manager.activeMachine
@@ -95,7 +96,7 @@ class BaseMaterialsModel(ListModel):
                 self._update()
             self.enabledChanged.emit()
 
-    @pyqtProperty(bool, fset=setEnabled, notify=enabledChanged)
+    @pyqtProperty(bool, fset = setEnabled, notify = enabledChanged)
     def enabled(self):
         return self._enabled
 
@@ -107,7 +108,10 @@ class BaseMaterialsModel(ListModel):
             return
         if material.variant.container_id != self._extruder_stack.variant.getId():
             return
-        if material.variant.machine.container_id != cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack().definition.getId():
+        global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
+        if not global_stack:
+            return
+        if material.variant.machine.container_id != global_stack.definition.getId():
             return
         self._update()
 
@@ -167,4 +171,3 @@ class BaseMaterialsModel(ListModel):
             "is_favorite":          root_material_id in self._favorite_ids
         }
         return item
-

+ 1 - 5
cura/Machines/Models/CustomQualityProfilesDropDownMenuModel.py

@@ -19,11 +19,7 @@ class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel):
             Logger.log("d", "No active GlobalStack, set %s as empty.", self.__class__.__name__)
             return
 
-        variant_names = [extruder.variant.getName() for extruder in active_global_stack.extruders.values()]
-        material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in active_global_stack.extruders.values()]
-        extruder_enabled = [extruder.isEnabled for extruder in active_global_stack.extruders.values()]
-        machine_node = ContainerTree.getInstance().machines[active_global_stack.definition.getId()]
-        quality_changes_list = machine_node.getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
+        quality_changes_list = ContainerTree.getInstance().getCurrentQualityChangesGroups()
 
         item_list = []
         for quality_changes_group in sorted(quality_changes_list, key = lambda qgc: qgc.name.lower()):

+ 2 - 0
cura/Machines/Models/IntentModel.py

@@ -33,6 +33,8 @@ class IntentModel(ListModel):
 
         self._intent_category = "engineering"
 
+        machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
+        machine_manager.globalContainerChanged.connect(self._update)
         ContainerRegistry.getInstance().containerAdded.connect(self._onChanged)
         ContainerRegistry.getInstance().containerRemoved.connect(self._onChanged)
         self._layer_height_unit = ""  # This is cached

+ 169 - 0
cura/Machines/Models/MaterialManagementModel.py

@@ -0,0 +1,169 @@
+# Copyright (c) 2019 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+import copy  # To duplicate materials.
+from PyQt5.QtCore import QObject, pyqtSlot  # To allow the preference page proxy to be used from the actual preferences page.
+from typing import Any, Dict, Optional, TYPE_CHECKING
+import uuid  # To generate new GUIDs for new materials.
+
+from UM.i18n import i18nCatalog
+from UM.Logger import Logger
+
+import cura.CuraApplication  # Imported like this to prevent circular imports.
+from cura.Machines.ContainerTree import ContainerTree
+from cura.Settings.CuraContainerRegistry import CuraContainerRegistry  # To find the sets of materials belonging to each other, and currently loaded extruder stacks.
+
+if TYPE_CHECKING:
+    from cura.Machines.MaterialNode import MaterialNode
+
+catalog = i18nCatalog("cura")
+
+##  Proxy class to the materials page in the preferences.
+#
+#   This class handles the actions in that page, such as creating new materials,
+#   renaming them, etc.
+class MaterialManagementModel(QObject):
+    ##  Can a certain material be deleted, or is it still in use in one of the
+    #   container stacks anywhere?
+    #
+    #   We forbid the user from deleting a material if it's in use in any stack.
+    #   Deleting it while it's in use can lead to corrupted stacks. In the
+    #   future we might enable this functionality again (deleting the material
+    #   from those stacks) but for now it is easier to prevent the user from
+    #   doing this.
+    #   \param material_node The ContainerTree node of the material to check.
+    #   \return Whether or not the material can be removed.
+    @pyqtSlot("QVariant", result = bool)
+    def canMaterialBeRemoved(self, material_node: "MaterialNode"):
+        container_registry = CuraContainerRegistry.getInstance()
+        ids_to_remove = {metadata.get("id", "") for metadata in container_registry.findInstanceContainersMetadata(base_file = material_node.base_file)}
+        for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"):
+            if extruder_stack.material.getId() in ids_to_remove:
+                return False
+        return True
+
+    ##  Change the user-visible name of a material.
+    #   \param material_node The ContainerTree node of the material to rename.
+    #   \param name The new name for the material.
+    @pyqtSlot("QVariant", str)
+    def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
+        container_registry = CuraContainerRegistry.getInstance()
+        root_material_id = material_node.base_file
+        if container_registry.isReadOnly(root_material_id):
+            Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
+            return
+        return container_registry.findContainers(id = root_material_id)[0].setName(name)
+
+    ##  Deletes a material from Cura.
+    #
+    #   This function does not do any safety checking any more. Please call this
+    #   function only if:
+    #   - The material is not read-only.
+    #   - The material is not used in any stacks.
+    #   If the material was not lazy-loaded yet, this will fully load the
+    #   container. When removing this material node, all other materials with
+    #   the same base fill will also be removed.
+    #   \param material_node The material to remove.
+    @pyqtSlot("QVariant")
+    def removeMaterial(self, material_node: "MaterialNode") -> None:
+        container_registry = CuraContainerRegistry.getInstance()
+        materials_this_base_file = container_registry.findContainersMetadata(base_file = material_node.base_file)
+        for material_metadata in materials_this_base_file:
+            container_registry.removeContainer(material_metadata["id"])
+
+    ##  Creates a duplicate of a material with the same GUID and base_file
+    #   metadata.
+    #   \param material_node The node representing the material to duplicate.
+    #   \param new_base_id A new material ID for the base material. The IDs of
+    #   the submaterials will be based off this one. If not provided, a material
+    #   ID will be generated automatically.
+    #   \param new_metadata Metadata for the new material. If not provided, this
+    #   will be duplicated from the original material.
+    #   \return The root material ID of the duplicate material.
+    @pyqtSlot("QVariant", result = str)
+    def duplicateMaterial(self, material_node: "MaterialNode", new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
+        container_registry = CuraContainerRegistry.getInstance()
+
+        root_materials = container_registry.findContainers(id = material_node.base_file)
+        if not root_materials:
+            Logger.log("i", "Unable to duplicate the root material with ID {root_id}, because it doesn't exist.".format(root_id = material_node.base_file))
+            return None
+        root_material = root_materials[0]
+
+        # Ensure that all settings are saved.
+        application = cura.CuraApplication.CuraApplication.getInstance()
+        application.saveSettings()
+
+        # Create a new ID and container to hold the data.
+        if new_base_id is None:
+            new_base_id = container_registry.uniqueName(root_material.getId())
+        new_root_material = copy.deepcopy(root_material)
+        new_root_material.getMetaData()["id"] = new_base_id
+        new_root_material.getMetaData()["base_file"] = new_base_id
+        if new_metadata is not None:
+            new_root_material.getMetaData().update(new_metadata)
+        new_containers = [new_root_material]
+
+        # Clone all submaterials.
+        for container_to_copy in container_registry.findInstanceContainers(base_file = material_node.base_file):
+            if container_to_copy.getId() == material_node.base_file:
+                continue  # We already have that one. Skip it.
+            new_id = new_base_id
+            definition = container_to_copy.getMetaDataEntry("definition")
+            if definition != "fdmprinter":
+                new_id += "_" + definition
+                variant_name = container_to_copy.getMetaDataEntry("variant_name")
+                if variant_name:
+                    new_id += "_" + variant_name.replace(" ", "_")
+
+            new_container = copy.deepcopy(container_to_copy)
+            new_container.getMetaData()["id"] = new_id
+            new_container.getMetaData()["base_file"] = new_base_id
+            if new_metadata is not None:
+                new_container.getMetaData().update(new_metadata)
+            new_containers.append(new_container)
+
+        for container_to_add in new_containers:
+            container_to_add.setDirty(True)
+            container_registry.addContainer(container_to_add)
+
+        # If the duplicated material was favorite then the new material should also be added to the favorites.
+        favorites_set = set(application.getPreferences().getValue("cura/favorite_materials").split(";"))
+        if material_node.base_file in favorites_set:
+            favorites_set.add(new_base_id)
+            application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites_set))
+
+        return new_base_id
+
+    ##  Create a new material by cloning the preferred material for the current
+    #   material diameter and generate a new GUID.
+    #
+    #   The material type is explicitly left to be the one from the preferred
+    #   material, since this allows the user to still have SOME profiles to work
+    #   with.
+    #   \return The ID of the newly created material.
+    @pyqtSlot(result = str)
+    def createMaterial(self) -> str:
+        # Ensure all settings are saved.
+        application = cura.CuraApplication.CuraApplication.getInstance()
+        application.saveSettings()
+
+        # Find the preferred material.
+        extruder_stack = application.getMachineManager().activeStack
+        active_variant_name = extruder_stack.variant.getName()
+        approximate_diameter = int(extruder_stack.approximateMaterialDiameter)
+        global_container_stack = application.getGlobalContainerStack()
+        if not global_container_stack:
+            return ""
+        machine_node = ContainerTree.getInstance().machines[global_container_stack.definition.getId()]
+        preferred_material_node = machine_node.variants[active_variant_name].preferredMaterial(approximate_diameter)
+
+        # Create a new ID & new metadata for the new material.
+        new_id = CuraContainerRegistry.getInstance().uniqueName("custom_material")
+        new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
+                        "brand": catalog.i18nc("@label", "Custom"),
+                        "GUID": str(uuid.uuid4()),
+                        }
+
+        self.duplicateMaterial(preferred_material_node, new_base_id = new_id, new_metadata = new_metadata)
+        return new_id

+ 142 - 11
cura/Machines/Models/QualityManagementModel.py

@@ -1,11 +1,22 @@
 # Copyright (c) 2019 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
-from PyQt5.QtCore import Qt, pyqtSlot
+from typing import Any, Dict, Optional, TYPE_CHECKING
+from PyQt5.QtCore import pyqtSlot, QObject, Qt
 
 from UM.Logger import Logger
 from UM.Qt.ListModel import ListModel
+from UM.Settings.InstanceContainer import InstanceContainer  # To create new profiles.
+
+import cura.CuraApplication  # Imported this way to prevent circular imports.
 from cura.Machines.ContainerTree import ContainerTree
+from cura.Settings.cura_empty_instance_containers import empty_quality_changes_container
+
+if TYPE_CHECKING:
+    from UM.Settings.Interfaces import ContainerInterface
+    from cura.Machines.QualityChangesGroup import QualityChangesGroup
+    from cura.Settings.ExtruderStack import ExtruderStack
+    from cura.Settings.GlobalStack import GlobalStack
 
 #
 # This the QML model for the quality management page.
@@ -24,17 +35,136 @@ class QualityManagementModel(ListModel):
         self.addRoleName(self.QualityGroupRole, "quality_group")
         self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
 
-        from cura.CuraApplication import CuraApplication
-        self._container_registry = CuraApplication.getInstance().getContainerRegistry()
-        self._machine_manager = CuraApplication.getInstance().getMachineManager()
-        self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
-        self._quality_manager = CuraApplication.getInstance().getQualityManager()
+        application = cura.CuraApplication.CuraApplication.getInstance()
+        container_registry = application.getContainerRegistry()
+        self._machine_manager = application.getMachineManager()
+        self._extruder_manager = application.getExtruderManager()
 
         self._machine_manager.globalContainerChanged.connect(self._update)
-        self._quality_manager.qualitiesUpdated.connect(self._update)
+        container_registry.containerAdded.connect(self._qualityChangesListChanged)
+        container_registry.containerRemoved.connect(self._qualityChangesListChanged)
+        container_registry.containerMetaDataChanged.connect(self._qualityChangesListChanged)
 
         self._update()
 
+    ##  Deletes a custom profile. It will be gone forever.
+    #   \param quality_changes_group The quality changes group representing the
+    #   profile to delete.
+    @pyqtSlot(QObject)
+    def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
+        Logger.log("i", "Removing quality changes group {group_name}".format(group_name = quality_changes_group.name))
+        removed_quality_changes_ids = set()
+        container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
+        for metadata in [quality_changes_group.metadata_for_global] + list(quality_changes_group.metadata_per_extruder.values()):
+            container_id = metadata["id"]
+            container_registry.removeContainer(container_id)
+            removed_quality_changes_ids.add(container_id)
+
+        # Reset all machines that have activated this custom profile.
+        for global_stack in container_registry.findContainerStacks(type = "machine"):
+            if global_stack.qualityChanges.getId() in removed_quality_changes_ids:
+                global_stack.qualityChanges = empty_quality_changes_container
+        for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"):
+            if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids:
+                extruder_stack.qualityChanges = empty_quality_changes_container
+
+    ##  Rename a custom profile.
+    #
+    #   Because the names must be unique, the new name may not actually become
+    #   the name that was given. The actual name is returned by this function.
+    #   \param quality_changes_group The custom profile that must be renamed.
+    #   \param new_name The desired name for the profile.
+    #   \return The actual new name of the profile, after making the name
+    #   unique.
+    @pyqtSlot(QObject, str, result = str)
+    def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str:
+        Logger.log("i", "Renaming QualityChangesGroup {old_name} to {new_name}.".format(old_name = quality_changes_group.name, new_name = new_name))
+        if new_name == quality_changes_group.name:
+            Logger.log("i", "QualityChangesGroup name {name} unchanged.".format(name = quality_changes_group.name))
+            return new_name
+
+        application = cura.CuraApplication.CuraApplication.getInstance()
+        new_name = application.getContainerRegistry().uniqueName(new_name)
+        for node in quality_changes_group.getAllNodes():
+            container = node.container
+            if container:
+                container.setName(new_name)
+
+        quality_changes_group.name = new_name
+
+        application.getMachineManager().activeQualityChanged.emit()
+        application.getMachineManager().activeQualityGroupChanged.emit()
+
+        return new_name
+
+    ##  Duplicates a given quality profile OR quality changes profile.
+    #   \param new_name The desired name of the new profile. This will be made
+    #   unique, so it might end up with a different name.
+    #   \param quality_model_item The item of this model to duplicate, as
+    #   dictionary. See the descriptions of the roles of this list model.
+    @pyqtSlot(str, "QVariantMap")
+    def duplicateQualityChanges(self, new_name: str, quality_model_item: Dict[str, Any]) -> None:
+        global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
+        if not global_stack:
+            Logger.log("i", "No active global stack, cannot duplicate quality (changes) profile.")
+            return
+
+        container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
+        new_name = container_registry.uniqueName(new_name)
+
+        quality_group = quality_model_item["quality_group"]
+        quality_changes_group = quality_model_item["quality_changes_group"]
+        if quality_changes_group is None:
+            # Create global quality changes only.
+            new_quality_changes = self._createQualityChanges(quality_group.quality_type, new_name, global_stack, extruder_stack = None)
+            container_registry.addContainer(new_quality_changes)
+        else:
+            for metadata in [quality_changes_group.metadata_for_global] + quality_changes_group.metadata_per_extruder.values():
+                containers = container_registry.findContainers(id = metadata["id"])
+                if not containers:
+                    continue
+                container = containers[0]
+                new_id = container_registry.uniqueName(container.getId())
+                container_registry.addContainer(container.duplicate(new_id, new_name))
+
+    ##  Create a quality changes container with the given set-up.
+    #   \param quality_type The quality type of the new container.
+    #   \param new_name The name of the container. This name must be unique.
+    #   \param machine The global stack to create the profile for.
+    #   \param extruder_stack The extruder stack to create the profile for. If
+    #   not provided, only a global container will be created.
+    def _createQualityChanges(self, quality_type: str, new_name: str, machine: "GlobalStack", extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer":
+        container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
+        base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId()
+        new_id = base_id + "_" + new_name
+        new_id = new_id.lower().replace(" ", "_")
+        new_id = container_registry.uniqueName(new_id)
+
+        # Create a new quality_changes container for the quality.
+        quality_changes = InstanceContainer(new_id)
+        quality_changes.setName(new_name)
+        quality_changes.setMetaDataEntry("type", "quality_changes")
+        quality_changes.setMetaDataEntry("quality_type", quality_type)
+
+        # If we are creating a container for an extruder, ensure we add that to the container.
+        if extruder_stack is not None:
+            quality_changes.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
+
+        # If the machine specifies qualities should be filtered, ensure we match the current criteria.
+        machine_definition_id = ContainerTree.getInstance().machines[machine.definition.getId()].quality_definition
+        quality_changes.setDefinition(machine_definition_id)
+
+        quality_changes.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.getInstance().SettingVersion)
+        return quality_changes
+
+    ##  Triggered when any container changed.
+    #
+    #   This filters the updates to the container manager: When it applies to
+    #   the list of quality changes, we need to update our list.
+    def _qualityChangesListChanged(self, container: "ContainerInterface") -> None:
+        if container.getMetaDataEntry("type") == "quality_changes":
+            self._update()
+
     def _update(self):
         Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
 
@@ -43,12 +173,13 @@ class QualityManagementModel(ListModel):
             self.setItems([])
             return
 
-        quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups()
-        quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(global_stack)
+        container_tree = ContainerTree.getInstance()
+        quality_group_dict = container_tree.getCurrentQualityGroups()
+        quality_changes_group_list = container_tree.getCurrentQualityChangesGroups()
 
         available_quality_types = set(quality_type for quality_type, quality_group in quality_group_dict.items()
                                       if quality_group.is_available)
-        if not available_quality_types and not quality_changes_group_dict:
+        if not available_quality_types and not quality_changes_group_list:
             # Nothing to show
             self.setItems([])
             return
@@ -69,7 +200,7 @@ class QualityManagementModel(ListModel):
 
         # Create quality_changes group items
         quality_changes_item_list = []
-        for quality_changes_group in quality_changes_group_dict.values():
+        for quality_changes_group in quality_changes_group_list:
             quality_group = quality_group_dict.get(quality_changes_group.quality_type)
             item = {"name": quality_changes_group.name,
                     "is_read_only": False,

Some files were not shown because too many files changed in this diff