Browse Source

Merge branch 'master' of https://github.com/Ultimaker/Cura into layerview_dev

Johan K 8 years ago
parent
commit
bbd49cee85

+ 12 - 0
cura/BuildVolume.py

@@ -1,6 +1,7 @@
 # Copyright (c) 2015 Ultimaker B.V.
 # Copyright (c) 2015 Ultimaker B.V.
 # Cura is released under the terms of the AGPLv3 or higher.
 # Cura is released under the terms of the AGPLv3 or higher.
 
 
+from UM.i18n import i18nCatalog
 from UM.Scene.SceneNode import SceneNode
 from UM.Scene.SceneNode import SceneNode
 from UM.Application import Application
 from UM.Application import Application
 from UM.Resources import Resources
 from UM.Resources import Resources
@@ -9,9 +10,11 @@ from UM.Math.Vector import Vector
 from UM.Math.Color import Color
 from UM.Math.Color import Color
 from UM.Math.AxisAlignedBox import AxisAlignedBox
 from UM.Math.AxisAlignedBox import AxisAlignedBox
 from UM.Math.Polygon import Polygon
 from UM.Math.Polygon import Polygon
+from UM.Message import Message
 
 
 from UM.View.RenderBatch import RenderBatch
 from UM.View.RenderBatch import RenderBatch
 from UM.View.GL.OpenGL import OpenGL
 from UM.View.GL.OpenGL import OpenGL
+catalog = i18nCatalog("cura")
 
 
 import numpy
 import numpy
 
 
@@ -162,6 +165,13 @@ class BuildVolume(SceneNode):
     def getBoundingBox(self):
     def getBoundingBox(self):
         return self._volume_aabb
         return self._volume_aabb
 
 
+    def _buildVolumeMessage(self):
+        Message(catalog.i18nc(
+            "@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()
+
     def _onGlobalContainerStackChanged(self):
     def _onGlobalContainerStackChanged(self):
         if self._active_container_stack:
         if self._active_container_stack:
             self._active_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
             self._active_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
@@ -174,6 +184,7 @@ class BuildVolume(SceneNode):
             self._width = self._active_container_stack.getProperty("machine_width", "value")
             self._width = self._active_container_stack.getProperty("machine_width", "value")
             if self._active_container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
             if self._active_container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
                 self._height = self._active_container_stack.getProperty("gantry_height", "value")
                 self._height = self._active_container_stack.getProperty("gantry_height", "value")
+                self._buildVolumeMessage()
             else:
             else:
                 self._height = self._active_container_stack.getProperty("machine_height", "value")
                 self._height = self._active_container_stack.getProperty("machine_height", "value")
             self._depth = self._active_container_stack.getProperty("machine_depth", "value")
             self._depth = self._active_container_stack.getProperty("machine_depth", "value")
@@ -189,6 +200,7 @@ class BuildVolume(SceneNode):
         if setting_key == "print_sequence":
         if setting_key == "print_sequence":
             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._height = self._active_container_stack.getProperty("gantry_height", "value")
+                self._buildVolumeMessage()
             else:
             else:
                 self._height = self._active_container_stack.getProperty("machine_height", "value")
                 self._height = self._active_container_stack.getProperty("machine_height", "value")
             self.rebuild()
             self.rebuild()

+ 73 - 34
cura/CuraApplication.py

@@ -19,7 +19,7 @@ from UM.JobQueue import JobQueue
 from UM.SaveFile import SaveFile
 from UM.SaveFile import SaveFile
 from UM.Scene.Selection import Selection
 from UM.Scene.Selection import Selection
 from UM.Scene.GroupDecorator import GroupDecorator
 from UM.Scene.GroupDecorator import GroupDecorator
-import UM.Settings.Validator
+from UM.Settings.Validator import Validator
 
 
 from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
 from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
 from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
 from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
@@ -32,8 +32,6 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
 
 
 from UM.i18n import i18nCatalog
 from UM.i18n import i18nCatalog
 
 
-from . import ExtruderManager
-from . import ExtrudersModel
 from . import PlatformPhysics
 from . import PlatformPhysics
 from . import BuildVolume
 from . import BuildVolume
 from . import CameraAnimation
 from . import CameraAnimation
@@ -42,13 +40,14 @@ from . import CuraActions
 from . import MultiMaterialDecorator
 from . import MultiMaterialDecorator
 from . import ZOffsetDecorator
 from . import ZOffsetDecorator
 from . import CuraSplashScreen
 from . import CuraSplashScreen
-from . import MachineManagerModel
-from . import ContainerSettingsModel
 from . import CameraImageProvider
 from . import CameraImageProvider
 from . import MachineActionManager
 from . import MachineActionManager
 
 
+import cura.Settings
+
 from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
 from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
 from PyQt5.QtGui import QColor, QIcon
 from PyQt5.QtGui import QColor, QIcon
+from PyQt5.QtWidgets import QMessageBox
 from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
 from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
 
 
 import platform
 import platform
@@ -99,7 +98,32 @@ class CuraApplication(QtApplication):
         SettingDefinition.addSupportedProperty("settable_per_extruder", 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_per_meshgroup", DefinitionPropertyType.Any, default = True)
         SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True)
         SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True)
-        SettingDefinition.addSettingType("extruder", int, str, UM.Settings.Validator)
+        SettingDefinition.addSettingType("extruder", int, str, Validator)
+
+        ## Add the 4 types of profiles to storage.
+        Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
+        Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
+        Resources.addStorageType(self.ResourceTypes.MaterialInstanceContainer, "materials")
+        Resources.addStorageType(self.ResourceTypes.UserInstanceContainer, "user")
+        Resources.addStorageType(self.ResourceTypes.ExtruderStack, "extruders")
+        Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
+
+        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer)
+        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.VariantInstanceContainer)
+        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MaterialInstanceContainer)
+        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.UserInstanceContainer)
+        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.ExtruderStack)
+        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MachineStack)
+
+        ##  Initialise the version upgrade manager with Cura's storage paths.
+        import UM.VersionUpgradeManager #Needs to be here to prevent circular dependencies.
+        self._version_upgrade_manager = UM.VersionUpgradeManager.VersionUpgradeManager(
+            {
+                ("quality", UM.Settings.InstanceContainer.Version):    (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
+                ("machine_stack", UM.Settings.ContainerStack.Version): (self.ResourceTypes.MachineStack, "application/x-uranium-containerstack"),
+                ("preferences", UM.Preferences.Version):               (Resources.Preferences, "application/x-uranium-preferences")
+            }
+        )
 
 
         self._machine_action_manager = MachineActionManager.MachineActionManager()
         self._machine_action_manager = MachineActionManager.MachineActionManager()
 
 
@@ -132,6 +156,9 @@ class CuraApplication(QtApplication):
         self._cura_actions = None
         self._cura_actions = None
         self._started = False
         self._started = False
 
 
+        self._message_box_callback = None
+        self._message_box_callback_arguments = []
+
         self._i18n_catalog = i18nCatalog("cura")
         self._i18n_catalog = i18nCatalog("cura")
 
 
         self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
         self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
@@ -142,21 +169,6 @@ class CuraApplication(QtApplication):
 
 
         self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading machines..."))
         self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading machines..."))
 
 
-        ## Add the 4 types of profiles to storage.
-        Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
-        Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
-        Resources.addStorageType(self.ResourceTypes.MaterialInstanceContainer, "materials")
-        Resources.addStorageType(self.ResourceTypes.UserInstanceContainer, "user")
-        Resources.addStorageType(self.ResourceTypes.ExtruderStack, "extruders")
-        Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
-
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer)
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.VariantInstanceContainer)
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MaterialInstanceContainer)
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.UserInstanceContainer)
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.ExtruderStack)
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MachineStack)
-
         # Add empty variant, material and quality containers.
         # Add empty variant, material and quality containers.
         # Since they are empty, they should never be serialized and instead just programmatically created.
         # Since they are empty, they should never be serialized and instead just programmatically created.
         # We need them to simplify the switching between materials.
         # We need them to simplify the switching between materials.
