Browse Source

Solved merge conflict.

Jack Ha 8 years ago
parent
commit
090b8d4f50

+ 62 - 31
cura/BuildVolume.py

@@ -74,10 +74,14 @@ class BuildVolume(SceneNode):
         self._adhesion_type = None
         self._adhesion_type = None
         self._platform = Platform(self)
         self._platform = Platform(self)
 
 
-        self._active_container_stack = None
+        self._global_container_stack = None
         Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged)
         Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged)
         self._onGlobalContainerStackChanged()
         self._onGlobalContainerStackChanged()
 
 
+        self._active_extruder_stack = None
+        ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
+        self._onActiveExtruderStackChanged()
+
     def setWidth(self, width):
     def setWidth(self, width):
         if width: self._width = width
         if width: self._width = width
 
 
@@ -208,22 +212,22 @@ class BuildVolume(SceneNode):
             "@info:status",
             "@info:status",
             "The build volume height has been reduced due to the value of the"
             "The build volume height has been reduced due to the value of the"
             " \"Print Sequence\" setting to prevent the gantry from colliding"
             " \"Print Sequence\" setting to prevent the gantry from colliding"
-            " with printed objects."), lifetime=10).show()
+            " with printed models."), lifetime=10).show()
 
 
     def getRaftThickness(self):
     def getRaftThickness(self):
         return self._raft_thickness
         return self._raft_thickness
 
 
     def _updateRaftThickness(self):
     def _updateRaftThickness(self):
         old_raft_thickness = self._raft_thickness
         old_raft_thickness = self._raft_thickness
-        self._adhesion_type = self._active_container_stack.getProperty("adhesion_type", "value")
+        self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
         self._raft_thickness = 0.0
         self._raft_thickness = 0.0
         if self._adhesion_type == "raft":
         if self._adhesion_type == "raft":
             self._raft_thickness = (
             self._raft_thickness = (
-                self._active_container_stack.getProperty("raft_base_thickness", "value") +
-                self._active_container_stack.getProperty("raft_interface_thickness", "value") +
-                self._active_container_stack.getProperty("raft_surface_layers", "value") *
-                    self._active_container_stack.getProperty("raft_surface_thickness", "value") +
-                self._active_container_stack.getProperty("raft_airgap", "value"))
+                self._global_container_stack.getProperty("raft_base_thickness", "value") +
+                self._global_container_stack.getProperty("raft_interface_thickness", "value") +
+                self._global_container_stack.getProperty("raft_surface_layers", "value") *
+                    self._global_container_stack.getProperty("raft_surface_thickness", "value") +
+                self._global_container_stack.getProperty("raft_airgap", "value"))
 
 
         # Rounding errors do not matter, we check if raft_thickness has changed at all
         # Rounding errors do not matter, we check if raft_thickness has changed at all
         if old_raft_thickness != self._raft_thickness:
         if old_raft_thickness != self._raft_thickness:
@@ -231,41 +235,52 @@ class BuildVolume(SceneNode):
             self.raftThicknessChanged.emit()
             self.raftThicknessChanged.emit()
 
 
     def _onGlobalContainerStackChanged(self):
     def _onGlobalContainerStackChanged(self):
-        if self._active_container_stack:
-            self._active_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
+        if self._global_container_stack:
+            self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
 
 
-        self._active_container_stack = Application.getInstance().getGlobalContainerStack()
+        self._global_container_stack = Application.getInstance().getGlobalContainerStack()
 
 
-        if self._active_container_stack:
-            self._active_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
+        if self._global_container_stack:
+            self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
 
 
-            self._width = self._active_container_stack.getProperty("machine_width", "value")
-            if self._active_container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
-                self._height = self._active_container_stack.getProperty("gantry_height", "value")
-                self._buildVolumeMessage()
+            self._width = self._global_container_stack.getProperty("machine_width", "value")
+            machine_height = self._global_container_stack.getProperty("machine_height", "value")
+            if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
+                self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
+                if self._height < machine_height:
+                    self._buildVolumeMessage()
             else:
             else:
-                self._height = self._active_container_stack.getProperty("machine_height", "value")
-            self._depth = self._active_container_stack.getProperty("machine_depth", "value")
+                self._height = self._global_container_stack.getProperty("machine_height", "value")
+            self._depth = self._global_container_stack.getProperty("machine_depth", "value")
 
 
             self._updateDisallowedAreas()
             self._updateDisallowedAreas()
             self._updateRaftThickness()
             self._updateRaftThickness()
 
 
             self.rebuild()
             self.rebuild()
 
 
+    def _onActiveExtruderStackChanged(self):
+        if self._active_extruder_stack:
+            self._active_extruder_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
+        self._active_extruder_stack = ExtruderManager.getInstance().getActiveExtruderStack()
+        if self._active_extruder_stack:
+            self._active_extruder_stack.propertyChanged.connect(self._onSettingPropertyChanged)
+
     def _onSettingPropertyChanged(self, setting_key, property_name):
     def _onSettingPropertyChanged(self, setting_key, property_name):
         if property_name != "value":
         if property_name != "value":
             return
             return
 
 
         rebuild_me = False
         rebuild_me = False
         if setting_key == "print_sequence":
         if setting_key == "print_sequence":
+            machine_height = self._global_container_stack.getProperty("machine_height", "value")
             if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time":
             if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time":
-                self._height = self._active_container_stack.getProperty("gantry_height", "value")
-                self._buildVolumeMessage()
+                self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
+                if self._height < machine_height:
+                    self._buildVolumeMessage()
             else:
             else:
-                self._height = self._active_container_stack.getProperty("machine_height", "value")
+                self._height = self._global_container_stack.getProperty("machine_height", "value")
             rebuild_me = True
             rebuild_me = True
 
 
-        if setting_key in self._skirt_settings:
+        if setting_key in self._skirt_settings or setting_key in self._prime_settings or setting_key in self._tower_settings:
             self._updateDisallowedAreas()
             self._updateDisallowedAreas()
             rebuild_me = True
             rebuild_me = True
 
 
@@ -277,19 +292,33 @@ class BuildVolume(SceneNode):
             self.rebuild()
             self.rebuild()
 
 
     def _updateDisallowedAreas(self):
     def _updateDisallowedAreas(self):
-        if not self._active_container_stack:
+        if not self._global_container_stack:
             return
             return
 
 
         disallowed_areas = copy.deepcopy(
         disallowed_areas = copy.deepcopy(
-            self._active_container_stack.getProperty("machine_disallowed_areas", "value"))
+            self._global_container_stack.getProperty("machine_disallowed_areas", "value"))
         areas = []
         areas = []
 
 
