Browse Source

Merge remote-tracking branch 'upstream/master' into master-CURA-1696

Thomas Karl Pietrowski 8 years ago
parent
commit
aa2ba26a82

+ 1 - 1
CMakeLists.txt

@@ -1,5 +1,5 @@
 
-project(cura)
+project(cura NONE)
 cmake_minimum_required(VERSION 2.8.12)
 
 include(UraniumTranslationTools)

+ 62 - 31
cura/BuildVolume.py

@@ -74,10 +74,14 @@ class BuildVolume(SceneNode):
         self._adhesion_type = None
         self._platform = Platform(self)
 
-        self._active_container_stack = None
+        self._global_container_stack = None
         Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged)
         self._onGlobalContainerStackChanged()
 
+        self._active_extruder_stack = None
+        ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
+        self._onActiveExtruderStackChanged()
+
     def setWidth(self, width):
         if width: self._width = width
 
@@ -208,22 +212,22 @@ class BuildVolume(SceneNode):
             "@info:status",
             "The build volume height has been reduced due to the value of the"
             " \"Print Sequence\" setting to prevent the gantry from colliding"
-            " with printed objects."), lifetime=10).show()
+            " with printed models.")).show()
 
     def getRaftThickness(self):
         return self._raft_thickness
 
     def _updateRaftThickness(self):
         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
         if self._adhesion_type == "raft":
             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
         if old_raft_thickness != self._raft_thickness:
@@ -231,41 +235,52 @@ class BuildVolume(SceneNode):
             self.raftThicknessChanged.emit()
 
     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:
-                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._updateRaftThickness()
 
             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):
         if property_name != "value":
             return
 
         rebuild_me = False
         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":
-                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:
-                self._height = self._active_container_stack.getProperty("machine_height", "value")
+                self._height = self._global_container_stack.getProperty("machine_height", "value")
             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()
             rebuild_me = True
 
@@ -277,19 +292,33 @@ class BuildVolume(SceneNode):
             self.rebuild()
 
     def _updateDisallowedAreas(self):
-        if not self._active_container_stack:
+        if not self._global_container_stack:
             return
 
         disallowed_areas = copy.deepcopy(
-            self._active_container_stack.getProperty("machine_disallowed_areas", "value"))
+            self._global_container_stack.getProperty("machine_disallowed_areas", "value"))
         areas = []
 
+        machine_width = self._global_container_stack.getProperty("machine_width", "value")
+        machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
+
+        # Add prime tower location as disallowed area.
+        if self._global_container_stack.getProperty("prime_tower_enable", "value") == True:
+            prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value")
+            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 - prime_tower_size, prime_tower_y - prime_tower_size],
+                [prime_tower_x, prime_tower_y - prime_tower_size],
+                [prime_tower_x, prime_tower_y],
+                [prime_tower_x - prime_tower_size, prime_tower_y],
+            ])
+
         # Add extruder prime locations as disallowed areas.
         # Probably needs some rework after coordinate system change.
         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:
             extruder_prime_pos_x = single_extruder.getProperty("extruder_prime_pos_x", "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],
             ])
 
-        bed_adhesion_size = self._getBedAdhesionSize(self._active_container_stack)
+        bed_adhesion_size = self._getBedAdhesionSize(self._global_container_stack)
 
         if disallowed_areas:
             # 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.
         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([
                 [-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"]
     _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"]

+ 36 - 14
cura/CuraApplication.py

@@ -13,7 +13,6 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
 from UM.Mesh.ReadMeshJob import ReadMeshJob
 from UM.Logger import Logger
 from UM.Preferences import Preferences
-from UM.Platform import Platform
 from UM.JobQueue import JobQueue
 from UM.SaveFile import SaveFile
 from UM.Scene.Selection import Selection
@@ -50,12 +49,12 @@ from PyQt5.QtGui import QColor, QIcon
 from PyQt5.QtWidgets import QMessageBox
 from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
 
-import platform
 import sys
 import os.path
 import numpy
 import copy
 import urllib
+
 numpy.seterr(all="ignore")
 
 try:
@@ -85,11 +84,12 @@ class CuraApplication(QtApplication):
         self._open_file_queue = []  # Files to open when plug-ins are loaded.
 
         # Need to do this before ContainerRegistry tries to load the machines
-        SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True)
-        SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True)
-        SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default = True)
-        SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True)
+        SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True, read_only = True)
+        SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True, read_only = True)
+        SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default = True, read_only = True)
+        SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True, read_only = True)
         SettingDefinition.addSupportedProperty("global_inherits_stack", DefinitionPropertyType.Function, default = "-1")