@@ -223,6 +235,7 @@ class CuraApplication(QtApplication):
             meshfix
             meshfix
             blackmagic
             blackmagic
                 print_sequence
                 print_sequence
+                infill_mesh
                 dual
                 dual
             experimental
             experimental
         """.replace("\n", ";").replace(" ", ""))
         """.replace("\n", ";").replace(" ", ""))
@@ -242,6 +255,23 @@ class CuraApplication(QtApplication):
     def _onEngineCreated(self):
     def _onEngineCreated(self):
         self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
         self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
 
 
+    ## A reusable dialogbox
+    #
+    showMessageBox = pyqtSignal(str, str, str, str, int, int, arguments = ["title", "text", "informativeText", "detailedText", "buttons", "icon"])
+    def messageBox(self, title, text, informativeText = "", detailedText = "", buttons = QMessageBox.Ok, icon = QMessageBox.NoIcon, callback = None, callback_arguments = []):
+        self._message_box_callback = callback
+        self._message_box_callback_arguments = callback_arguments
+        self.showMessageBox.emit(title, text, informativeText, detailedText, buttons, icon)
+
+    @pyqtSlot(int)
+    def messageBoxClosed(self, button):
+        if self._message_box_callback:
+            self._message_box_callback(button, *self._message_box_callback_arguments)
+            self._message_box_callback = None
+            self._message_box_callback_arguments = []
+
+    showPrintMonitor = pyqtSignal(bool, arguments = ["show"])
+
     ##  Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
     ##  Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
     #
     #
     #   Note that the AutoSave plugin also calls this method.
     #   Note that the AutoSave plugin also calls this method.
@@ -261,7 +291,8 @@ class CuraApplication(QtApplication):
                 Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
                 Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
                 continue
                 continue
 
 
-            file_name = urllib.parse.quote_plus(instance.getId()) + ".inst.cfg"
+            mime_type = ContainerRegistry.getMimeTypeForContainer(type(instance))
+            file_name = urllib.parse.quote_plus(instance.getId()) + "." + mime_type.preferredSuffix
             instance_type = instance.getMetaDataEntry("type")
             instance_type = instance.getMetaDataEntry("type")
             path = None
             path = None
             if instance_type == "material":
             if instance_type == "material":
@@ -289,7 +320,8 @@ class CuraApplication(QtApplication):
                 Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
                 Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
                 continue
                 continue
 
 