+        machine_width = self._global_container_stack.getProperty("machine_width", "value")
+        machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
+
+        # Add prima tower location as disallowed area.
+        if self._global_container_stack.getProperty("prime_tower_enable", "value"):
+            half_prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value") / 2
+            prime_tower_x = self._global_container_stack.getProperty("prime_tower_position_x", "value") - machine_width / 2
+            prime_tower_y = - self._global_container_stack.getProperty("prime_tower_position_y", "value") + machine_depth / 2
+
+            disallowed_areas.append([
+                [prime_tower_x - half_prime_tower_size, prime_tower_y - half_prime_tower_size],
+                [prime_tower_x + half_prime_tower_size, prime_tower_y - half_prime_tower_size],
+                [prime_tower_x + half_prime_tower_size, prime_tower_y + half_prime_tower_size],
+                [prime_tower_x - half_prime_tower_size, prime_tower_y + half_prime_tower_size],
+            ])
+
         # Add extruder prime locations as disallowed areas.
         # Add extruder prime locations as disallowed areas.
         # Probably needs some rework after coordinate system change.
         # Probably needs some rework after coordinate system change.
         extruder_manager = ExtruderManager.getInstance()
         extruder_manager = ExtruderManager.getInstance()
-        extruders = extruder_manager.getMachineExtruders(self._active_container_stack.getId())
-        machine_width = self._active_container_stack.getProperty("machine_width", "value")
-        machine_depth = self._active_container_stack.getProperty("machine_depth", "value")
+        extruders = extruder_manager.getMachineExtruders(self._global_container_stack.getId())
         for single_extruder in extruders:
         for single_extruder in extruders:
             extruder_prime_pos_x = single_extruder.getProperty("extruder_prime_pos_x", "value")
             extruder_prime_pos_x = single_extruder.getProperty("extruder_prime_pos_x", "value")
             extruder_prime_pos_y = single_extruder.getProperty("extruder_prime_pos_y", "value")
             extruder_prime_pos_y = single_extruder.getProperty("extruder_prime_pos_y", "value")
@@ -305,7 +334,7 @@ class BuildVolume(SceneNode):
                 [prime_x - PRIME_CLEARANCE, prime_y + PRIME_CLEARANCE],
                 [prime_x - PRIME_CLEARANCE, prime_y + PRIME_CLEARANCE],
             ])
             ])
 
 
-        bed_adhesion_size = self._getBedAdhesionSize(self._active_container_stack)
+        bed_adhesion_size = self._getBedAdhesionSize(self._global_container_stack)
 
 
         if disallowed_areas:
         if disallowed_areas:
             # Extend every area already in the disallowed_areas with the skirt size.
             # Extend every area already in the disallowed_areas with the skirt size.
@@ -317,8 +346,8 @@ class BuildVolume(SceneNode):
 
 
         # Add the skirt areas around the borders of the build plate.
         # Add the skirt areas around the borders of the build plate.
         if bed_adhesion_size > 0:
         if bed_adhesion_size > 0:
-            half_machine_width = self._active_container_stack.getProperty("machine_width", "value") / 2
-            half_machine_depth = self._active_container_stack.getProperty("machine_depth", "value") / 2
+            half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
+            half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2
 
 
             areas.append(Polygon(numpy.array([
             areas.append(Polygon(numpy.array([
                 [-half_machine_width, -half_machine_depth],
                 [-half_machine_width, -half_machine_depth],
@@ -377,3 +406,5 @@ class BuildVolume(SceneNode):
 
 
     _skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "xy_offset"]
     _skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "xy_offset"]
     _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap"]
     _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap"]
+    _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z"]
+    _tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]

+ 8 - 1
cura/CuraApplication.py

@@ -242,11 +242,17 @@ class CuraApplication(QtApplication):
                 raft_airgap
                 raft_airgap
                 layer_0_z_overlap
                 layer_0_z_overlap
                 raft_surface_layers
                 raft_surface_layers
+            dual
+                adhesion_extruder_nr
+                support_extruder_nr
+                prime_tower_enable
+                prime_tower_size
+                prime_tower_position_x
+                prime_tower_position_y
             meshfix
             meshfix
             blackmagic
             blackmagic
                 print_sequence
                 print_sequence
                 infill_mesh
                 infill_mesh
-                dual
             experimental
             experimental
         """.replace("\n", ";").replace(" ", ""))
         """.replace("\n", ";").replace(" ", ""))
 
 
@@ -475,6 +481,7 @@ class CuraApplication(QtApplication):
 
 
         qmlRegisterType(cura.Settings.ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
         qmlRegisterType(cura.Settings.ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
         qmlRegisterType(cura.Settings.MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
         qmlRegisterType(cura.Settings.MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
+        qmlRegisterType(cura.Settings.QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
 
 
         qmlRegisterSingletonType(cura.Settings.ContainerManager, "Cura", 1, 0, "ContainerManager", cura.Settings.ContainerManager.createContainerManager)
         qmlRegisterSingletonType(cura.Settings.ContainerManager, "Cura", 1, 0, "ContainerManager", cura.Settings.ContainerManager.createContainerManager)
 
 

+ 8 - 0
cura/PlatformPhysics.py

@@ -41,6 +41,10 @@ class PlatformPhysics:
 
 
         root = self._controller.getScene().getRoot()
         root = self._controller.getScene().getRoot()
 
 
+        # Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the
+        # same direction.
+        transformed_nodes = []
+
         for node in BreadthFirstIterator(root):
         for node in BreadthFirstIterator(root):
             if node is root or type(node) is not SceneNode or node.getBoundingBox() is None:
             if node is root or type(node) is not SceneNode or node.getBoundingBox() is None:
                 continue
                 continue
@@ -91,6 +95,9 @@ class PlatformPhysics:
                     if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
                     if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
                         continue
                         continue
 
 
+                    if other_node in transformed_nodes:
+                        continue # Other node is already moving, wait for next pass.
+
                     # Get the overlap distance for both convex hulls. If this returns None, there is no intersection.
                     # Get the overlap distance for both convex hulls. If this returns None, there is no intersection.
                     head_hull = node.callDecoration("getConvexHullHead")
                     head_hull = node.callDecoration("getConvexHullHead")
                     if head_hull:
                     if head_hull:
@@ -125,6 +132,7 @@ class PlatformPhysics:
                     node._outside_buildarea = True
                     node._outside_buildarea = True
 
 
             if not Vector.Null.equals(move_vector, epsilon=1e-5):
             if not Vector.Null.equals(move_vector, epsilon=1e-5):
+                transformed_nodes.append(node)
                 op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
                 op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
                 op.push()
                 op.push()
 
 

+ 12 - 2
cura/PrinterOutputDevice.py

@@ -1,10 +1,15 @@
+from UM.i18n import i18nCatalog
 from UM.OutputDevice.OutputDevice import OutputDevice
 from UM.OutputDevice.OutputDevice import OutputDevice
 from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
 from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
+from PyQt5.QtWidgets import QMessageBox
+
 from enum import IntEnum  # For the connection state tracking.
 from enum import IntEnum  # For the connection state tracking.
 from UM.Logger import Logger
 from UM.Logger import Logger
-
+from UM.Application import Application
 from UM.Signal import signalemitter
 from UM.Signal import signalemitter
 
 
+i18n_catalog = i18nCatalog("cura")
+
 ##  Printer output device adds extra interface options on top of output device.
 ##  Printer output device adds extra interface options on top of output device.
 #
 #
 #   The assumption is made the printer is a FDM printer.
 #   The assumption is made the printer is a FDM printer.
@@ -276,6 +281,11 @@ class PrinterOutputDevice(QObject, OutputDevice):
             self._hotend_ids[index] = hotend_id
             self._hotend_ids[index] = hotend_id
             self.hotendIdChanged.emit(index, hotend_id)
             self.hotendIdChanged.emit(index, hotend_id)
 
 
+    ##  Let the user decide if the hotends and/or material should be synced with the printer
+    #   NB: the UX needs to be implemented by the plugin
+    def materialHotendChangedMessage(self, callback):
+        Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
+        callback(QMessageBox.Yes)
 
 
     ##  Attempt to establish connection
     ##  Attempt to establish connection
     def connect(self):
     def connect(self):
@@ -329,7 +339,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
         return self._head_z
         return self._head_z
 
 
     ##  Update the saved position of the head
     ##  Update the saved position of the head
-    #   This function should be called when a new position for the head is recieved. 
+    #   This function should be called when a new position for the head is received.
     def _updateHeadPosition(self, x, y ,z):
     def _updateHeadPosition(self, x, y ,z):
         position_changed = False
         position_changed = False
         if self._head_x != x:
         if self._head_x != x:

+ 199 - 40
cura/Settings/ContainerManager.py

@@ -351,47 +351,49 @@ class ContainerManager(QObject):
 
 
         return { "status": "success", "message": "Successfully imported container {0}".format(container.getName()) }
         return { "status": "success", "message": "Successfully imported container {0}".format(container.getName()) }
 
 
+    ##  Update the current active quality changes container with the settings from the user container.
+    #
+    #   This will go through the active global stack and all active extruder stacks and merge the changes from the user
+    #   container into the quality_changes container. After that, the user container is cleared.
+    #
+    #   \return \type{bool} True if successful, False if not.
     @pyqtSlot(result = bool)
     @pyqtSlot(result = bool)
     def updateQualityChanges(self):
     def updateQualityChanges(self):
         global_stack = UM.Application.getInstance().getGlobalContainerStack()
         global_stack = UM.Application.getInstance().getGlobalContainerStack()
-
-        containers_to_merge = []
-
-        global_quality_changes = global_stack.findContainer(type = "quality_changes")
-        if not global_quality_changes or global_quality_changes.isReadOnly():
-            UM.Logger.log("e", "Could not update quality of a nonexistant or read only quality profile")
+        if not global_stack:
             return False
             return False
 
 
         UM.Application.getInstance().getMachineManager().blurSettings.emit()
         UM.Application.getInstance().getMachineManager().blurSettings.emit()
 
 
-        containers_to_merge.append((global_quality_changes, global_stack.getTop()))
-
-        for extruder in cura.Settings.ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
-            quality_changes = extruder.findContainer(type = "quality_changes")
+        for stack in cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
+            # Find the quality_changes container for this stack and merge the contents of the top container into it.
+            quality_changes = stack.findContainer(type = "quality_changes")
             if not quality_changes or quality_changes.isReadOnly():
             if not quality_changes or quality_changes.isReadOnly():
-                UM.Logger.log("e", "Could not update quality of a nonexistant or read only quality profile")
-                return False
-
-            containers_to_merge.append((quality_changes, extruder.getTop()))
+                UM.Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
+                continue
 
 
-        for merge_into, merge in containers_to_merge:
-            self._performMerge(merge_into, merge)
+            self._performMerge(quality_changes, stack.getTop())
 
 
         UM.Application.getInstance().getMachineManager().activeQualityChanged.emit()
         UM.Application.getInstance().getMachineManager().activeQualityChanged.emit()
 
 
         return True
         return True
 
 
+    ##  Clear the top-most (user) containers of the active stacks.
     @pyqtSlot()
     @pyqtSlot()
     def clearUserContainers(self):
     def clearUserContainers(self):
-        global_stack = UM.Application.getInstance().getGlobalContainerStack()
-
         UM.Application.getInstance().getMachineManager().blurSettings.emit()
         UM.Application.getInstance().getMachineManager().blurSettings.emit()
 
 
-        for extruder in cura.Settings.ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
-            extruder.getTop().clear()
-
-        global_stack.getTop().clear()
+        # Go through global and extruder stacks and clear their topmost container (the user settings).
+        for stack in cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
+            stack.getTop().clear()
 
 
+    ##  Create quality changes containers from the user containers in the active stacks.
+    #
+    #   This will go through the global and extruder stacks and create quality_changes containers from
+    #   the user containers in each stack. These then replace the quality_changes containers in the
+    #   stack and clear the user settings.
+    #
+    #   \return \type{bool} True if the operation was successfully, False if not.
     @pyqtSlot(result = bool)
     @pyqtSlot(result = bool)
     def createQualityChanges(self):
     def createQualityChanges(self):
         global_stack = UM.Application.getInstance().getGlobalContainerStack()
         global_stack = UM.Application.getInstance().getGlobalContainerStack()
@@ -406,39 +408,115 @@ class ContainerManager(QObject):
         UM.Application.getInstance().getMachineManager().blurSettings.emit()
         UM.Application.getInstance().getMachineManager().blurSettings.emit()
 
 
         unique_name = UM.Settings.ContainerRegistry.getInstance().uniqueName(quality_container.getName())
         unique_name = UM.Settings.ContainerRegistry.getInstance().uniqueName(quality_container.getName())
-        unique_id = unique_name.lower()
-        unique_id.replace(" ", "_")
 
 
-        stacks = [ s for s in cura.Settings.ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()) ]
-        stacks.insert(0, global_stack)
-
-        for stack in stacks:
+        # Go through the active stacks and create quality_changes containers from the user containers.
+        for stack in cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
             user_container = stack.getTop()
             user_container = stack.getTop()
             quality_container = stack.findContainer(type = "quality")
             quality_container = stack.findContainer(type = "quality")
             quality_changes_container = stack.findContainer(type = "quality_changes")
             quality_changes_container = stack.findContainer(type = "quality_changes")
             if not quality_container or not quality_changes_container:
             if not quality_container or not quality_changes_container:
                 UM.Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
                 UM.Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
-                return False
+                continue
 
 
-            new_quality_changes = user_container.duplicate(stack.getId() + "_" + unique_id, unique_name)
-            new_quality_changes.setMetaDataEntry("type", "quality_changes")
-            new_quality_changes.addMetaDataEntry("quality", quality_container.getId())
+            new_changes = self._createQualityChanges(quality_container, unique_name, stack.getId())
+            self._performMerge(new_changes, user_container)
 
 
-            if not global_stack.getMetaDataEntry("has_machine_quality"):
-                new_quality_changes.setDefinition(UM.Settings.ContainerRegistry.getInstance().findContainers(id = "fdmprinter")[0])
+            UM.Settings.ContainerRegistry.getInstance().addContainer(new_changes)
+            stack.replaceContainer(stack.getContainerIndex(quality_changes_container), new_changes)
 
 
-            if global_stack.getMetaDataEntry("has_materials"):
-                material = stack.findContainer(type = "material")
-                new_quality_changes.addMetaDataEntry("material", material.getId())
+        UM.Application.getInstance().getMachineManager().activeQualityChanged.emit()
+        return True
 
 
-            UM.Settings.ContainerRegistry.getInstance().addContainer(new_quality_changes)
+    ##  Remove all quality changes containers matching a specified name.
+    #
+    #   This will search for quality_changes containers matching the supplied name and remove them.
+    #   Note that if the machine specifies that qualities should be filtered by machine and/or material
+    #   only the containers related to the active machine/material are removed.
+    #
+    #   \param quality_name The name of the quality changes to remove.
+    #
+    #   \return \type{bool} True if successful, False if not.
+    @pyqtSlot(str, result = bool)
+    def removeQualityChanges(self, quality_name):
+        if not quality_name:
+            return False
 
 
-            stack.replaceContainer(stack.getContainerIndex(quality_changes_container), new_quality_changes)
-            stack.getTop().clear()
+        for container in self._getFilteredContainers(name = quality_name, type = "quality_changes"):
+            UM.Settings.ContainerRegistry.getInstance().removeContainer(container.getId())
+
+        return True
+
+    ##  Rename a set of quality changes containers.
+    #
+    #   This will search for quality_changes containers matching the supplied name and rename them.
+    #   Note that if the machine specifies that qualities should be filtered by machine and/or material
+    #   only the containers related to the active machine/material are renamed.
+    #
+    #   \param quality_name The name of the quality changes containers to rename.
+    #   \param new_name The new name of the quality changes.
+    #
+    #   \return True if successful, False if not.
+    @pyqtSlot(str, str, result = bool)
+    def renameQualityChanges(self, quality_name, new_name):
+        if not quality_name or not new_name:
+            return False
+
+        if quality_name == new_name:
+            return True
+
+        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        if not global_stack:
+            return False
+
+        UM.Application.getInstance().getMachineManager().blurSettings.emit()
+
+        new_name = UM.Settings.ContainerRegistry.getInstance().uniqueName(new_name)
+
+        container_registry = UM.Settings.ContainerRegistry.getInstance()
+        for container in self._getFilteredContainers(name = quality_name, type = "quality_changes"):
+            stack_id = container.getMetaDataEntry("extruder", global_stack.getId())
+            container_registry.renameContainer(container.getId(), new_name, self._createUniqueId(stack_id, new_name))
 
 
         UM.Application.getInstance().getMachineManager().activeQualityChanged.emit()
         UM.Application.getInstance().getMachineManager().activeQualityChanged.emit()
         return True
         return True
 
 
+    ##  Duplicate a specified set of quality or quality_changes containers.
+    #
+    #   This will search for containers matching the specified name. If the container is a "quality" type container, a new
+    #   quality_changes container will be created with the specified quality as base. If the container is a "quality_changes"
+    #   container, it is simply duplicated and renamed.
+    #
+    #   \param quality_name The name of the quality to duplicate.
+    #
+    #   \return A string containing the name of the duplicated containers, or an empty string if it failed.
+    @pyqtSlot(str, result = str)
+    def duplicateQualityOrQualityChanges(self, quality_name):
+        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        if not global_stack or not quality_name:
+            return ""
+
+        containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(name = quality_name)
+        if not containers:
+            return ""
+
+        new_name = UM.Settings.ContainerRegistry.getInstance().uniqueName(quality_name)
+
+        container_type = containers[0].getMetaDataEntry("type")
+        if container_type == "quality":
+            for container in self._getFilteredContainers(name = quality_name, type = "quality"):
+                for stack in cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
+                    new_changes = self._createQualityChanges(container, new_name, stack.getId())
+                    UM.Settings.ContainerRegistry.getInstance().addContainer(new_changes)
+        elif container_type == "quality_changes":
+            for container in self._getFilteredContainers(name = quality_name, type = "quality_changes"):
+                stack_id = container.getMetaDataEntry("extruder", global_stack.getId())
+                new_container = container.duplicate(self._createUniqueId(stack_id, new_name), new_name)
+                UM.Settings.ContainerRegistry.getInstance().addContainer(new_container)
+        else:
+            return ""
+
+        return new_name
+
     # Factory function, used by QML
     # Factory function, used by QML
     @staticmethod
     @staticmethod
     def createContainerManager(engine, js_engine):
     def createContainerManager(engine, js_engine):
@@ -498,3 +576,84 @@ class ContainerManager(QObject):
 
 
             name_filter = "{0} ({1})".format(mime_type.comment, suffix_list)
             name_filter = "{0} ({1})".format(mime_type.comment, suffix_list)
             self._container_name_filters[name_filter] = entry
             self._container_name_filters[name_filter] = entry
+
+    ##  Return a generator that iterates over a set of containers that are filtered by machine and material when needed.
+    #
+    #   \param kwargs Initial search criteria that the containers need to match.
+    #
+    #   \return A generator that iterates over the list of containers matching the search criteria.
+    def _getFilteredContainers(self, **kwargs):
+        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        if not global_stack:
+            return False
+
+        criteria = kwargs
+
+        filter_by_material = False
+
+        if global_stack.getMetaDataEntry("has_machine_quality"):
+            criteria["definition"] = global_stack.getBottom().getId()
+
+            filter_by_material = global_stack.getMetaDataEntry("has_materials")
+
+        material_ids = []
+        if filter_by_material:
+            for stack in cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
+                material_ids.append(stack.findContainer(type = "material").getId())
+
+        containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
+        for container in containers:
+            # If the machine specifies we should filter by material, exclude containers that do not match any active material.
+            if filter_by_material and container.getMetaDataEntry("material") not in material_ids:
+                continue
+
+            yield container
+
+    ##  Creates a unique ID for a container by prefixing the name with the stack ID.
+    #
+    #   This method creates a unique ID for a container by prefixing it with a specified stack ID.
+    #   This is done to ensure we have an easily identified ID for quality changes, which have the
+    #   same name across several stacks.
+    #
+    #   \param stack_id The ID of the stack to prepend.
+    #   \param container_name The name of the container that we are creating a unique ID for.
+    #
+    #   \return Container name prefixed with stack ID, in lower case with spaces replaced by underscores.
+    def _createUniqueId(self, stack_id, container_name):
+        result = stack_id + "_" + container_name
+        result = result.lower()
+        result.replace(" ", "_")
+        return result
+
+    ##  Create a quality changes container for a specified quality container.
+    #
+    #   \param quality_container The quality container to create a changes container for.
+    #   \param new_name The name of the new quality_changes container.
+    #   \param stack_id The ID of the container stack the new container "belongs to". It is used primarily to ensure a unique ID.
+    #
+    #   \return A new quality_changes container with the specified container as base.
+    def _createQualityChanges(self, quality_container, new_name, stack_id):
+        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        assert global_stack is not None
+
+        # Create a new quality_changes container for the quality.
+        quality_changes = UM.Settings.InstanceContainer(self._createUniqueId(stack_id, new_name))
+        quality_changes.setName(new_name)
+        quality_changes.addMetaDataEntry("type", "quality_changes")
+        quality_changes.addMetaDataEntry("quality", quality_container.getMetaDataEntry("quality_type"))
+
+        # If we are creating a container for an extruder, ensure we add that to the container
+        if stack_id != global_stack.getId():
+            quality_changes.addMetaDataEntry("extruder", stack_id)
+
+        # If the machine specifies qualities should be filtered, ensure we match the current criteria.
+        if not global_stack.getMetaDataEntry("has_machine_quality"):
+            quality_changes.setDefinition(UM.Settings.ContainerRegistry.getInstance().findContainers(id = "fdmprinter")[0])
+        else:
+            quality_changes.setDefinition(global_stack.getBottom())
+
+            if global_stack.getMetaDataEntry("has_materials"):
+                material = quality_container.getMetaDataEntry("material")
+                quality_changes.addMetaDataEntry("material", material)
+
+        return quality_changes

+ 1 - 1
cura/Settings/ContainerSettingsModel.py

@@ -41,7 +41,6 @@ class ContainerSettingsModel(ListModel):
             keys = keys + list(container.getAllKeys())
             keys = keys + list(container.getAllKeys())
 
 
         keys = list(set(keys)) # remove duplicate keys
         keys = list(set(keys)) # remove duplicate keys
-        keys.sort()
 
 
         for key in keys:
         for key in keys:
             definition = None
             definition = None
@@ -72,6 +71,7 @@ class ContainerSettingsModel(ListModel):
                 "unit": definition.unit,
                 "unit": definition.unit,
                 "category": category.label
                 "category": category.label
             })
             })
+        self.sort(lambda k: (k["category"], k["key"]))
 
 
     ##  Set the ids of the containers which have the settings this model should list.
     ##  Set the ids of the containers which have the settings this model should list.
     #   Also makes sure the model updates when the containers have property changes
     #   Also makes sure the model updates when the containers have property changes

+ 1 - 0
cura/Settings/CuraContainerRegistry.py

@@ -170,6 +170,7 @@ class CuraContainerRegistry(ContainerRegistry):
                 profile.addMetaDataEntry("material", self._activeMaterialId())
                 profile.addMetaDataEntry("material", self._activeMaterialId())
         else:
         else:
             profile.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0])
             profile.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0])