+        SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default = None)
         SettingDefinition.addSettingType("extruder", None, str, Validator)
 
         SettingFunction.registerOperator("extruderValues", cura.Settings.ExtruderManager.getExtruderValues)
@@ -182,6 +182,10 @@ class CuraApplication(QtApplication):
         empty_quality_container._id = "empty_quality"
         empty_quality_container.addMetaDataEntry("type", "quality")
         ContainerRegistry.getInstance().addContainer(empty_quality_container)
+        empty_quality_changes_container = copy.deepcopy(empty_container)
+        empty_quality_changes_container._id = "empty_quality_changes"
+        empty_quality_changes_container.addMetaDataEntry("type", "quality_changes")
+        ContainerRegistry.getInstance().addContainer(empty_quality_changes_container)
 
         ContainerRegistry.getInstance().load()
 
@@ -237,11 +241,17 @@ class CuraApplication(QtApplication):
                 raft_airgap
                 layer_0_z_overlap
                 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
             blackmagic
                 print_sequence
                 infill_mesh
-                dual
             experimental
         """.replace("\n", ";").replace(" ", ""))
 
@@ -302,7 +312,7 @@ class CuraApplication(QtApplication):
             path = None
             if instance_type == "material":
                 path = Resources.getStoragePath(self.ResourceTypes.MaterialInstanceContainer, file_name)
-            elif instance_type == "quality":
+            elif instance_type == "quality" or instance_type == "quality_changes":
                 path = Resources.getStoragePath(self.ResourceTypes.QualityInstanceContainer, file_name)
             elif instance_type == "user":
                 path = Resources.getStoragePath(self.ResourceTypes.UserInstanceContainer, file_name)
@@ -470,6 +480,7 @@ class CuraApplication(QtApplication):
 
         qmlRegisterType(cura.Settings.ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
         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)
 
@@ -783,12 +794,19 @@ class CuraApplication(QtApplication):
             return
         multi_material_decorator = MultiMaterialDecorator.MultiMaterialDecorator()
         group_node.addDecorator(multi_material_decorator)
-        # Reset the position of each node
-        for node in group_node.getChildren():
-            new_position = node.getMeshData().getCenterPosition()
-            new_position = new_position.scale(node.getScale())
-            node.setPosition(new_position)
-        
+
+        # Compute the center of the objects when their origins are aligned.
+        object_centers = [node.getMeshData().getCenterPosition().scale(node.getScale()) for node in group_node.getChildren()]
+        middle_x = sum([v.x for v in object_centers]) / len(object_centers)
+        middle_y = sum([v.y for v in object_centers]) / len(object_centers)
+        middle_z = sum([v.z for v in object_centers]) / len(object_centers)
+        offset = Vector(middle_x, middle_y, middle_z)
+
+        # Move each node to the same position.
+        for center, node in zip(object_centers, group_node.getChildren()):
+            # Align the object and also apply the offset to center it inside the group.
+            node.setPosition(center - offset)
+
         # Use the previously found center of the group bounding box as the new location of the group
         group_node.setPosition(group_node.getBoundingBox().center)
 
@@ -914,3 +932,7 @@ class CuraApplication(QtApplication):
         self._additional_components[area_id].append(component)
 
         self.additionalComponentsChanged.emit(area_id)
+
+    @pyqtSlot(str)
+    def log(self, msg):
+        Logger.log("d", msg)

+ 10 - 1
cura/PlatformPhysics.py

@@ -31,6 +31,7 @@ class PlatformPhysics:
         self._change_timer.timeout.connect(self._onChangeTimerFinished)
 
         Preferences.getInstance().addPreference("physics/automatic_push_free", True)
+        Preferences.getInstance().addPreference("physics/automatic_drop_down", True)
 
     def _onSceneChanged(self, source):
         self._change_timer.start()
@@ -41,6 +42,10 @@ class PlatformPhysics:
 
         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):
             if node is root or type(node) is not SceneNode or node.getBoundingBox() is None:
                 continue
@@ -64,7 +69,7 @@ class PlatformPhysics:
 
             # Move it downwards if bottom is above platform
             move_vector = Vector()
-            if not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down
+            if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down
                 z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
                 move_vector = move_vector.set(y=-bbox.bottom + z_offset)
 
@@ -91,6 +96,9 @@ class PlatformPhysics:
                     if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
                         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.
                     head_hull = node.callDecoration("getConvexHullHead")
                     if head_hull:
@@ -125,6 +133,7 @@ class PlatformPhysics:
                     node._outside_buildarea = True
 
             if not Vector.Null.equals(move_vector, epsilon=1e-5):
+                transformed_nodes.append(node)
                 op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
                 op.push()
 

+ 12 - 2
cura/PrinterOutputDevice.py

@@ -1,10 +1,15 @@
+from UM.i18n import i18nCatalog
 from UM.OutputDevice.OutputDevice import OutputDevice
 from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
+from PyQt5.QtWidgets import QMessageBox
+
 from enum import IntEnum  # For the connection state tracking.
 from UM.Logger import Logger
-
+from UM.Application import Application
 from UM.Signal import signalemitter
 
+i18n_catalog = i18nCatalog("cura")
+
 ##  Printer output device adds extra interface options on top of output device.
 #
 #   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.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
     def connect(self):
@@ -329,7 +339,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
         return self._head_z
 
     ##  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):
         position_changed = False
         if self._head_x != x:

+ 2 - 2
cura/ProfileWriter.py

@@ -18,8 +18,8 @@ class ProfileWriter(PluginObject):
     #   The profile writer may write its own file format to the specified file.
     #
     #   \param path \type{string} The file to output to.
-    #   \param profile \type{Profile} The profile to write to the file.
+    #   \param profiles \type{Profile} or \type{List} The profile(s) to write to the file.
     #   \return \code True \endcode if the writing was successful, or \code
     #   False \endcode if it wasn't.
-    def write(self, path, node):
+    def write(self, path, profiles):
         raise NotImplementedError("Profile writer plugin was not correctly implemented. No write was specified.")

+ 342 - 26
cura/Settings/ContainerManager.py

@@ -14,6 +14,8 @@ import UM.Platform
 import UM.MimeTypeDatabase
 import UM.Logger
 
+import cura.Settings
+
 from UM.MimeTypeDatabase import MimeTypeNotFoundError
 
 from UM.i18n import i18nCatalog
@@ -28,7 +30,9 @@ class ContainerManager(QObject):
     def __init__(self, parent = None):
         super().__init__(parent)
 
-        self._registry = UM.Settings.ContainerRegistry.getInstance()
+        self._container_registry = UM.Settings.ContainerRegistry.getInstance()
+        self._machine_manager = UM.Application.getInstance().getMachineManager()
+
         self._container_name_filters = {}
 
     ##  Create a duplicate of the specified container
@@ -41,7 +45,7 @@ class ContainerManager(QObject):
     #   \return The ID of the new container, or an empty string if duplication failed.
     @pyqtSlot(str, result = str)
     def duplicateContainer(self, container_id):
-        containers = self._registry.findContainers(None, id = container_id)
+        containers = self._container_registry.findContainers(None, id = container_id)
         if not containers:
             UM.Logger.log("w", "Could duplicate container %s because it was not found.", container_id)
             return ""
@@ -49,7 +53,7 @@ class ContainerManager(QObject):
         container = containers[0]
 
         new_container = None
-        new_name = self._registry.uniqueName(container.getName())
+        new_name = self._container_registry.uniqueName(container.getName())
         # Only InstanceContainer has a duplicate method at the moment.
         # So fall back to serialize/deserialize when no duplicate method exists.
         if hasattr(container, "duplicate"):
@@ -60,7 +64,7 @@ class ContainerManager(QObject):
             new_container.setName(new_name)
 
         if new_container:
-            self._registry.addContainer(new_container)
+            self._container_registry.addContainer(new_container)
 
         return new_container.getId()
 
@@ -73,24 +77,24 @@ class ContainerManager(QObject):
     #   \return True if successful, False if not.
     @pyqtSlot(str, str, str, result = bool)
     def renameContainer(self, container_id, new_id, new_name):
-        containers = self._registry.findContainers(None, id = container_id)
+        containers = self._container_registry.findContainers(None, id = container_id)
         if not containers:
             UM.Logger.log("w", "Could rename container %s because it was not found.", container_id)
             return False
 
         container = containers[0]
         # First, remove the container from the registry. This will clean up any files related to the container.
-        self._registry.removeContainer(container)
+        self._container_registry.removeContainer(container)
 
         # Ensure we have a unique name for the container
-        new_name = self._registry.uniqueName(new_name)
+        new_name = self._container_registry.uniqueName(new_name)
 
         # Then, update the name and ID of the container
         container.setName(new_name)
         container._id = new_id # TODO: Find a nicer way to set a new, unique ID
 
         # Finally, re-add the container so it will be properly serialized again.
-        self._registry.addContainer(container)
+        self._container_registry.addContainer(container)
 
         return True
 
@@ -101,12 +105,12 @@ class ContainerManager(QObject):
     #   \return True if the container was successfully removed, False if not.
     @pyqtSlot(str, result = bool)
     def removeContainer(self, container_id):
-        containers = self._registry.findContainers(None, id = container_id)
+        containers = self._container_registry.findContainers(None, id = container_id)
         if not containers:
             UM.Logger.log("w", "Could remove container %s because it was not found.", container_id)
             return False
 
-        self._registry.removeContainer(containers[0].getId())
+        self._container_registry.removeContainer(containers[0].getId())
 
         return True
 
@@ -121,26 +125,25 @@ class ContainerManager(QObject):
     #   \return True if successfully merged, False if not.
     @pyqtSlot(str, result = bool)
     def mergeContainers(self, merge_into_id, merge_id):
-        containers = self._registry.findContainers(None, id = merge_into_id)
+        containers = self._container_registry.findContainers(None, id = merge_into_id)
         if not containers:
             UM.Logger.log("w", "Could merge into container %s because it was not found.", merge_into_id)
             return False
 
         merge_into = containers[0]
 
-        containers = self._registry.findContainers(None, id = merge_id)
+        containers = self._container_registry.findContainers(None, id = merge_id)
         if not containers:
             UM.Logger.log("w", "Could not merge container %s because it was not found", merge_id)
             return False
 
         merge = containers[0]
 
-        if type(merge) != type(merge_into):
+        if not isinstance(merge, type(merge_into)):
             UM.Logger.log("w", "Cannot merge two containers of different types")
             return False
 
-        for key in merge.getAllKeys():
-            merge_into.setProperty(key, "value", merge.getProperty(key, "value"))
+        self._performMerge(merge_into, merge)
 
         return True
 
@@ -151,7 +154,7 @@ class ContainerManager(QObject):
     #   \return True if successful, False if not.
     @pyqtSlot(str, result = bool)
     def clearContainer(self, container_id):
-        containers = self._registry.findContainers(None, id = container_id)
+        containers = self._container_registry.findContainers(None, id = container_id)
         if not containers:
             UM.Logger.log("w", "Could clear container %s because it was not found.", container_id)
             return False
@@ -178,9 +181,9 @@ class ContainerManager(QObject):
     #   \return True if successful, False if not.
     @pyqtSlot(str, str, str, result = bool)
     def setContainerMetaDataEntry(self, container_id, entry_name, entry_value):
-        containers = UM.Settings.ContainerRegistry.getInstance().findContainers(None, id = container_id)
+        containers = self._container_registry.findContainers(None, id = container_id)
         if not containers:
-            UM.Logger.log("w", "Could set metadata of container %s because it was not found.", container_id)
+            UM.Logger.log("w", "Could not set metadata of container %s because it was not found.", container_id)
             return False
 
         container = containers[0]
@@ -209,6 +212,24 @@ class ContainerManager(QObject):
 
         return True
 
+    ##  Set the name of the specified container.
+    @pyqtSlot(str, str, result = bool)
+    def setContainerName(self, container_id, new_name):
+        containers = self._container_registry.findContainers(None, id = container_id)
+        if not containers:
+            UM.Logger.log("w", "Could not set name of container %s because it was not found.", container_id)
+            return False
+
+        container = containers[0]
+
+        if container.isReadOnly():
+            UM.Logger.log("w", "Cannot set name of read-only container %s.", container_id)
+            return False
+
+        container.setName(new_name)
+
+        return True
+
     ##  Find instance containers matching certain criteria.
     #
     #   This effectively forwards to ContainerRegistry::findInstanceContainers.
@@ -219,11 +240,20 @@ class ContainerManager(QObject):
     @pyqtSlot("QVariantMap", result = "QVariantList")
     def findInstanceContainers(self, criteria):
         result = []
-        for entry in self._registry.findInstanceContainers(**criteria):
+        for entry in self._container_registry.findInstanceContainers(**criteria):
             result.append(entry.getId())
 
         return result
 
+    @pyqtSlot(str, result = bool)
+    def isContainerUsed(self, container_id):
+        UM.Logger.log("d", "Checking if container %s is currently used in the active stacks", container_id)
+        for stack in cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
+            if container_id in [child.getId() for child in stack.getContainers()]:
+                UM.Logger.log("d", "The container is in use by %s", stack.getId())
+                return True
+        return False
+
     ##  Get a list of string that can be used as name filters for a Qt File Dialog
     #
     #   This will go through the list of available container types and generate a list of strings
@@ -276,7 +306,7 @@ class ContainerManager(QObject):
         else:
             mime_type = self._container_name_filters[file_type]["mime"]
 
-        containers = UM.Settings.ContainerRegistry.getInstance().findContainers(None, id = container_id)
+        containers = self._container_registry.findContainers(None, id = container_id)
         if not containers:
             return { "status": "error", "message": "Container not found"}
         container = containers[0]
@@ -334,7 +364,7 @@ class ContainerManager(QObject):
             return { "status": "error", "message": "Could not find a container to handle the specified file."}
 
         container_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_url)))
-        container_id = UM.Settings.ContainerRegistry.getInstance().uniqueName(container_id)
+        container_id = self._container_registry.uniqueName(container_id)
 
         container = container_type(container_id)
 
@@ -346,10 +376,220 @@ class ContainerManager(QObject):
 
         container.setName(container_id)
 
-        UM.Settings.ContainerRegistry.getInstance().addContainer(container)
+        self._container_registry.addContainer(container)
 
         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)
+    def updateQualityChanges(self):
+        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        if not global_stack:
+            return False
+
+        self._machine_manager.blurSettings.emit()
+
+        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():
+                UM.Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
+                continue
+
+            self._performMerge(quality_changes, stack.getTop())
+
+        self._machine_manager.activeQualityChanged.emit()
+
+        return True
+
+    ##  Clear the top-most (user) containers of the active stacks.
+    @pyqtSlot()
+    def clearUserContainers(self):
+        self._machine_manager.blurSettings.emit()
+
+        # 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(str, result = bool)
+    def createQualityChanges(self, base_name):
+        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        if not global_stack:
+            return False
+
+        active_quality_name = self._machine_manager.activeQualityName
+        if active_quality_name == "":
+            UM.Logger.log("w", "No quality container found in stack %s, cannot create profile", global_stack.getId())
+            return False
+
+        self._machine_manager.blurSettings.emit()
+
+        if base_name is None:
+            base_name = active_quality_name
+        unique_name = self._container_registry.uniqueName(base_name)
+
+        # 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()
+            quality_container = stack.findContainer(type = "quality")
+            quality_changes_container = stack.findContainer(type = "quality_changes")
+            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())
+                continue
+
+            new_changes = self._createQualityChanges(quality_container, unique_name, stack.getId())
+            self._performMerge(new_changes, user_container)
+
+            self._container_registry.addContainer(new_changes)
+            stack.replaceContainer(stack.getContainerIndex(quality_changes_container), new_changes)
+
+        self._machine_manager.activeQualityChanged.emit()
+        return True
+
+    ##  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):
+        UM.Logger.log("d", "Attempting to remove the quality change containers with name %s", quality_name)
+        containers_found = False
+
+        if not quality_name:
+            return containers_found  # Without a name we will never find a container to remove.
+
+        # If the container that is being removed is the currently active quality, set another quality as the active quality
+        activate_quality = quality_name == self._machine_manager.activeQualityName
+        activate_quality_type = None
+
+        for container in self._getFilteredContainers(name = quality_name, type = "quality_changes"):
+            containers_found = True
+            if activate_quality and not activate_quality_type:
+                activate_quality_type = container.getMetaDataEntry("quality")
+            self._container_registry.removeContainer(container.getId())
+
+        if not containers_found:
+            UM.Logger.log("d", "Unable to remove quality containers, as we did not find any by the name of %s", quality_name)
+
+        elif activate_quality:
+            definition_id = "fdmprinter" if not self._machine_manager.filterQualityByMachine else self._machine_manager.activeDefinitionId
+            containers = self._container_registry.findInstanceContainers(type = "quality", definition = definition_id, quality_type = activate_quality_type)
+            if containers:
+                self._machine_manager.setActiveQuality(containers[0].getId())
+                self._machine_manager.activeQualityChanged.emit()
+
+        return containers_found
+
+    ##  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):
+        UM.Logger.log("d", "User requested QualityChanges container rename of %s to %s", quality_name, new_name)
+        if not quality_name or not new_name:
+            return False
+
+        if quality_name == new_name:
+            UM.Logger.log("w", "Unable to rename %s to %s, because they are the same.", quality_name, new_name)
+            return True
+
+        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        if not global_stack:
+            return False
+
+        self._machine_manager.blurSettings.emit()
+
+        new_name = self._container_registry.uniqueName(new_name)
+
+        container_registry = self._container_registry
+        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))
+
+        self._machine_manager.activeQualityChanged.emit()
+        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, str, result = str)
+    def duplicateQualityOrQualityChanges(self, quality_name, base_name):
+        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        if not global_stack or not quality_name:
+            return ""
+        UM.Logger.log("d", "Attempting to duplicate the quality %s", quality_name)
+        containers = self._container_registry.findInstanceContainers(name = quality_name)
+        if not containers:
+            UM.Logger.log("d", "Unable to duplicate the quality %s, because it doesn't exist.", quality_name)
+            return ""
+
+        if base_name is None:
+            base_name = quality_name
+
+        new_name = self._container_registry.uniqueName(base_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())
+                    self._container_registry.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)
+                self._container_registry.addContainer(new_container)
+        else:
+            return ""
+
+        return new_name
+
+    # Factory function, used by QML
+    @staticmethod
+    def createContainerManager(engine, js_engine):
+        return ContainerManager()
+
+    def _performMerge(self, merge_into, merge):
+        assert isinstance(merge, type(merge_into))
+
+        if merge == merge_into:
+            return
+
+        for key in merge.getAllKeys():
+            merge_into.setProperty(key, "value", merge.getProperty(key, "value"))
+
+        merge.clear()
+
     def _updateContainerNameFilters(self):
         self._container_name_filters = {}
         for plugin_id, container_type in UM.Settings.ContainerRegistry.getContainerTypes():
@@ -394,7 +634,83 @@ class ContainerManager(QObject):
             name_filter = "{0} ({1})".format(mime_type.comment, suffix_list)
             self._container_name_filters[name_filter] = entry
 
-    # Factory function, used by QML
-    @staticmethod
-    def createContainerManager(engine, js_engine):
-        return ContainerManager()
+    ##  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 = self._container_registry.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(self._container_registry.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

+ 4 - 3
cura/Settings/ContainerSettingsModel.py

@@ -31,7 +31,7 @@ class ContainerSettingsModel(ListModel):
             self._update()
 
     def _update(self):
-        self.clear()
+        items = []
 
         if len(self._container_ids) == 0:
             return
@@ -41,7 +41,6 @@ class ContainerSettingsModel(ListModel):
             keys = keys + list(container.getAllKeys())
 
         keys = list(set(keys)) # remove duplicate keys
-        keys.sort()
 
         for key in keys:
             definition = None
@@ -65,13 +64,15 @@ class ContainerSettingsModel(ListModel):
                 else:
                     values.append("")
 
-            self.appendItem({
+            items.append({
                 "key": key,
                 "values": values,
                 "label": definition.label,
                 "unit": definition.unit,
                 "category": category.label
             })
+        items.sort(key = lambda k: (k["category"], k["key"]))
+        self.setItems(items)
 
     ##  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

+ 16 - 12
cura/Settings/CuraContainerRegistry.py

@@ -58,12 +58,10 @@ class CuraContainerRegistry(ContainerRegistry):
 
     ##  Exports an profile to a file
     #
-    #   \param instance_id \type{str} the ID of the profile to export.
+    #   \param instance_ids \type{list} the IDs of the profiles to export.
     #   \param file_name \type{str} the full path and filename to export to.
     #   \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
-    def exportProfile(self, instance_id, file_name, file_type):
-        Logger.log('d', 'exportProfile instance_id: '+str(instance_id))
-
+    def exportProfile(self, instance_ids, file_name, file_type):
         # Parse the fileType to deduce what plugin can save the file format.
         # fileType has the format "<description> (*.<extension>)"
         split = file_type.rfind(" (*.")  # Find where the description ends and the extension starts.
@@ -82,16 +80,16 @@ class CuraContainerRegistry(ContainerRegistry):
                                               catalog.i18nc("@label", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
                 if result == QMessageBox.No:
                     return
-
-        containers = ContainerRegistry.getInstance().findInstanceContainers(id=instance_id)
-        if not containers:
-            return
-        container = containers[0]
+        found_containers = []
+        for instance_id in instance_ids:
+            containers = ContainerRegistry.getInstance().findInstanceContainers(id=instance_id)
+            if containers:
+                found_containers.append(containers[0])
 
         profile_writer = self._findProfileWriter(extension, description)
 
         try:
-            success = profile_writer.write(file_name, container)
+            success = profile_writer.write(file_name, found_containers)
         except Exception as e:
             Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
             m = Message(catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", file_name, str(e)), lifetime = 0)
@@ -130,6 +128,7 @@ class CuraContainerRegistry(ContainerRegistry):
             return { "status": "error", "message": catalog.i18nc("@info:status", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "Invalid path")}
 
         plugin_registry = PluginRegistry.getInstance()
+        container_registry = ContainerRegistry.getInstance()
         for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
             profile_reader = plugin_registry.getPluginObject(plugin_id)
             try:
@@ -146,7 +145,11 @@ class CuraContainerRegistry(ContainerRegistry):
                     return { "status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName()) }
                 else:
                     for profile in profile_or_list:
-                        self._configureProfile(profile, name_seed)
+                        profile.setDirty(True)  # Ensure the profiles are correctly saved
+                        if profile.getId() != "":
+                            container_registry.addContainer(profile)
+                        else:
+                            self._configureProfile(profile, name_seed)
 
                     if len(profile_or_list) == 1:
                         return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
@@ -160,7 +163,7 @@ class CuraContainerRegistry(ContainerRegistry):
     def _configureProfile(self, profile, name_seed):
         profile.setReadOnly(False)
 
-        new_name = self.createUniqueName("quality", "", name_seed, catalog.i18nc("@label", "Custom profile"))
+        new_name = self.createUniqueName("quality_changes", "", name_seed, catalog.i18nc("@label", "Custom profile"))
         profile.setName(new_name)
         profile._id = new_name
 
@@ -170,6 +173,7 @@ class CuraContainerRegistry(ContainerRegistry):
                 profile.addMetaDataEntry("material", self._activeMaterialId())
         else:
             profile.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0])
+
         ContainerRegistry.getInstance().addContainer(profile)
 
     ##  Gets a list of profile writer plugins

+ 41 - 6
cura/Settings/ExtruderManager.py

@@ -55,6 +55,13 @@ class ExtruderManager(QObject):
             map[position] = self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()][position].getId()
         return map
 
+    @pyqtSlot(str, result = str)
+    def getQualityChangesIdByExtruderStackId(self, id):
+        for position in self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()]:
+            extruder = self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()][position]
+            if extruder.getId() == id:
+                return extruder.findContainer(type = "quality_changes").getId()
+
     ##  The instance of the singleton pattern.
     #
     #   It's None if the extruder manager hasn't been created yet.
@@ -131,9 +138,9 @@ class ExtruderManager(QObject):
 
                 # Make sure the next stack is a stack that contains only the machine definition
                 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
         if changed:
             self.extrudersChanged.emit(machine_id)
@@ -235,6 +242,9 @@ class ExtruderManager(QObject):
 
         container_stack.addContainer(quality)
 
+        empty_quality_changes = container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
+        container_stack.addContainer(empty_quality_changes)
+
         user_profile = container_registry.findInstanceContainers(type = "user", extruder = extruder_stack_id)
         if user_profile: # There was already a user profile, loaded from settings.
             user_profile = user_profile[0]
@@ -248,9 +258,9 @@ class ExtruderManager(QObject):
 
         # Make sure the next stack is a stack that contains only the machine definition
         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)
 
@@ -274,6 +284,20 @@ class ExtruderManager(QObject):
         for name in self._extruder_trains[machine_id]:
             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):
         self._addCurrentMachineExtruders()
         self.activeExtruderChanged.emit()
@@ -313,6 +337,17 @@ class ExtruderManager(QObject):
 
         return result
 
+    ##  Get all extruder values for a certain setting.
+    #
+    #   This is exposed to qml for display purposes
+    #
+    #   \param key The key of the setting to retieve values for.
+    #
+    #   \return String representing the extruder values
+    @pyqtSlot(str, result="QList<int>")
+    def getInstanceExtruderValues(self, key):
+        return ExtruderManager.getExtruderValues(key)
+
     ##  Get the value for a setting from a specific extruder.
     #
     #   This is exposed to SettingFunction to use in value functions.

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