-            file_name = urllib.parse.quote_plus(stack.getId()) + ".stack.cfg"
+            mime_type = ContainerRegistry.getMimeTypeForContainer(type(stack))
+            file_name = urllib.parse.quote_plus(stack.getId()) + "." + mime_type.preferredSuffix
             stack_type = stack.getMetaDataEntry("type", None)
             stack_type = stack.getMetaDataEntry("type", None)
             path = None
             path = None
             if not stack_type or stack_type == "machine":
             if not stack_type or stack_type == "machine":
@@ -366,9 +398,9 @@ class CuraApplication(QtApplication):
         self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface..."))
         self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface..."))
 
 
         # Initialise extruder so as to listen to global container stack changes before the first global container stack is set.
         # Initialise extruder so as to listen to global container stack changes before the first global container stack is set.
-        ExtruderManager.ExtruderManager.getInstance()
-        qmlRegisterSingletonType(MachineManagerModel.MachineManagerModel, "Cura", 1, 0, "MachineManager",
-                                 MachineManagerModel.createMachineManagerModel)
+        cura.Settings.ExtruderManager.getInstance()
+        qmlRegisterSingletonType(cura.Settings.MachineManager, "Cura", 1, 0, "MachineManager",
+                                 cura.Settings.MachineManager.createMachineManager)
 
 
         qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
         qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
         self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
         self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
@@ -412,6 +444,7 @@ class CuraApplication(QtApplication):
     #   \param engine The QML engine.
     #   \param engine The QML engine.
     def registerObjects(self, engine):
     def registerObjects(self, engine):
         engine.rootContext().setContextProperty("Printer", self)
         engine.rootContext().setContextProperty("Printer", self)
+        engine.rootContext().setContextProperty("CuraApplication", self)
         self._print_information = PrintInformation.PrintInformation()
         self._print_information = PrintInformation.PrintInformation()
         engine.rootContext().setContextProperty("PrintInformation", self._print_information)
         engine.rootContext().setContextProperty("PrintInformation", self._print_information)
         self._cura_actions = CuraActions.CuraActions(self)
         self._cura_actions = CuraActions.CuraActions(self)
@@ -419,13 +452,16 @@ class CuraApplication(QtApplication):
 
 
         qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
         qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
 
 