+
         ContainerRegistry.getInstance().addContainer(profile)
         ContainerRegistry.getInstance().addContainer(profile)
 
 
     ##  Gets a list of profile writer plugins
     ##  Gets a list of profile writer plugins

+ 20 - 6
cura/Settings/ExtruderManager.py

@@ -131,9 +131,9 @@ class ExtruderManager(QObject):
 
 
                 # Make sure the next stack is a stack that contains only the machine definition
                 # Make sure the next stack is a stack that contains only the machine definition
                 if not extruder_train.getNextStack():
                 if not extruder_train.getNextStack():
-                    shallowStack = UM.Settings.ContainerStack(machine_id + "_shallow")
-                    shallowStack.addContainer(machine_definition)
-                    extruder_train.setNextStack(shallowStack)
+                    shallow_stack = UM.Settings.ContainerStack(machine_id + "_shallow")
+                    shallow_stack.addContainer(machine_definition)
+                    extruder_train.setNextStack(shallow_stack)
                 changed = True
                 changed = True
         if changed:
         if changed:
             self.extrudersChanged.emit(machine_id)
             self.extrudersChanged.emit(machine_id)
@@ -251,9 +251,9 @@ class ExtruderManager(QObject):
 
 
         # Make sure the next stack is a stack that contains only the machine definition
         # Make sure the next stack is a stack that contains only the machine definition
         if not container_stack.getNextStack():
         if not container_stack.getNextStack():
-            shallowStack = UM.Settings.ContainerStack(machine_id + "_shallow")
-            shallowStack.addContainer(machine_definition)
-            container_stack.setNextStack(shallowStack)
+            shallow_stack = UM.Settings.ContainerStack(machine_id + "_shallow")
+            shallow_stack.addContainer(machine_definition)
+            container_stack.setNextStack(shallow_stack)
 
 
         container_registry.addContainer(container_stack)
         container_registry.addContainer(container_stack)
 
 
@@ -277,6 +277,20 @@ class ExtruderManager(QObject):
         for name in self._extruder_trains[machine_id]:
         for name in self._extruder_trains[machine_id]:
             yield self._extruder_trains[machine_id][name]
             yield self._extruder_trains[machine_id][name]
 
 
+    ##  Returns a generator that will iterate over the global stack and per-extruder stacks.
+    #
+    #   The first generated element is the global container stack. After that any extruder stacks are generated.
+    def getActiveGlobalAndExtruderStacks(self):
+        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        if not global_stack:
+            return
+
+        yield global_stack
+
+        global_id = global_stack.getId()
+        for name in self._extruder_trains[global_id]:
+            yield self._extruder_trains[global_id][name]
+
     def __globalContainerStackChanged(self):
     def __globalContainerStackChanged(self):
         self._addCurrentMachineExtruders()
         self._addCurrentMachineExtruders()
         self.activeExtruderChanged.emit()
         self.activeExtruderChanged.emit()

+ 120 - 102
cura/Settings/MachineManager.py

@@ -26,6 +26,7 @@ class MachineManager(QObject):
         self._global_container_stack = None
         self._global_container_stack = None
 
 
         Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
         Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
+        ##  When the global container is changed, active material probably needs to be updated.
         self.globalContainerChanged.connect(self.activeMaterialChanged)
         self.globalContainerChanged.connect(self.activeMaterialChanged)
         self.globalContainerChanged.connect(self.activeVariantChanged)
         self.globalContainerChanged.connect(self.activeVariantChanged)
         self.globalContainerChanged.connect(self.activeQualityChanged)
         self.globalContainerChanged.connect(self.activeQualityChanged)
@@ -36,8 +37,6 @@ class MachineManager(QObject):
         ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
         ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
         self._onActiveExtruderStackChanged()
         self._onActiveExtruderStackChanged()
 
 
-        ##  When the global container is changed, active material probably needs to be updated.
-
         ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeMaterialChanged)
         ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeMaterialChanged)
         ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeVariantChanged)
         ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeVariantChanged)
         ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeQualityChanged)
         ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeQualityChanged)
@@ -118,16 +117,7 @@ class MachineManager(QObject):
             if matching_extruder and matching_extruder.findContainer({"type": "variant"}).getName() != hotend_id:
             if matching_extruder and matching_extruder.findContainer({"type": "variant"}).getName() != hotend_id:
                 # Save the material that needs to be changed. Multiple changes will be handled by the callback.
                 # Save the material that needs to be changed. Multiple changes will be handled by the callback.
                 self._auto_hotends_changed[str(index)] = containers[0].getId()
                 self._auto_hotends_changed[str(index)] = containers[0].getId()
-                Application.getInstance().messageBox(catalog.i18nc("@window:title", "Changes on the Printer"),
-                                                     catalog.i18nc("@label",
-                                                                   "Do you want to change the materials and hotends to match the material in your printer?"),
-                                                     catalog.i18nc("@label",
-                                                                   "The materials and / or hotends on your printer were changed. For best results always slice for the materials . hotends that are inserted in your printer."),
-                                                     buttons=QMessageBox.Yes + QMessageBox.No,
-                                                     icon=QMessageBox.Question,
-                                                     callback=self._materialHotendChangedCallback)
-
-
+                self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
         else:
         else:
             Logger.log("w", "No variant found for printer definition %s with id %s" % (self._global_container_stack.getBottom().getId(), hotend_id))
             Logger.log("w", "No variant found for printer definition %s with id %s" % (self._global_container_stack.getBottom().getId(), hotend_id))
 
 