-        qmlRegisterType(ExtrudersModel.ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
+        qmlRegisterType(cura.Settings.ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
+
+        qmlRegisterType(cura.Settings.ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
+        qmlRegisterType(cura.Settings.MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
 
 
-        qmlRegisterType(ContainerSettingsModel.ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
+        qmlRegisterSingletonType(cura.Settings.ContainerManager, "Cura", 1, 0, "ContainerManager", cura.Settings.ContainerManager.createContainerManager)
 
 
         qmlRegisterSingletonType(QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")), "Cura", 1, 0, "Actions")
         qmlRegisterSingletonType(QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")), "Cura", 1, 0, "Actions")
 
 
-        engine.rootContext().setContextProperty("ExtruderManager", ExtruderManager.ExtruderManager.getInstance())
+        engine.rootContext().setContextProperty("ExtruderManager", cura.Settings.ExtruderManager.getInstance())
 
 
         for path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.QmlFiles):
         for path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.QmlFiles):
             type_name = os.path.splitext(os.path.basename(path))[0]
             type_name = os.path.splitext(os.path.basename(path))[0]
@@ -546,12 +582,12 @@ class CuraApplication(QtApplication):
             for _ in range(count):
             for _ in range(count):
                 if node.getParent() and node.getParent().callDecoration("isGroup"):
                 if node.getParent() and node.getParent().callDecoration("isGroup"):
                     new_node = copy.deepcopy(node.getParent()) #Copy the group node.
                     new_node = copy.deepcopy(node.getParent()) #Copy the group node.
-                    new_node.callDecoration("setConvexHull",None)
+                    new_node.callDecoration("recomputeConvexHull")
 
 
                     op.addOperation(AddSceneNodeOperation(new_node,node.getParent().getParent()))
                     op.addOperation(AddSceneNodeOperation(new_node,node.getParent().getParent()))
                 else:
                 else:
                     new_node = copy.deepcopy(node)
                     new_node = copy.deepcopy(node)
-                    new_node.callDecoration("setConvexHull", None)
+                    new_node.callDecoration("recomputeConvexHull")
                     op.addOperation(AddSceneNodeOperation(new_node, node.getParent()))
                     op.addOperation(AddSceneNodeOperation(new_node, node.getParent()))
 
 
             op.push()
             op.push()
@@ -576,6 +612,7 @@ class CuraApplication(QtApplication):
     ##  Delete all nodes containing mesh data in the scene.
     ##  Delete all nodes containing mesh data in the scene.
     @pyqtSlot()
     @pyqtSlot()
     def deleteAll(self):
     def deleteAll(self):
+        Logger.log("i", "Clearing scene")
         if not self.getController().getToolsEnabled():
         if not self.getController().getToolsEnabled():
             return
             return
 
 
@@ -599,6 +636,7 @@ class CuraApplication(QtApplication):
     ## Reset all translation on nodes with mesh data. 
     ## Reset all translation on nodes with mesh data. 
     @pyqtSlot()
     @pyqtSlot()
     def resetAllTranslation(self):
     def resetAllTranslation(self):
+        Logger.log("i", "Resetting all scene translations")
         nodes = []
         nodes = []
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
             if type(node) is not SceneNode:
             if type(node) is not SceneNode:
@@ -621,6 +659,7 @@ class CuraApplication(QtApplication):
     ## Reset all transformations on nodes with mesh data. 
     ## Reset all transformations on nodes with mesh data. 
     @pyqtSlot()
     @pyqtSlot()
     def resetAll(self):
     def resetAll(self):
+        Logger.log("i", "Resetting all scene transformations")
         nodes = []
         nodes = []
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
             if type(node) is not SceneNode:
             if type(node) is not SceneNode:
@@ -644,6 +683,7 @@ class CuraApplication(QtApplication):
     ##  Reload all mesh data on the screen from file.
     ##  Reload all mesh data on the screen from file.
     @pyqtSlot()
     @pyqtSlot()
     def reloadAll(self):
     def reloadAll(self):
+        Logger.log("i", "Reloading all loaded mesh data.")
         nodes = []
         nodes = []
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
             if type(node) is not SceneNode or not node.getMeshData():
             if type(node) is not SceneNode or not node.getMeshData():
@@ -655,15 +695,14 @@ class CuraApplication(QtApplication):
             return
             return
 
 
         for node in nodes:
         for node in nodes:
-            if not node.getMeshData():
-                continue
-
             file_name = node.getMeshData().getFileName()
             file_name = node.getMeshData().getFileName()
             if file_name:
             if file_name:
                 job = ReadMeshJob(file_name)
                 job = ReadMeshJob(file_name)
                 job._node = node
                 job._node = node
                 job.finished.connect(self._reloadMeshFinished)
                 job.finished.connect(self._reloadMeshFinished)
                 job.start()
                 job.start()
+            else:
+                Logger.log("w", "Unable to reload data because we don't have a filename.")
     
     
     ##  Get logging data of the backend engine
     ##  Get logging data of the backend engine
     #   \returns \type{string} Logging data
     #   \returns \type{string} Logging data

+ 5 - 2
cura/CuraSplashScreen.py

@@ -25,10 +25,13 @@ class CuraSplashScreen(QSplashScreen):
         if buildtype:
         if buildtype:
             version[0] += " (%s)" %(buildtype)
             version[0] += " (%s)" %(buildtype)
 
 
-        painter.setFont(QFont("Proxima Nova Rg", 20 ))
+        font = QFont() # Using system-default font here
+        font.setPointSize(20)
+        painter.setFont(font)
         painter.drawText(0, 0, 330 * self._scale, 230 * self._scale, Qt.AlignHCenter | Qt.AlignBottom, version[0])
         painter.drawText(0, 0, 330 * self._scale, 230 * self._scale, Qt.AlignHCenter | Qt.AlignBottom, version[0])
         if len(version) > 1:
         if len(version) > 1:
-            painter.setFont(QFont("Proxima Nova Rg", 12 ))
+            font.setPointSize(12)
+            painter.setFont(font)
             painter.drawText(0, 0, 330 * self._scale, 255 * self._scale, Qt.AlignHCenter | Qt.AlignBottom, version[1])
             painter.drawText(0, 0, 330 * self._scale, 255 * self._scale, Qt.AlignHCenter | Qt.AlignBottom, version[1])
 
 
         painter.restore()
         painter.restore()

+ 4 - 1
cura/MultiMaterialDecorator.py

@@ -5,4 +5,7 @@ class MultiMaterialDecorator(SceneNodeDecorator):
         super().__init__()
         super().__init__()
         
         
     def isMultiMaterial(self):
     def isMultiMaterial(self):
-        return True
+        return True
+
+    def __deepcopy__(self, memo):
+        return MultiMaterialDecorator()

+ 17 - 25
cura/PlatformPhysics.py

@@ -7,7 +7,6 @@ from UM.Scene.SceneNode import SceneNode
 from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
 from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
 from UM.Math.Vector import Vector
 from UM.Math.Vector import Vector
 from UM.Math.AxisAlignedBox import AxisAlignedBox
 from UM.Math.AxisAlignedBox import AxisAlignedBox
-from UM.Application import Application
 from UM.Scene.Selection import Selection
 from UM.Scene.Selection import Selection
 from UM.Preferences import Preferences
 from UM.Preferences import Preferences
 
 
@@ -16,8 +15,6 @@ from cura.ConvexHullDecorator import ConvexHullDecorator
 from . import PlatformPhysicsOperation
 from . import PlatformPhysicsOperation
 from . import ZOffsetDecorator
 from . import ZOffsetDecorator
 
 
-import copy
-
 class PlatformPhysics:
 class PlatformPhysics:
     def __init__(self, controller, volume):
     def __init__(self, controller, volume):
         super().__init__()
         super().__init__()
@@ -66,9 +63,6 @@ class PlatformPhysics:
                 elif bbox.bottom < z_offset:
                 elif bbox.bottom < z_offset:
                     move_vector = move_vector.set(y=(-bbox.bottom) - z_offset)
                     move_vector = move_vector.set(y=(-bbox.bottom) - z_offset)
 
 
-            #if not Float.fuzzyCompare(bbox.bottom, 0.0):
-            #   pass#move_vector.setY(-bbox.bottom)
-
             # If there is no convex hull for the node, start calculating it and continue.
             # If there is no convex hull for the node, start calculating it and continue.
             if not node.getDecorator(ConvexHullDecorator):
             if not node.getDecorator(ConvexHullDecorator):
                 node.addDecorator(ConvexHullDecorator())
                 node.addDecorator(ConvexHullDecorator())
@@ -81,37 +75,35 @@ class PlatformPhysics:
                     if other_node is root or type(other_node) is not SceneNode or other_node is node:
                     if other_node is root or type(other_node) is not SceneNode or other_node is node:
                         continue
                         continue
                     
                     
-                    # Ignore colissions of a group with it's own children
+                    # Ignore collisions of a group with it's own children
                     if other_node in node.getAllChildren() or node in other_node.getAllChildren():
                     if other_node in node.getAllChildren() or node in other_node.getAllChildren():
                         continue
                         continue
                     
                     
-                    # Ignore colissions within a group
+                    # Ignore collisions within a group
                     if other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None:
                     if other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None:
                         continue
                         continue
-                        #if node.getParent().callDecoration("isGroup") is other_node.getParent().callDecoration("isGroup"):
-                        #    continue
                     
                     
                     # Ignore nodes that do not have the right properties set.
                     # Ignore nodes that do not have the right properties set.
                     if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
                     if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
                         continue
                         continue
 
 
-                    # Check to see if the bounding boxes intersect. If not, we can ignore the node as there is no way the hull intersects.
-                    #if node.getBoundingBox().intersectsBox(other_node.getBoundingBox()) == AxisAlignedBox.IntersectionResult.NoIntersection:
-                    #    continue
-
                     # 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.
-                    try:
-                        head_hull = node.callDecoration("getConvexHullHead")
-                        if head_hull:
-                            overlap = head_hull.intersectsPolygon(other_node.callDecoration("getConvexHull"))
-                            if not overlap:
-                                other_head_hull = other_node.callDecoration("getConvexHullHead")
-                                if other_head_hull:
-                                    overlap = node.callDecoration("getConvexHull").intersectsPolygon(other_head_hull)
+                    head_hull = node.callDecoration("getConvexHullHead")
+                    if head_hull:
+                        overlap = head_hull.intersectsPolygon(other_node.callDecoration("getConvexHull"))
+                        if not overlap:
+                            other_head_hull = other_node.callDecoration("getConvexHullHead")
+                            if other_head_hull:
+                                overlap = node.callDecoration("getConvexHull").intersectsPolygon(other_head_hull)
+                    else:
+                        own_convex_hull = node.callDecoration("getConvexHull")
+                        other_convex_hull = other_node.callDecoration("getConvexHull")
+                        if own_convex_hull and other_convex_hull:
+                            overlap = own_convex_hull.intersectsPolygon(other_convex_hull)
                         else:
                         else:
-                            overlap = node.callDecoration("getConvexHull").intersectsPolygon(other_node.callDecoration("getConvexHull"))
-                    except:
-                        overlap = None #It can sometimes occur that the calculated convex hull has no size, in which case there is no overlap.
+                            # This can happen in some cases if the object is not yet done with being loaded.
+                            #  Simply waiting for the next tick seems to resolve this correctly.
+                            overlap = None
 
 
                     if overlap is None:
                     if overlap is None:
                         continue
                         continue

+ 11 - 10
cura/PrintInformation.py

@@ -44,7 +44,7 @@ class PrintInformation(QObject):
 
 
         self._current_print_time = Duration(None, self)
         self._current_print_time = Duration(None, self)
 
 
-        self._material_amount = -1
+        self._material_amounts = []
 
 
         self._backend = Application.getInstance().getBackend()
         self._backend = Application.getInstance().getBackend()
         if self._backend:
         if self._backend:
@@ -62,21 +62,22 @@ class PrintInformation(QObject):
     def currentPrintTime(self):
     def currentPrintTime(self):
         return self._current_print_time
         return self._current_print_time
 
 
-    materialAmountChanged = pyqtSignal()
+    materialAmountsChanged = pyqtSignal()
 
 
-    @pyqtProperty(float, notify = materialAmountChanged)
-    def materialAmount(self):
-        return self._material_amount
+    @pyqtProperty("QVariantList", notify = materialAmountsChanged)
+    def materialAmounts(self):
+        return self._material_amounts
 
 
-    def _onPrintDurationMessage(self, time, amount):
-        #if self._slice_pass == self.SlicePass.CurrentSettings:
-        self._current_print_time.setDuration(time)
+    def _onPrintDurationMessage(self, total_time, material_amounts):
+        self._current_print_time.setDuration(total_time)
         self.currentPrintTimeChanged.emit()
         self.currentPrintTimeChanged.emit()
 
 
         # Material amount is sent as an amount of mm^3, so calculate length from that
         # Material amount is sent as an amount of mm^3, so calculate length from that
         r = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2
         r = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2
-        self._material_amount = round((amount / (math.pi * r ** 2)) / 1000, 2)
-        self.materialAmountChanged.emit()
+        self._material_amounts = []
+        for amount in material_amounts:
+            self._material_amounts.append(round((amount / (math.pi * r ** 2)) / 1000, 2))
+        self.materialAmountsChanged.emit()
 
 
     @pyqtSlot(str)
     @pyqtSlot(str)
     def setJobName(self, name):
     def setJobName(self, name):

+ 36 - 0
cura/PrinterOutputDevice.py

@@ -24,6 +24,8 @@ class PrinterOutputDevice(QObject, OutputDevice):
         self._num_extruders = 1
         self._num_extruders = 1
         self._hotend_temperatures = [0] * self._num_extruders
         self._hotend_temperatures = [0] * self._num_extruders
         self._target_hotend_temperatures = [0] * self._num_extruders
         self._target_hotend_temperatures = [0] * self._num_extruders
+        self._material_ids = [""] * self._num_extruders
+        self._hotend_ids = [""] * self._num_extruders
         self._progress = 0
         self._progress = 0
         self._head_x = 0
         self._head_x = 0
         self._head_y = 0
         self._head_y = 0
@@ -57,6 +59,12 @@ class PrinterOutputDevice(QObject, OutputDevice):
     # Signal to be emitted when head position is changed (x,y,z)
     # Signal to be emitted when head position is changed (x,y,z)
     headPositionChanged = pyqtSignal()
     headPositionChanged = pyqtSignal()
 
 
+    # Signal to be emitted when either of the material ids is changed
+    materialIdChanged = pyqtSignal(int, str, arguments = ["index", "id"])
+
+    # Signal to be emitted when either of the hotend ids is changed
+    hotendIdChanged = pyqtSignal(int, str, arguments = ["index", "id"])
+
     # Signal that is emitted every time connection state is changed.
     # Signal that is emitted every time connection state is changed.
     # it also sends it's own device_id (for convenience sake)
     # it also sends it's own device_id (for convenience sake)
     connectionStateChanged = pyqtSignal(str)
     connectionStateChanged = pyqtSignal(str)
@@ -212,6 +220,34 @@ class PrinterOutputDevice(QObject, OutputDevice):
         self._hotend_temperatures[index] = temperature
         self._hotend_temperatures[index] = temperature
         self.hotendTemperaturesChanged.emit()
         self.hotendTemperaturesChanged.emit()
 
 
+    @pyqtProperty("QVariantList", notify = materialIdChanged)
+    def materialIds(self):
+        return self._material_ids
+
+    ##  Protected setter for the current material id.
+    #   /param index Index of the extruder
+    #   /param material_id id of the material
+    def _setMaterialId(self, index, material_id):
+        if material_id and material_id != "" and material_id != self._material_ids[index]:
+            Logger.log("d", "Setting material id of hotend %d to %s" % (index, material_id))
+            self._material_ids[index] = material_id
+            self.materialIdChanged.emit(index, material_id)
+
+
+    @pyqtProperty("QVariantList", notify = hotendIdChanged)
+    def hotendIds(self):
+        return self._hotend_ids
+
+    ##  Protected setter for the current hotend id.
+    #   /param index Index of the extruder
+    #   /param hotend_id id of the hotend
+    def _setHotendId(self, index, hotend_id):
+        if hotend_id and hotend_id != "" and hotend_id != self._hotend_ids[index]:
+            Logger.log("d", "Setting hotend id of hotend %d to %s" % (index, hotend_id))
+            self._hotend_ids[index] = hotend_id
+            self.hotendIdChanged.emit(index, hotend_id)
+
+
     ##  Attempt to establish connection
     ##  Attempt to establish connection
     def connect(self):
     def connect(self):
         raise NotImplementedError("connect needs to be implemented")
         raise NotImplementedError("connect needs to be implemented")

+ 387 - 0
cura/Settings/ContainerManager.py

@@ -0,0 +1,387 @@
+# Copyright (c) 2016 Ultimaker B.V.
+# Cura is released under the terms of the AGPLv3 or higher.
+
+import os.path
+import urllib
+
+from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal, QUrl
+from PyQt5.QtWidgets import QMessageBox
+
+import UM.PluginRegistry
+import UM.Settings
+import UM.SaveFile
+import UM.Platform
+import UM.MimeTypeDatabase
+import UM.Logger
+
+from UM.MimeTypeDatabase import MimeTypeNotFoundError
+
+from UM.i18n import i18nCatalog
+catalog = i18nCatalog("cura")
+
+##  Manager class that contains common actions to deal with containers in Cura.
+#
+#   This is primarily intended as a class to be able to perform certain actions
+#   from within QML. We want to be able to trigger things like removing a container
+#   when a certain action happens. This can be done through this class.
+class ContainerManager(QObject):
+    def __init__(self, parent = None):
+        super().__init__(parent)
+
+        self._registry = UM.Settings.ContainerRegistry.getInstance()
+        self._container_name_filters = {}
+
+    ##  Create a duplicate of the specified container
+    #
+    #   This will create and add a duplicate of the container corresponding
+    #   to the container ID.
+    #
+    #   \param container_id \type{str} The ID of the container to duplicate.
+    #
+    #   \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)
+        if not containers:
+            UM.Logger.log("w", "Could duplicate container %s because it was not found.", container_id)
+            return ""
+
+        container = containers[0]
+
+        new_container = None
+        new_name = self._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"):
+            new_container = container.duplicate(new_name)
+        else:
+            new_container = container.__class__(new_name)
+            new_container.deserialize(container.serialize())
+            new_container.setName(new_name)
+
+        if new_container:
+            self._registry.addContainer(new_container)
+
+        return new_container.getId()
+
+    ##  Change the name of a specified container to a new name.
+    #
+    #   \param container_id \type{str} The ID of the container to change the name of.
+    #   \param new_id \type{str} The new ID of the container.
+    #   \param new_name \type{str} The new name of the specified container.
+    #
+    #   \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)
+        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)
+
+        # Ensure we have a unique name for the container
+        new_name = self._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)
+
+        return True
+
+    ##  Remove the specified container.
+    #
+    #   \param container_id \type{str} The ID of the container to remove.
+    #
+    #   \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)
+        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())
+
+        return True
+
+    ##  Merge a container with another.
+    #
+    #   This will try to merge one container into the other, by going through the container
+    #   and setting the right properties on the other container.
+    #
+    #   \param merge_into_id \type{str} The ID of the container to merge into.
+    #   \param merge_id \type{str} The ID of the container to merge.
+    #
+    #   \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)
+        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)
+        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):
+            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"))
+
+        return True
+
+    ##  Clear the contents of a container.
+    #
+    #   \param container_id \type{str} The ID of the container to clear.
+    #
+    #   \return True if successful, False if not.
+    @pyqtSlot(str, result = bool)
+    def clearContainer(self, container_id):
+        containers = self._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
+
+        if containers[0].isReadOnly():
+            UM.Logger.log("w", "Cannot clear read-only container %s", container_id)
+            return False
+
+        containers[0].clear()
+
+        return True
+
+    ##  Set a metadata entry of the specified container.
+    #
+    #   This will set the specified entry of the container's metadata to the specified
+    #   value. Note that entries containing dictionaries can have their entries changed
+    #   by using "/" as a separator. For example, to change an entry "foo" in a
+    #   dictionary entry "bar", you can specify "bar/foo" as entry name.
+    #
+    #   \param container_id \type{str} The ID of the container to change.
+    #   \param entry_name \type{str} The name of the metadata entry to change.
+    #   \param entry_value The new value of the entry.
+    #
+    #   \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)
+        if not containers:
+            UM.Logger.log("w", "Could set metadata of container %s because it was not found.", container_id)
+            return False
+
+        container = containers[0]
+
+        if container.isReadOnly():
+            UM.Logger.log("w", "Cannot set metadata of read-only container %s.", container_id)
+            return False
+
+        entries = entry_name.split("/")
+        entry_name = entries.pop()
+
+        if entries:
+            root_name = entries.pop(0)
+            root = container.getMetaDataEntry(root_name)
+
+            item = root
+            for entry in entries:
+                item = item.get(entries.pop(0), { })
+
+            item[entry_name] = entry_value
+
+            entry_name = root_name
+            entry_value = root
+
+        container.setMetaDataEntry(entry_name, entry_value)
+
+        return True
+
+    ##  Find instance containers matching certain criteria.
+    #
+    #   This effectively forwards to ContainerRegistry::findInstanceContainers.
+    #
+    #   \param criteria A dict of key - value pairs to search for.
+    #
+    #   \return A list of container IDs that match the given criteria.
+    @pyqtSlot("QVariantMap", result = "QVariantList")
+    def findInstanceContainers(self, criteria):
+        result = []
+        for entry in self._registry.findInstanceContainers(**criteria):
+            result.append(entry.getId())
+
+        return result
+
+    ##  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
+    #   out of that. The strings are formatted as "description (*.extension)" and can be directly
+    #   passed to a nameFilters property of a Qt File Dialog.
+    #
+    #   \param type_name Which types of containers to list. These types correspond to the "type"
+    #                    key of the plugin metadata.
+    #
+    #   \return A string list with name filters.
+    @pyqtSlot(str, result = "QStringList")
+    def getContainerNameFilters(self, type_name):
+        if not self._container_name_filters:
+            self._updateContainerNameFilters()
+
+        filters = []
+        for filter_string, entry in self._container_name_filters.items():
+            if not type_name or entry["type"] == type_name:
+                filters.append(filter_string)
+
+        return filters
+
+    ##  Export a container to a file
+    #
+    #   \param container_id The ID of the container to export
+    #   \param file_type The type of file to save as. Should be in the form of "description (*.extension, *.ext)"
+    #   \param file_url The URL where to save the file.
+    #
+    #   \return A dictionary containing a key "status" with a status code and a key "message" with a message
+    #           explaining the status.
+    #           The status code can be one of "error", "cancelled", "success"
+    @pyqtSlot(str, str, QUrl, result = "QVariantMap")
+    def exportContainer(self, container_id, file_type, file_url):
+        if not container_id or not file_type or not file_url:
+            return { "status": "error", "message": "Invalid arguments"}
+
+        if isinstance(file_url, QUrl):
+            file_url = file_url.toLocalFile()
+
+        if not file_url:
+            return { "status": "error", "message": "Invalid path"}
+
+        mime_type = None
+        if not file_type in self._container_name_filters:
+            try:
+                mime_type = UM.MimeTypeDatabase.getMimeTypeForFile(file_url)
+            except MimeTypeNotFoundError:
+                return { "status": "error", "message": "Unknown File Type" }
+        else:
+            mime_type = self._container_name_filters[file_type]["mime"]
+
+        containers = UM.Settings.ContainerRegistry.getInstance().findContainers(None, id = container_id)
+        if not containers:
+            return { "status": "error", "message": "Container not found"}
+        container = containers[0]
+
+        for suffix in mime_type.suffixes:
+            if file_url.endswith(suffix):
+                break
+        else:
+            file_url += "." + mime_type.preferredSuffix
+
+        if not UM.Platform.isWindows():
+            if os.path.exists(file_url):
+                result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
+                                              catalog.i18nc("@label", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_url))
+                if result == QMessageBox.No:
+                    return { "status": "cancelled", "message": "User cancelled"}
+
+        try:
+            contents = container.serialize()
+        except NotImplementedError:
+            return { "status": "error", "message": "Unable to serialize container"}
+
+        with UM.SaveFile(file_url, "w") as f:
+            f.write(contents)
+
+        return { "status": "success", "message": "Succesfully exported container"}
+
+    ##  Imports a profile from a file
+    #
+    #   \param file_url A URL that points to the file to import.
+    #
+    #   \return \type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key
+    #       containing a message for the user
+    @pyqtSlot(QUrl, result = "QVariantMap")
+    def importContainer(self, file_url):
+        if not file_url:
+            return { "status": "error", "message": "Invalid path"}
+
+        if isinstance(file_url, QUrl):
+            file_url = file_url.toLocalFile()
+
+        if not file_url or not os.path.exists(file_url):
+            return { "status": "error", "message": "Invalid path" }
+
+        try:
+            mime_type = UM.MimeTypeDatabase.getMimeTypeForFile(file_url)
+        except MimeTypeNotFoundError:
+            return { "status": "error", "message": "Could not determine mime type of file" }
+
+        container_type = UM.Settings.ContainerRegistry.getContainerForMimeType(mime_type)
+        if not container_type:
+            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 = container_type(container_id)
+
+        try:
+            with open(file_url, "rt") as f:
+                container.deserialize(f.read())
+        except PermissionError:
+            return { "status": "error", "message": "Permission denied when trying to read the file"}
+
+        container.setName(container_id)
+
+        UM.Settings.ContainerRegistry.getInstance().addContainer(container)
+
+        return { "status": "success", "message": "Successfully imported container {0}".format(container.getName()) }
+
+    def _updateContainerNameFilters(self):
+        self._container_name_filters = {}
+        for plugin_id, container_type in UM.Settings.ContainerRegistry.getContainerTypes():
+            # Ignore default container types since those are not plugins
+            if container_type in (UM.Settings.InstanceContainer, UM.Settings.ContainerStack, UM.Settings.DefinitionContainer):
+                continue
+
+            serialize_type = ""
+            try:
+                plugin_metadata = UM.PluginRegistry.getInstance().getMetaData(plugin_id)
+                if plugin_metadata:
+                    serialize_type = plugin_metadata["settings_container"]["type"]
+                else:
+                    continue
+            except KeyError as e:
+                continue
+
+            mime_type = UM.Settings.ContainerRegistry.getMimeTypeForContainer(container_type)
+
+            entry = {
+                "type": serialize_type,
+                "mime": mime_type,
+                "container": container_type
+            }
+
+            suffix_list = "*." + mime_type.preferredSuffix
+            for suffix in mime_type.suffixes:
+                if suffix == mime_type.preferredSuffix:
+                    continue
+
+                suffix_list += ", *." + suffix
+
+            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()