@@ -167,10 +157,7 @@ class MachineManager(QObject):
             if matching_extruder and matching_extruder.findContainer({"type":"material"}).getMetaDataEntry("GUID") != material_id:
             if matching_extruder and matching_extruder.findContainer({"type":"material"}).getMetaDataEntry("GUID") != material_id:
                 # Save the material that needs to be changed. Multiple changes will be handled by the callback.
                 # Save the material that needs to be changed. Multiple changes will be handled by the callback.
                 self._auto_materials_changed[str(index)] = containers[0].getId()
                 self._auto_materials_changed[str(index)] = containers[0].getId()
-                Application.getInstance().messageBox(catalog.i18nc("@window:title", "Changes on the Printer"), catalog.i18nc("@label", "Do you want to change the materials and hotends to match the material in your printer?"),
-                                                 catalog.i18nc("@label", "The materials and / or hotends on your printer were changed. For best results always slice for the materials and hotends that are inserted in your printer."),
-                                                 buttons = QMessageBox.Yes + QMessageBox.No, icon = QMessageBox.Question, callback = self._materialHotendChangedCallback)
-
+                self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
         else:
         else:
             Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id))
             Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id))
 
 
@@ -199,14 +186,6 @@ class MachineManager(QObject):
             if old_index is not None:
             if old_index is not None:
                 extruder_manager.setActiveExtruderIndex(old_index)
                 extruder_manager.setActiveExtruderIndex(old_index)
 
 
-
-
-
-
-
-
-
-
     def _onGlobalContainerChanged(self):
     def _onGlobalContainerChanged(self):
         if self._global_container_stack:
         if self._global_container_stack:
             self._global_container_stack.nameChanged.disconnect(self._onMachineNameChanged)
             self._global_container_stack.nameChanged.disconnect(self._onMachineNameChanged)
@@ -260,6 +239,20 @@ class MachineManager(QObject):
             self.activeQualityChanged.emit()
             self.activeQualityChanged.emit()
 
 
     def _onPropertyChanged(self, key, property_name):
     def _onPropertyChanged(self, key, property_name):
+        if property_name == "validationState":
+            if self._active_stack_valid:
+                if self._active_container_stack.getProperty(key, "settable_per_extruder"):
+                    changed_validation_state = self._active_container_stack.getProperty(key, property_name)
+                else:
+                    changed_validation_state = self._global_container_stack.getProperty(key, property_name)
+                if changed_validation_state in (UM.Settings.ValidatorState.Exception, UM.Settings.ValidatorState.MaximumError, UM.Settings.ValidatorState.MinimumError):
+                    self._active_stack_valid = False
+                    self.activeValidationChanged.emit()
+            else:
+                if not self._checkStackForErrors(self._active_container_stack) and not self._checkStackForErrors(self._global_container_stack):
+                    self._active_stack_valid = True
+                    self.activeValidationChanged.emit()
+
         self.activeStackChanged.emit()
         self.activeStackChanged.emit()
 
 
     @pyqtSlot(str)
     @pyqtSlot(str)
@@ -281,7 +274,7 @@ class MachineManager(QObject):
 
 
             variant_instance_container = self._updateVariantContainer(definition)
             variant_instance_container = self._updateVariantContainer(definition)
             material_instance_container = self._updateMaterialContainer(definition, variant_instance_container)
             material_instance_container = self._updateMaterialContainer(definition, variant_instance_container)
-            quality_instance_container = self._updateQualityContainer(definition, material_instance_container)
+            quality_instance_container = self._updateQualityContainer(definition, variant_instance_container, material_instance_container)
 
 
             current_settings_instance_container = UM.Settings.InstanceContainer(name + "_current_settings")
             current_settings_instance_container = UM.Settings.InstanceContainer(name + "_current_settings")
             current_settings_instance_container.addMetaDataEntry("machine", name)
             current_settings_instance_container.addMetaDataEntry("machine", name)
@@ -403,6 +396,31 @@ class MachineManager(QObject):
 
 
         return ""
         return ""
 
 
+    @pyqtProperty("QVariantMap", notify = activeMaterialChanged)
+    def allActiveMaterialIds(self):
+        if not self._global_container_stack:
+            return {}
+
+        result = {}
+
+        for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
+            material_container = stack.findContainer(type = "material")
+            if not material_container:
+                continue
+
+            result[stack.getId()] = material_container.getId()
+
+        return result
+
+    @pyqtProperty(str, notify=activeQualityChanged)
+    def activeQualityMaterialId(self):
+        if self._active_container_stack:
+            quality = self._active_container_stack.findContainer({"type": "quality"})
+            if quality:
+                return quality.getMetaDataEntry("material")
+
+        return ""
+
     @pyqtProperty(str, notify=activeQualityChanged)
     @pyqtProperty(str, notify=activeQualityChanged)
     def activeQualityName(self):
     def activeQualityName(self):
         if self._active_container_stack:
         if self._active_container_stack:
@@ -425,6 +443,22 @@ class MachineManager(QObject):
                 return quality.getId()
                 return quality.getId()
         return ""
         return ""
 
 
+    @pyqtProperty(str, notify = activeQualityChanged)
+    def activeQualityType(self):
+        if self._global_container_stack:
+            quality = self._global_container_stack.findContainer(type = "quality")
+            if quality:
+                return quality.getMetaDataEntry("quality_type")
+        return ""
+
+    @pyqtProperty(str, notify = activeQualityChanged)
+    def activeQualityChangesId(self):
+        if self._global_container_stack:
+            changes = self._global_container_stack.findContainer(type = "quality_changes")
+            if changes:
+                return changes.getId()
+        return ""
+
     ## Check if a container is read_only
     ## Check if a container is read_only
     @pyqtSlot(str, result = bool)
     @pyqtSlot(str, result = bool)
     def isReadOnly(self, container_id):
     def isReadOnly(self, container_id):
@@ -446,77 +480,13 @@ class MachineManager(QObject):
             if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
             if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
                 extruder_stack.getTop().setProperty(key, "value", new_value)
                 extruder_stack.getTop().setProperty(key, "value", new_value)
 
 
-    @pyqtSlot(str, result=str)
-    def duplicateContainer(self, container_id):
-        if not self._active_container_stack:
-            return ""
-        containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
-        if containers:
-            new_name = self._createUniqueName("quality", "", containers[0].getName(), catalog.i18nc("@label", "Custom profile"))
-
-            new_container = containers[0].duplicate(new_name, new_name)
-
-            UM.Settings.ContainerRegistry.getInstance().addContainer(new_container)
-
-            return new_name
-
-        return ""
-
-    @pyqtSlot(str, str)
-    def renameQualityContainer(self, container_id, new_name):
-        containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id, type = "quality")
-        if containers:
-            new_name = self._createUniqueName("quality", containers[0].getName(), new_name,
-                                              catalog.i18nc("@label", "Custom profile"))
-
-            if containers[0].getName() == new_name:
-                # Nothing to do.
-                return
-
-            # As we also want the id of the container to be changed (so that profile name is the name of the file
-            # on disk. We need to create a new instance and remove it (so the old file of the container is removed)
-            # If we don't do that, we might get duplicates & other weird issues.
-            new_container = UM.Settings.InstanceContainer("")
-            new_container.deserialize(containers[0].serialize())
-
-            # Actually set the name
-            new_container.setName(new_name)
-            new_container._id = new_name  # Todo: Fix proper id change function for this.
-
-            # Add the "new" container.
-            UM.Settings.ContainerRegistry.getInstance().addContainer(new_container)
-
-            # Ensure that the renamed profile is saved -before- we remove the old profile.
-            Application.getInstance().saveSettings()
-
-            # Actually set & remove new / old quality.
-            self.setActiveQuality(new_name)
-            self.removeQualityContainer(containers[0].getId())
-
-    @pyqtSlot(str)
-    def removeQualityContainer(self, container_id):
-        containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
-        if not containers or not self._active_container_stack:
-            return
-
-        # If the container that is being removed is the currently active container, set another machine as the active container
-        activate_new_container = container_id == self.activeQualityId
-
-        UM.Settings.ContainerRegistry.getInstance().removeContainer(container_id)
-
-        if activate_new_container:
-            definition_id = "fdmprinter" if not self.filterQualityByMachine else self.activeDefinitionId
-            containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "quality", definition = definition_id)
-            if containers:
-                self.setActiveQuality(containers[0].getId())
-                self.activeQualityChanged.emit()
-
     @pyqtSlot(str)
     @pyqtSlot(str)
     def setActiveMaterial(self, material_id):
     def setActiveMaterial(self, material_id):
         containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = material_id)
         containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = material_id)
         if not containers or not self._active_container_stack:
         if not containers or not self._active_container_stack:
             return
             return
 
 
+        old_variant = self._active_container_stack.findContainer({"type":"variant"})
         old_material = self._active_container_stack.findContainer({"type":"material"})
         old_material = self._active_container_stack.findContainer({"type":"material"})
         old_quality = self._active_container_stack.findContainer({"type": "quality"})
         old_quality = self._active_container_stack.findContainer({"type": "quality"})
         if old_material:
         if old_material:
@@ -531,7 +501,7 @@ class MachineManager(QObject):
             if old_quality:
             if old_quality:
                 preferred_quality_name = old_quality.getName()
                 preferred_quality_name = old_quality.getName()
 
 
-            self.setActiveQuality(self._updateQualityContainer(self._global_container_stack.getBottom(), containers[0], preferred_quality_name).id)
+            self.setActiveQuality(self._updateQualityContainer(self._global_container_stack.getBottom(), old_variant, containers[0], preferred_quality_name).id)
         else:
         else:
             Logger.log("w", "While trying to set the active material, no material was found to replace.")
             Logger.log("w", "While trying to set the active material, no material was found to replace.")
 
 
@@ -568,7 +538,8 @@ class MachineManager(QObject):
             quality_container = containers[0]
             quality_container = containers[0]
         elif container_type == "quality_changes":
         elif container_type == "quality_changes":
             quality_changes_container = containers[0]
             quality_changes_container = containers[0]
-            containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = quality_changes_container.getMetaDataEntry("quality"))
+            containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(
+                quality_type = quality_changes_container.getMetaDataEntry("quality"))
             if not containers:
             if not containers:
                 Logger.log("e", "Could not find quality %s for changes %s, not changing quality", quality_changes_container.getMetaDataEntry("quality"), quality_changes_container.getId())
                 Logger.log("e", "Could not find quality %s for changes %s, not changing quality", quality_changes_container.getMetaDataEntry("quality"), quality_changes_container.getId())
                 return
                 return
@@ -577,14 +548,27 @@ class MachineManager(QObject):
             Logger.log("e", "Tried to set quality to a container that is not of the right type")
             Logger.log("e", "Tried to set quality to a container that is not of the right type")
             return
             return
 
 
-        stacks = [ s for s in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()) ]
-        stacks.insert(0, self._global_container_stack)
+        quality_type = quality_container.getMetaDataEntry("quality_type")
+        if not quality_type:
+            quality_type = quality_changes_container.getName()
 
 
-        for stack in stacks:
+        for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
             extruder_id = stack.getId() if stack != self._global_container_stack else None
             extruder_id = stack.getId() if stack != self._global_container_stack else None
-            stack_quality = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(name = quality_container.getName(), extruder = extruder_id)
+
+            criteria = { "quality_type": quality_type, "extruder": extruder_id }
+
+            if self._global_container_stack.getMetaDataEntry("has_machine_quality"):
+                material = stack.findContainer(type = "material")
+                criteria["material"] = material.getId()
+
+            stack_quality = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
             if not stack_quality:
             if not stack_quality:
-                stack_quality = quality_container
+                criteria.pop("extruder")
+                stack_quality = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
+                if not stack_quality:
+                    stack_quality = quality_container
+                else:
+                    stack_quality = stack_quality[0]
             else:
             else:
                 stack_quality = stack_quality[0]
                 stack_quality = stack_quality[0]
 
 
@@ -624,7 +608,11 @@ class MachineManager(QObject):
             pass
             pass
         elif button == QMessageBox.No:
         elif button == QMessageBox.No:
             # No, discard the settings in the user profile
             # No, discard the settings in the user profile
-            self.clearUserSettings()
+            global_stack = Application.getInstance().getGlobalContainerStack()
+            for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
+                extruder.getTop().clear()
+
+            global_stack.getTop().clear()
 
 
     @pyqtProperty(str, notify = activeVariantChanged)
     @pyqtProperty(str, notify = activeVariantChanged)
     def activeVariantName(self):
     def activeVariantName(self):
@@ -653,6 +641,16 @@ class MachineManager(QObject):
 
 
         return ""
         return ""
 
 
+    ##  Gets how the active definition calls variants
+    #   Caveat: per-definition-variant-title is currently not translated (though the fallback is)
+    @pyqtProperty(str, notify = globalContainerChanged)
+    def activeDefinitionVariantsName(self):
+        fallback_title = catalog.i18nc("@label", "Nozzle")
+        if self._global_container_stack:
+            return self._global_container_stack.getBottom().getMetaDataEntry("variants_name", fallback_title)
+
+        return fallback_title
+
     @pyqtSlot(str, str)
     @pyqtSlot(str, str)
     def renameMachine(self, machine_id, new_name):
     def renameMachine(self, machine_id, new_name):
         containers = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
         containers = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
@@ -776,7 +774,8 @@ class MachineManager(QObject):
 
 
         return self._empty_material_container
         return self._empty_material_container
 
 
-    def _updateQualityContainer(self, definition, material_container = None, preferred_quality_name = None):
+    def _updateQualityContainer(self, definition, variant_container, material_container = None, preferred_quality_name = None):
+        container_registry = UM.Settings.ContainerRegistry.getInstance()
         search_criteria = { "type": "quality" }
         search_criteria = { "type": "quality" }
 
 
         if definition.getMetaDataEntry("has_machine_quality"):
         if definition.getMetaDataEntry("has_machine_quality"):
@@ -787,23 +786,42 @@ class MachineManager(QObject):
         else:
         else:
             search_criteria["definition"] = "fdmprinter"
             search_criteria["definition"] = "fdmprinter"
 
 
-        if preferred_quality_name:
+        if preferred_quality_name and preferred_quality_name != "empty":
             search_criteria["name"] = preferred_quality_name
             search_criteria["name"] = preferred_quality_name
         else:
         else:
             preferred_quality = definition.getMetaDataEntry("preferred_quality")
             preferred_quality = definition.getMetaDataEntry("preferred_quality")
             if preferred_quality:
             if preferred_quality:
                 search_criteria["id"] = preferred_quality
                 search_criteria["id"] = preferred_quality
 
 