+ 1 - 1
cura/ContainerSettingsModel.py → cura/Settings/ContainerSettingsModel.py

@@ -90,4 +90,4 @@ class ContainerSettingsModel(ListModel):
     containersChanged = pyqtSignal()
     containersChanged = pyqtSignal()
     @pyqtProperty("QVariantList", fset = setContainers, notify = containersChanged)
     @pyqtProperty("QVariantList", fset = setContainers, notify = containersChanged)
     def containers(self):
     def containers(self):
-        return self.container_ids
+        return self.container_ids

+ 1 - 1
cura/CuraContainerRegistry.py → cura/Settings/CuraContainerRegistry.py

@@ -53,7 +53,7 @@ class CuraContainerRegistry(ContainerRegistry):
     def _containerExists(self, container_type, container_name):
     def _containerExists(self, container_type, container_name):
         container_class = ContainerStack if container_type == "machine" else InstanceContainer
         container_class = ContainerStack if container_type == "machine" else InstanceContainer
 
 
-        return self.findContainers(container_class, id = container_name, type = container_type) or \
+        return self.findContainers(container_class, id = container_name, type = container_type, ignore_case = True) or \
                 self.findContainers(container_class, name = container_name, type = container_type)
                 self.findContainers(container_class, name = container_name, type = container_type)
 
 
     ##  Exports an profile to a file
     ##  Exports an profile to a file

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