-        containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
+        containers = container_registry.findInstanceContainers(**search_criteria)
         if containers:
         if containers:
             return containers[0]
             return containers[0]
 
 
+        if "material" in search_criteria:
+            # If a quality for this specific material cannot be found, try finding qualities for a generic version of the material
+            material_search_criteria = { "type": "material", "material": material_container.getMetaDataEntry("material"), "color_name": "Generic" }
+            if definition.getMetaDataEntry("has_machine_quality"):
+                material_search_criteria["definition"] = definition.id
+
+                if definition.getMetaDataEntry("has_variants") and variant_container:
+                    material_search_criteria["variant"] = variant_container.id
+            else:
+                material_search_criteria["definition"] = "fdmprinter"
+
+            material_containers = container_registry.findInstanceContainers(**material_search_criteria)
+            if material_containers:
+                search_criteria["material"] = material_containers[0].getId()
+
+                containers = container_registry.findInstanceContainers(**search_criteria)
+                if containers:
+                    return containers[0]
+
         if "name" in search_criteria or "id" in search_criteria:
         if "name" in search_criteria or "id" in search_criteria:
             # If a quality by this name can not be found, try a wider set of search criteria
             # If a quality by this name can not be found, try a wider set of search criteria
             search_criteria.pop("name", None)
             search_criteria.pop("name", None)
             search_criteria.pop("id", None)
             search_criteria.pop("id", None)
 
 
-            containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
+            containers = container_registry.findInstanceContainers(**search_criteria)
             if containers:
             if containers:
                 return containers[0]
                 return containers[0]
 
 

+ 173 - 0
cura/Settings/QualitySettingsModel.py

@@ -0,0 +1,173 @@
+# Copyright (c) 2016 Ultimaker B.V.
+# Cura is released under the terms of the AGPLv3 or higher.
+
+import collections
+
+from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
+
+import UM.Application
+import UM.Logger
+import UM.Qt
+import UM.Settings
+
+
+class QualitySettingsModel(UM.Qt.ListModel.ListModel):
+    KeyRole = Qt.UserRole + 1
+    LabelRole = Qt.UserRole + 2
+    UnitRole = Qt.UserRole + 3
+    ProfileValueRole = Qt.UserRole + 4
+    UserValueRole = Qt.UserRole + 5
+    CategoryRole = Qt.UserRole + 6
+
+    def __init__(self, parent = None):
+        super().__init__(parent = parent)
+
+        self._extruder_id = None
+        self._quality = None
+        self._material = None
+
+        self.addRoleName(self.KeyRole, "key")
+        self.addRoleName(self.LabelRole, "label")
+        self.addRoleName(self.UnitRole, "unit")
+        self.addRoleName(self.ProfileValueRole, "profile_value")
+        self.addRoleName(self.UserValueRole, "user_value")
+        self.addRoleName(self.CategoryRole, "category")
+
+    def setExtruderId(self, extruder_id):
+        if extruder_id != self._extruder_id:
+            self._extruder_id = extruder_id
+            self._update()
+            self.extruderIdChanged.emit()
+
+    extruderIdChanged = pyqtSignal()
+    @pyqtProperty(str, fset = setExtruderId, notify = extruderIdChanged)
+    def extruderId(self):
+        return self._extruder_id
+
+    def setQuality(self, quality):
+        if quality != self._quality:
+            self._quality = quality
+            self._update()
+            self.qualityChanged.emit()
+
+    qualityChanged = pyqtSignal()
+    @pyqtProperty(str, fset = setQuality, notify = qualityChanged)
+    def quality(self):
+        return self._quality
+
+    def setMaterial(self, material):
+        if material != self._material:
+            self._material = material
+            self._update()
+            self.materialChanged.emit()
+
+    materialChanged = pyqtSignal()
+    @pyqtProperty(str, fset = setMaterial, notify = materialChanged)
+    def material(self):
+        return self._material
+
+    def _update(self):
+        if not self._quality:
+            return
+
+        self.clear()
+
+        settings = collections.OrderedDict()
+        definition_container = UM.Application.getInstance().getGlobalContainerStack().getBottom()
+
+        containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = self._quality)
+        if not containers:
+            UM.Logger.log("w", "Could not find a quality container with id %s", self._quality)
+            return
+
+        quality_container = None
+        quality_changes_container = None
+
+        if containers[0].getMetaDataEntry("type") == "quality":
+            quality_container = containers[0]
+        else:
+            quality_changes_container = containers[0]
+
+            criteria = {
+                "type": "quality",
+                "quality_type": quality_changes_container.getMetaDataEntry("quality"),
+                "definition": quality_changes_container.getDefinition().getId()
+            }
+
+            if self._material:
+                criteria["material"] = self._material
+
+            quality_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
+            if not quality_container:
+                UM.Logger.log("w", "Could not find a quality container matching quality changes %s", quality_changes_container.getId())
+                return
+            quality_container = quality_container[0]
+
+        quality_type = quality_container.getMetaDataEntry("quality_type")
+        definition_id = quality_container.getDefinition().getId()
+
+        criteria = { "type": "quality", "quality_type": quality_type, "definition": definition_id }
+
+        if self._material:
+            criteria["material"] = self._material
+
+        criteria["extruder"] = self._extruder_id
+
+        containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
+        if not containers:
+            # Try again, this time without extruder
+            new_criteria = criteria.copy()
+            new_criteria.pop("extruder")
+            containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**new_criteria)
+
+        if not containers:
+            # Try again, this time without material
+            criteria.pop("material")
+            containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
+
+        if not containers:
+            # Try again, this time without material or extruder
+            criteria.pop("extruder") # "material" has already been popped
+            containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
+
+        if not containers:
+            UM.Logger.log("Could not find any quality containers matching the search criteria %s" % str(criteria))
+            return
+
+        if quality_changes_container:
+            criteria = {"type": "quality_changes", "quality": quality_type, "extruder": self._extruder_id, "definition": definition_id }
+            changes = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
+            if changes:
+                containers.extend(changes)
+
+        current_category = ""
+        for definition in definition_container.findDefinitions():
+            if definition.type == "category":
+                current_category = definition.label
+                continue
+
+            profile_value = None
+            for container in containers:
+                new_value = container.getProperty(definition.key, "value")
+                if new_value:
+                    profile_value = new_value
+
+            user_value = None
+            if not self._extruder_id:
+                user_value = UM.Application.getInstance().getGlobalContainerStack().getTop().getProperty(definition.key, "value")
+            else:
+                extruder_stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_id)
+                if extruder_stack:
+                    user_value = extruder_stack[0].getTop().getProperty(definition.key, "value")
+
+            if not profile_value and not user_value:
+                continue
+
+            self.appendItem({
+                "key": definition.key,
+                "label": definition.label,
+                "unit": definition.unit,
+                "profile_value": "" if profile_value is None else str(profile_value),  # it is for display only
+                "user_value": "" if user_value is None else str(user_value),
+                "category": current_category
+            })

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