Browse Source

Merge remote-tracking branch 'upstream/master' into mb-infill-line-angles

# Conflicts:
#	cura/CuraApplication.py
Mark Burton 8 years ago
parent
commit
0237a5a386

+ 2 - 4
cura/BuildVolume.py

@@ -2,6 +2,7 @@
 # Cura is released under the terms of the AGPLv3 or higher.
 
 from cura.Settings.ExtruderManager import ExtruderManager
+from UM.Settings.ContainerRegistry import ContainerRegistry
 from UM.i18n import i18nCatalog
 from UM.Scene.Platform import Platform
 from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
@@ -25,9 +26,6 @@ import numpy
 import copy
 import math
 
-import UM.Settings.ContainerRegistry
-
-
 # Setting for clearance around the prime
 PRIME_CLEARANCE = 6.5
 
@@ -796,7 +794,7 @@ class BuildVolume(SceneNode):
                 stack = self._global_container_stack
             else:
                 extruder_stack_id = ExtruderManager.getInstance().extruderIds[str(extruder_index)]
-                stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
+                stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
 
         value = stack.getProperty(setting_key, property)
         setting_type = stack.getProperty(setting_key, "type")

+ 6 - 6
cura/ConvexHullDecorator.py

@@ -1,13 +1,13 @@
 # Copyright (c) 2016 Ultimaker B.V.
 # Cura is released under the terms of the AGPLv3 or higher.
 
-from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
 from UM.Application import Application
-from cura.Settings.ExtruderManager import ExtruderManager
 from UM.Math.Polygon import Polygon
-from . import ConvexHullNode
+from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
+from UM.Settings.ContainerRegistry import ContainerRegistry
 
-import UM.Settings.ContainerRegistry
+from cura.Settings.ExtruderManager import ExtruderManager
+from . import ConvexHullNode
 
 import numpy
 
@@ -308,11 +308,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
             extruder_stack_id = self._node.callDecoration("getActiveExtruder")
             if not extruder_stack_id: #Decoration doesn't exist.
                 extruder_stack_id = ExtruderManager.getInstance().extruderIds["0"]
-            extruder_stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
+            extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
             return extruder_stack.getProperty(setting_key, property)
         else: #Limit_to_extruder is set. Use that one.
             extruder_stack_id = ExtruderManager.getInstance().extruderIds[str(extruder_index)]
-            stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
+            stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
             return stack.getProperty(setting_key, property)
 
     ## Returns true if node is a descendant or the same as the root node.

+ 8 - 4
cura/CrashHandler.py

@@ -12,10 +12,14 @@ from UM.Logger import Logger
 from UM.i18n import i18nCatalog
 catalog = i18nCatalog("cura")
 
-try:
-    from cura.CuraVersion import CuraDebugMode
-except ImportError:
-    CuraDebugMode = False  # [CodeStyle: Reflecting imported value]
+MYPY = False
+if MYPY:
+    CuraDebugMode = False
+else:
+    try:
+        from cura.CuraVersion import CuraDebugMode
+    except ImportError:
+        CuraDebugMode = False  # [CodeStyle: Reflecting imported value]
 
 # List of exceptions that should be considered "fatal" and abort the program.
 # These are primarily some exception types that we simply cannot really recover from

+ 150 - 32
cura/CuraApplication.py

@@ -1,5 +1,7 @@
 # Copyright (c) 2015 Ultimaker B.V.
 # Cura is released under the terms of the AGPLv3 or higher.
+from PyQt5.QtNetwork import QLocalServer
+from PyQt5.QtNetwork import QLocalSocket
 
 from UM.Qt.QtApplication import QtApplication
 from UM.Scene.SceneNode import SceneNode
@@ -26,7 +28,6 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
 from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
 from UM.Operations.GroupedOperation import GroupedOperation
 from UM.Operations.SetTransformOperation import SetTransformOperation
-from UM.Operations.TranslateOperation import TranslateOperation
 from cura.SetParentOperation import SetParentOperation
 from cura.SliceableObjectDecorator import SliceableObjectDecorator
 from cura.BlockSlicingDecorator import BlockSlicingDecorator
@@ -34,6 +35,11 @@ from cura.BlockSlicingDecorator import BlockSlicingDecorator
 from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
 from UM.Settings.ContainerRegistry import ContainerRegistry
 from UM.Settings.SettingFunction import SettingFunction
+from cura.Settings.MachineNameValidator import MachineNameValidator
+from cura.Settings.ProfilesModel import ProfilesModel
+from cura.Settings.QualityAndUserProfilesModel import QualityAndUserProfilesModel
+from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
+from cura.Settings.UserProfilesModel import UserProfilesModel
 
 from . import PlatformPhysics
 from . import BuildVolume
@@ -45,7 +51,14 @@ from . import CuraSplashScreen
 from . import CameraImageProvider
 from . import MachineActionManager
 
-import cura.Settings
+from cura.Settings.MachineManager import MachineManager
+from cura.Settings.ExtruderManager import ExtruderManager
+from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
+from cura.Settings.ExtrudersModel import ExtrudersModel
+from cura.Settings.ContainerSettingsModel import ContainerSettingsModel
+from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
+from cura.Settings.QualitySettingsModel import QualitySettingsModel
+from cura.Settings.ContainerManager import ContainerManager
 
 from PyQt5.QtCore import QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
 from UM.FlameProfiler import pyqtSlot
@@ -59,15 +72,18 @@ import numpy
 import copy
 import urllib.parse
 import os
-
+import argparse
+import json
 
 numpy.seterr(all="ignore")
 
-try:
-    from cura.CuraVersion import CuraVersion, CuraBuildType
-except ImportError:
-    CuraVersion = "master"  # [CodeStyle: Reflecting imported value]
-    CuraBuildType = ""
+MYPY = False
+if not MYPY:
+    try:
+        from cura.CuraVersion import CuraVersion, CuraBuildType
+    except ImportError:
+        CuraVersion = "master"  # [CodeStyle: Reflecting imported value]
+        CuraBuildType = ""
 
 class CuraApplication(QtApplication):
     class ResourceTypes:
@@ -83,6 +99,7 @@ class CuraApplication(QtApplication):
     Q_ENUMS(ResourceTypes)
 
     def __init__(self):
+
         Resources.addSearchPath(os.path.join(QtApplication.getInstallPrefix(), "share", "cura", "resources"))
         if not hasattr(sys, "frozen"):
             Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources"))
@@ -109,9 +126,9 @@ class CuraApplication(QtApplication):
 
         SettingDefinition.addSettingType("[int]", None, str, None)
 
-        SettingFunction.registerOperator("extruderValues", cura.Settings.ExtruderManager.getExtruderValues)
-        SettingFunction.registerOperator("extruderValue", cura.Settings.ExtruderManager.getExtruderValue)
-        SettingFunction.registerOperator("resolveOrValue", cura.Settings.ExtruderManager.getResolveOrValue)
+        SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues)
+        SettingFunction.registerOperator("extruderValue", ExtruderManager.getExtruderValue)
+        SettingFunction.registerOperator("resolveOrValue", ExtruderManager.getResolveOrValue)
 
         ## Add the 4 types of profiles to storage.
         Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
@@ -130,13 +147,14 @@ class CuraApplication(QtApplication):
 
         ##  Initialise the version upgrade manager with Cura's storage paths.
         import UM.VersionUpgradeManager #Needs to be here to prevent circular dependencies.
+
         UM.VersionUpgradeManager.VersionUpgradeManager.getInstance().setCurrentVersions(
             {
-                ("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"),
-                ("extruder_train", UM.Settings.ContainerStack.Version): (self.ResourceTypes.ExtruderStack, "application/x-uranium-extruderstack"),
-                ("preferences", UM.Preferences.Version):               (Resources.Preferences, "application/x-uranium-preferences"),
-                ("user", UM.Settings.InstanceContainer.Version):       (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer")
+                ("quality", UM.Settings.InstanceContainer.InstanceContainer.Version):    (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
+                ("machine_stack", UM.Settings.ContainerStack.ContainerStack.Version): (self.ResourceTypes.MachineStack, "application/x-uranium-containerstack"),
+                ("extruder_train", UM.Settings.ContainerStack.ContainerStack.Version): (self.ResourceTypes.ExtruderStack, "application/x-uranium-extruderstack"),
+                ("preferences", Preferences.Version):               (Resources.Preferences, "application/x-uranium-preferences"),
+                ("user", UM.Settings.InstanceContainer.InstanceContainer.Version):       (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer")
             }
         )
 
@@ -402,7 +420,11 @@ class CuraApplication(QtApplication):
 
     @pyqtSlot(str, str)
     def setDefaultPath(self, key, default_path):
-        Preferences.getInstance().setValue("local_file/%s" % key, default_path)
+        Preferences.getInstance().setValue("local_file/%s" % key, QUrl(default_path).toLocalFile())
+
+    @classmethod
+    def getStaticVersion(cls):
+        return CuraVersion
 
     ##  Handle loading of all plugin types (and the backend explicitly)
     #   \sa PluginRegistery
@@ -422,13 +444,107 @@ class CuraApplication(QtApplication):
 
         self._plugins_loaded = True
 
+    @classmethod
     def addCommandLineOptions(self, parser):
         super().addCommandLineOptions(parser)
         parser.add_argument("file", nargs="*", help="Files to load after starting the application.")
+        parser.add_argument("--single-instance", action="store_true", default=False)
+
+    # Set up a local socket server which listener which coordinates single instances Curas and accepts commands.
+    def _setUpSingleInstanceServer(self):
+        if self.getCommandLineOption("single_instance", False):
+            self.__single_instance_server = QLocalServer()
+            self.__single_instance_server.newConnection.connect(self._singleInstanceServerNewConnection)
+            self.__single_instance_server.listen("ultimaker-cura")
+
+    def _singleInstanceServerNewConnection(self):
+        Logger.log("i", "New connection recevied on our single-instance server")
+        remote_cura_connection = self.__single_instance_server.nextPendingConnection()
+
+        if remote_cura_connection is not None:
+            def readCommands():
+                line = remote_cura_connection.readLine()
+                while len(line) != 0:    # There is also a .canReadLine()
+                    try:
+                        payload = json.loads(str(line, encoding="ASCII").strip())
+                        command = payload["command"]
+
+                        # Command: Remove all models from the build plate.
+                        if command == "clear-all":
+                            self.deleteAll()
+
+                        # Command: Load a model file
+                        elif command == "open":
+                            self._openFile(payload["filePath"])
+                            # WARNING ^ this method is async and we really should wait until
+                            # the file load is complete before processing more commands.
+
+                        # Command: Activate the window and bring it to the top.
+                        elif command == "focus":
+                            # Operating systems these days prevent windows from moving around by themselves.
+                            # 'alert' or flashing the icon in the taskbar is the best thing we do now.
+                            self.getMainWindow().alert(0)
+
+                        # Command: Close the socket connection. We're done.
+                        elif command == "close-connection":
+                            remote_cura_connection.close()
+
+                        else:
+                            Logger.log("w", "Received an unrecognized command " + str(command))
+                    except json.decoder.JSONDecodeError as ex:
+                        Logger.log("w", "Unable to parse JSON command in _singleInstanceServerNewConnection(): " + repr(ex))
+                    line = remote_cura_connection.readLine()
+
+            remote_cura_connection.readyRead.connect(readCommands)
+
+    ##  Perform any checks before creating the main application.
+    #
+    #   This should be called directly before creating an instance of CuraApplication.
+    #   \returns \type{bool} True if the whole Cura app should continue running.
+    @classmethod
+    def preStartUp(cls):
+        # Peek the arguments and look for the 'single-instance' flag.
+        parser = argparse.ArgumentParser(prog="cura")  # pylint: disable=bad-whitespace
+        CuraApplication.addCommandLineOptions(parser)
+        parsed_command_line = vars(parser.parse_args())
+
+        if "single_instance" in parsed_command_line and parsed_command_line["single_instance"]:
+            Logger.log("i", "Checking for the presence of an ready running Cura instance.")
+            single_instance_socket = QLocalSocket()
+            Logger.log("d", "preStartUp(): full server name: " + single_instance_socket.fullServerName())
+            single_instance_socket.connectToServer("ultimaker-cura")
+            single_instance_socket.waitForConnected()
+            if single_instance_socket.state() == QLocalSocket.ConnectedState:
+                Logger.log("i", "Connection has been made to the single-instance Cura socket.")
+
+                # Protocol is one line of JSON terminated with a carriage return.
+                # "command" field is required and holds the name of the command to execute.
+                # Other fields depend on the command.
+
+                payload = {"command": "clear-all"}
+                single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding="ASCII"))
+
+                payload = {"command": "focus"}
+                single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding="ASCII"))
+
+                if len(parsed_command_line["file"]) != 0:
+                    for filename in parsed_command_line["file"]:
+                        payload = {"command": "open", "filePath": filename}
+                        single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding="ASCII"))
+
+                payload = {"command": "close-connection"}
+                single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding="ASCII"))
+
+                single_instance_socket.flush()
+                single_instance_socket.waitForDisconnected()
+                return False
+        return True
 
     def run(self):
         self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
 
+        self._setUpSingleInstanceServer()
+
         controller = self.getController()
 
         controller.setActiveView("SolidView")
@@ -464,9 +580,11 @@ class CuraApplication(QtApplication):
         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.
-        cura.Settings.ExtruderManager.getInstance()
-        qmlRegisterSingletonType(cura.Settings.MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
-        qmlRegisterSingletonType(cura.Settings.SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager", self.getSettingInheritanceManager)
+        ExtruderManager.getInstance()
+        qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
+        qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager",
+                         self.getSettingInheritanceManager)
+
         qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
         self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
         self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
@@ -486,12 +604,12 @@ class CuraApplication(QtApplication):
 
     def getMachineManager(self, *args):
         if self._machine_manager is None:
-            self._machine_manager = cura.Settings.MachineManager.createMachineManager()
+            self._machine_manager = MachineManager.createMachineManager()
         return self._machine_manager
 
     def getSettingInheritanceManager(self, *args):
         if self._setting_inheritance_manager is None:
-            self._setting_inheritance_manager = cura.Settings.SettingInheritanceManager.createSettingInheritanceManager()
+            self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
         return self._setting_inheritance_manager
 
     ##  Get the machine action manager
@@ -527,23 +645,23 @@ class CuraApplication(QtApplication):
 
         qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
 
-        qmlRegisterType(cura.Settings.ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
+        qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
 
-        qmlRegisterType(cura.Settings.ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
-        qmlRegisterSingletonType(cura.Settings.ProfilesModel, "Cura", 1, 0, "ProfilesModel", cura.Settings.ProfilesModel.createProfilesModel)
-        qmlRegisterType(cura.Settings.QualityAndUserProfilesModel, "Cura", 1, 0, "QualityAndUserProfilesModel")
-        qmlRegisterType(cura.Settings.UserProfilesModel, "Cura", 1, 0, "UserProfilesModel")
-        qmlRegisterType(cura.Settings.MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
-        qmlRegisterType(cura.Settings.QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
-        qmlRegisterType(cura.Settings.MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
+        qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
+        qmlRegisterSingletonType(ProfilesModel, "Cura", 1, 0, "ProfilesModel", ProfilesModel.createProfilesModel)
+        qmlRegisterType(QualityAndUserProfilesModel, "Cura", 1, 0, "QualityAndUserProfilesModel")
+        qmlRegisterType(UserProfilesModel, "Cura", 1, 0, "UserProfilesModel")
+        qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
+        qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
+        qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
 
-        qmlRegisterSingletonType(cura.Settings.ContainerManager, "Cura", 1, 0, "ContainerManager", cura.Settings.ContainerManager.createContainerManager)
+        qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.createContainerManager)
 
         # As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
         actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
         qmlRegisterSingletonType(actions_url, "Cura", 1, 0, "Actions")
 
-        engine.rootContext().setContextProperty("ExtruderManager", cura.Settings.ExtruderManager.getInstance())
+        engine.rootContext().setContextProperty("ExtruderManager", ExtruderManager.getInstance())
 
         for path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.QmlFiles):
             type_name = os.path.splitext(os.path.basename(path))[0]

+ 2 - 2
cura/LayerPolygon.py

@@ -1,6 +1,6 @@
 from UM.Math.Color import Color
 from UM.Application import Application
-
+from typing import Any
 import numpy
 
 
@@ -173,7 +173,7 @@ class LayerPolygon:
 
         return normals
 
-    __color_map = None
+    __color_map = None # type: numpy.ndarray[Any]
 
     ##  Gets the instance of the VersionUpgradeManager, or creates one.
     @classmethod

+ 3 - 3
cura/PrintInformation.py

@@ -7,9 +7,9 @@ from UM.FlameProfiler import pyqtSlot
 from UM.Application import Application
 from UM.Qt.Duration import Duration
 from UM.Preferences import Preferences
-from UM.Settings import ContainerRegistry
+from UM.Settings.ContainerRegistry import ContainerRegistry
 
-import cura.Settings.ExtruderManager
+from cura.Settings.ExtruderManager import ExtruderManager
 
 import math
 import os.path
@@ -124,7 +124,7 @@ class PrintInformation(QObject):
 
         material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings"))
 
-        extruder_stacks = list(cura.Settings.ExtruderManager.getInstance().getMachineExtruders(Application.getInstance().getGlobalContainerStack().getId()))
+        extruder_stacks = list(ExtruderManager.getInstance().getMachineExtruders(Application.getInstance().getGlobalContainerStack().getId()))
         for index, amount in enumerate(self._material_amounts):
             ## Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
             #  list comprehension filtering to solve this for us.

+ 63 - 3
cura/PrinterOutputDevice.py

@@ -1,12 +1,15 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the AGPLv3 or higher.
+
 from UM.i18n import i18nCatalog
 from UM.OutputDevice.OutputDevice import OutputDevice
 from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
 from PyQt5.QtWidgets import QMessageBox
-import UM.Settings.ContainerRegistry
+
+from UM.Settings.ContainerRegistry import ContainerRegistry
 
 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")
@@ -25,7 +28,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
     def __init__(self, device_id, parent = None):
         super().__init__(device_id = device_id, parent = parent)
 
-        self._container_registry = UM.Settings.ContainerRegistry.getInstance()
+        self._container_registry = ContainerRegistry.getInstance()
         self._target_bed_temperature = 0
         self._bed_temperature = 0
         self._num_extruders = 1
@@ -45,6 +48,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
         self._job_name = ""
         self._error_text = ""
         self._accepts_commands = True
+        self._preheat_bed_timeout = 900 #Default time-out for pre-heating the bed, in seconds.
 
         self._printer_state = ""
         self._printer_type = "unknown"
@@ -161,6 +165,17 @@ class PrinterOutputDevice(QObject, OutputDevice):
             self._job_name = name
             self.jobNameChanged.emit()
 
+    ##  Gives a human-readable address where the device can be found.
+    @pyqtProperty(str, constant = True)
+    def address(self):
+        Logger.log("w", "address is not implemented by this output device.")
+
+    ##  A human-readable name for the device.
+    @pyqtProperty(str, constant = True)
+    def name(self):
+        Logger.log("w", "name is not implemented by this output device.")
+        return ""
+
     @pyqtProperty(str, notify = errorTextChanged)
     def errorText(self):
         return self._error_text
@@ -199,6 +214,13 @@ class PrinterOutputDevice(QObject, OutputDevice):
             self._target_bed_temperature = temperature
             self.targetBedTemperatureChanged.emit()
 
+    ##  The duration of the time-out to pre-heat the bed, in seconds.
+    #
+    #   \return The duration of the time-out to pre-heat the bed, in seconds.
+    @pyqtProperty(int)
+    def preheatBedTimeout(self):
+        return self._preheat_bed_timeout
+
     ## Time the print has been printing.
     #  Note that timeTotal - timeElapsed should give time remaining.
     @pyqtProperty(float, notify = timeElapsedChanged)
@@ -254,6 +276,22 @@ class PrinterOutputDevice(QObject, OutputDevice):
     def _setTargetBedTemperature(self, temperature):
         Logger.log("w", "_setTargetBedTemperature is not implemented by this output device")
 
+    ##  Pre-heats the heated bed of the printer.
+    #
+    #   \param temperature The temperature to heat the bed to, in degrees
+    #   Celsius.
+    #   \param duration How long the bed should stay warm, in seconds.
+    @pyqtSlot(float, float)
+    def preheatBed(self, temperature, duration):
+        Logger.log("w", "preheatBed is not implemented by this output device.")
+
+    ##  Cancels pre-heating the heated bed of the printer.
+    #
+    #   If the bed is not pre-heated, nothing happens.
+    @pyqtSlot()
+    def cancelPreheatBed(self):
+        Logger.log("w", "cancelPreheatBed is not implemented by this output device.")
+
     ##  Protected setter for the current bed temperature.
     #   This simply sets the bed temperature, but ensures that a signal is emitted.
     #   /param temperature temperature of the bed.
@@ -323,6 +361,28 @@ class PrinterOutputDevice(QObject, OutputDevice):
                 result.append(i18n_catalog.i18nc("@item:material", "Unknown material"))
         return result
 
+    ##  List of the colours of the currently loaded materials.
+    #
+    #   The list is in order of extruders. If there is no material in an
+    #   extruder, the colour is shown as transparent.
+    #
+    #   The colours are returned in hex-format AARRGGBB or RRGGBB
+    #   (e.g. #800000ff for transparent blue or #00ff00 for pure green).
+    @pyqtProperty("QVariantList", notify = materialIdChanged)
+    def materialColors(self):
+        result = []
+        for material_id in self._material_ids:
+            if material_id is None:
+                result.append("#00000000") #No material.
+                continue
+
+            containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_id)
+            if containers:
+                result.append(containers[0].getMetaDataEntry("color_code"))
+            else:
+                result.append("#00000000") #Unknown material.
+        return result
+
     ##  Protected setter for the current material id.
     #   /param index Index of the extruder
     #   /param material_id id of the material

+ 19 - 15
cura/QualityManager.py

@@ -1,12 +1,16 @@
 # Copyright (c) 2016 Ultimaker B.V.
 # Cura is released under the terms of the AGPLv3 or higher.
 
-import UM.Application
-import cura.Settings.ExtruderManager
-import UM.Settings.ContainerRegistry
-
 # This collects a lot of quality and quality changes related code which was split between ContainerManager
 # and the MachineManager and really needs to usable from both.
+from typing import List
+
+from UM.Application import Application
+from UM.Settings.ContainerRegistry import ContainerRegistry
+from UM.Settings.DefinitionContainer import DefinitionContainer
+from UM.Settings.InstanceContainer import InstanceContainer
+from cura.Settings.ExtruderManager import ExtruderManager
+
 
 class QualityManager:
 
@@ -18,7 +22,7 @@ class QualityManager:
             QualityManager.__instance = cls()
         return QualityManager.__instance
 
-    __instance = None
+    __instance = None   # type: "QualityManager"
 
     ##  Find a quality by name for a specific machine definition and materials.
     #
@@ -121,14 +125,14 @@ class QualityManager:
     #
     #   \param machine_definition \type{DefinitionContainer} the machine definition.
     #   \return \type{List[InstanceContainer]} the list of quality changes
-    def findAllQualityChangesForMachine(self, machine_definition):
+    def findAllQualityChangesForMachine(self, machine_definition: DefinitionContainer) -> List[InstanceContainer]:
         if machine_definition.getMetaDataEntry("has_machine_quality"):
             definition_id = machine_definition.getId()
         else:
             definition_id = "fdmprinter"
 
         filter_dict = { "type": "quality_changes", "extruder": None, "definition": definition_id }
-        quality_changes_list = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**filter_dict)
+        quality_changes_list = ContainerRegistry.getInstance().findInstanceContainers(**filter_dict)
         return quality_changes_list
 
     ##  Find all usable qualities for a machine and extruders.
@@ -177,7 +181,7 @@ class QualityManager:
         if base_material:
             # There is a basic material specified
             criteria = { "type": "material", "name": base_material, "definition": definition_id }
-            containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
+            containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
             containers = [basic_material for basic_material in containers if
                                basic_material.getMetaDataEntry("variant") == material_container.getMetaDataEntry(
                                    "variant")]
@@ -191,13 +195,13 @@ class QualityManager:
     def _getFilteredContainersForStack(self, machine_definition=None, material_containers=None, **kwargs):
         # Fill in any default values.
         if machine_definition is None:
-            machine_definition = UM.Application.getInstance().getGlobalContainerStack().getBottom()
+            machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
             quality_definition_id = machine_definition.getMetaDataEntry("quality_definition")
             if quality_definition_id is not None:
-                machine_definition = UM.Settings.ContainerRegistry.getInstance().findDefinitionContainers(id=quality_definition_id)[0]
+                machine_definition = ContainerRegistry.getInstance().findDefinitionContainers(id=quality_definition_id)[0]
 
         if material_containers is None:
-            active_stacks = cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
+            active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
             material_containers = [stack.findContainer(type="material") for stack in active_stacks]
 
         criteria = kwargs
@@ -225,7 +229,7 @@ class QualityManager:
                         material_ids.add(basic_material.getId())
                     material_ids.add(material_instance.getId())
 
-        containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
+        containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
 
         result = []
         for container in containers:
@@ -241,8 +245,8 @@ class QualityManager:
     #               an extruder definition.
     #    \return  \type{DefinitionContainer} the parent machine definition. If the given machine
     #               definition doesn't have a parent then it is simply returned.
-    def getParentMachineDefinition(self, machine_definition):
-        container_registry = UM.Settings.ContainerRegistry.getInstance()
+    def getParentMachineDefinition(self, machine_definition: DefinitionContainer) -> DefinitionContainer:
+        container_registry = ContainerRegistry.getInstance()
 
         machine_entry = machine_definition.getMetaDataEntry("machine")
         if machine_entry is None:
@@ -277,6 +281,6 @@ class QualityManager:
             # This already is a 'global' machine definition.
             return machine_definition
         else:
-            container_registry = UM.Settings.ContainerRegistry.getInstance()
+            container_registry = ContainerRegistry.getInstance()
             whole_machine = container_registry.findDefinitionContainers(id=machine_entry)[0]
             return whole_machine

+ 60 - 55
cura/Settings/ContainerManager.py

@@ -8,19 +8,25 @@ from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QUrl, QVariant
 from UM.FlameProfiler import pyqtSlot
 from PyQt5.QtWidgets import QMessageBox
 
-import UM.PluginRegistry
-import UM.Settings
+from UM.PluginRegistry import PluginRegistry
 import UM.SaveFile
 import UM.Platform
 import UM.MimeTypeDatabase
-import UM.Logger
 
-import cura.Settings
+from UM.Logger import Logger
+from UM.Application import Application
+from UM.Settings.ContainerStack import ContainerStack
+from UM.Settings.DefinitionContainer import DefinitionContainer
+from UM.Settings.InstanceContainer import InstanceContainer
 from cura.QualityManager import QualityManager
 
 from UM.MimeTypeDatabase import MimeTypeNotFoundError
+from UM.Settings.ContainerRegistry import ContainerRegistry
 
 from UM.i18n import i18nCatalog
+
+from cura.Settings.ExtruderManager import ExtruderManager
+
 catalog = i18nCatalog("cura")
 
 ##  Manager class that contains common actions to deal with containers in Cura.
@@ -32,9 +38,8 @@ class ContainerManager(QObject):
     def __init__(self, parent = None):
         super().__init__(parent)
 
-        self._container_registry = UM.Settings.ContainerRegistry.getInstance()
-        self._machine_manager = UM.Application.getInstance().getMachineManager()
-
+        self._container_registry = ContainerRegistry.getInstance()
+        self._machine_manager = Application.getInstance().getMachineManager()
         self._container_name_filters = {}
 
     ##  Create a duplicate of the specified container
@@ -49,7 +54,7 @@ class ContainerManager(QObject):
     def duplicateContainer(self, 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)
+            Logger.log("w", "Could duplicate container %s because it was not found.", container_id)
             return ""
 
         container = containers[0]
@@ -81,7 +86,7 @@ class ContainerManager(QObject):
     def renameContainer(self, container_id, new_id, new_name):
         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)
+            Logger.log("w", "Could rename container %s because it was not found.", container_id)
             return False
 
         container = containers[0]
@@ -109,7 +114,7 @@ class ContainerManager(QObject):
     def removeContainer(self, 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)
+            Logger.log("w", "Could remove container %s because it was not found.", container_id)
             return False
 
         self._container_registry.removeContainer(containers[0].getId())
@@ -129,20 +134,20 @@ class ContainerManager(QObject):
     def mergeContainers(self, merge_into_id, merge_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)
+            Logger.log("w", "Could merge into container %s because it was not found.", merge_into_id)
             return False
 
         merge_into = containers[0]
 
         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)
+            Logger.log("w", "Could not merge container %s because it was not found", merge_id)
             return False
 
         merge = containers[0]
 
         if not isinstance(merge, type(merge_into)):
-            UM.Logger.log("w", "Cannot merge two containers of different types")
+            Logger.log("w", "Cannot merge two containers of different types")
             return False
 
         self._performMerge(merge_into, merge)
@@ -158,11 +163,11 @@ class ContainerManager(QObject):
     def clearContainer(self, 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)
+            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)
+            Logger.log("w", "Cannot clear read-only container %s", container_id)
             return False
 
         containers[0].clear()
@@ -173,7 +178,7 @@ class ContainerManager(QObject):
     def getContainerMetaDataEntry(self, container_id, entry_name):
         containers = self._container_registry.findContainers(None, id=container_id)
         if not containers:
-            UM.Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
+            Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
             return ""
 
         result = containers[0].getMetaDataEntry(entry_name)
@@ -198,13 +203,13 @@ class ContainerManager(QObject):
     def setContainerMetaDataEntry(self, container_id, entry_name, entry_value):
         containers = self._container_registry.findContainers(None, id = container_id)
         if not containers:
-            UM.Logger.log("w", "Could not set metadata of container %s because it was not found.", container_id)
+            Logger.log("w", "Could not 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)
+            Logger.log("w", "Cannot set metadata of read-only container %s.", container_id)
             return False
 
         entries = entry_name.split("/")
@@ -232,13 +237,13 @@ class ContainerManager(QObject):
     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)
+            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)
+            Logger.log("w", "Cannot set name of read-only container %s.", container_id)
             return False
 
         container.setName(new_name)
@@ -262,11 +267,11 @@ class ContainerManager(QObject):
 
     @pyqtSlot(str, result = bool)
     def isContainerUsed(self, container_id):
-        UM.Logger.log("d", "Checking if container %s is currently used", container_id)
+        Logger.log("d", "Checking if container %s is currently used", container_id)
         containers = self._container_registry.findContainerStacks()
         for stack in containers:
             if container_id in [child.getId() for child in stack.getContainers()]:
-                UM.Logger.log("d", "The container is in use by %s", stack.getId())
+                Logger.log("d", "The container is in use by %s", stack.getId())
                 return True
         return False
 
@@ -382,7 +387,7 @@ class ContainerManager(QObject):
         except MimeTypeNotFoundError:
             return { "status": "error", "message": "Could not determine mime type of file" }
 
-        container_type = UM.Settings.ContainerRegistry.getContainerForMimeType(mime_type)
+        container_type = self._container_registry.getContainerForMimeType(mime_type)
         if not container_type:
             return { "status": "error", "message": "Could not find a container to handle the specified file."}
 
@@ -411,17 +416,17 @@ class ContainerManager(QObject):
     #   \return \type{bool} True if successful, False if not.
     @pyqtSlot(result = bool)
     def updateQualityChanges(self):
-        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        global_stack = Application.getInstance().getGlobalContainerStack()
         if not global_stack:
             return False
 
         self._machine_manager.blurSettings.emit()
 
-        for stack in cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
+        for stack in 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())
+                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())
@@ -438,7 +443,7 @@ class ContainerManager(QObject):
         send_emits_containers = []
 
         # Go through global and extruder stacks and clear their topmost container (the user settings).
-        for stack in cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
+        for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
             container = stack.getTop()
             container.clear()
             send_emits_containers.append(container)
@@ -455,13 +460,13 @@ class ContainerManager(QObject):
     #   \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()
+        global_stack = 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())
+            Logger.log("w", "No quality container found in stack %s, cannot create profile", global_stack.getId())
             return False
 
         self._machine_manager.blurSettings.emit()
@@ -470,17 +475,17 @@ class ContainerManager(QObject):
         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():
+        for stack in 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())
+                Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
                 continue
 
             extruder_id = None if stack is global_stack else QualityManager.getInstance().getParentMachineDefinition(stack.getBottom()).getId()
             new_changes = self._createQualityChanges(quality_container, unique_name,
-                                                     UM.Application.getInstance().getGlobalContainerStack().getBottom(),
+                                                     Application.getInstance().getGlobalContainerStack().getBottom(),
                                                      extruder_id)
             self._performMerge(new_changes, quality_changes_container, clear_settings = False)
             self._performMerge(new_changes, user_container)
@@ -502,7 +507,7 @@ class ContainerManager(QObject):
     #   \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)
+        Logger.log("d", "Attempting to remove the quality change containers with name %s", quality_name)
         containers_found = False
 
         if not quality_name:
@@ -512,7 +517,7 @@ class ContainerManager(QObject):
         activate_quality = quality_name == self._machine_manager.activeQualityName
         activate_quality_type = None
 
-        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        global_stack = Application.getInstance().getGlobalContainerStack()
         if not global_stack or not quality_name:
             return ""
         machine_definition = global_stack.getBottom()
@@ -524,7 +529,7 @@ class ContainerManager(QObject):
             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)
+            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
@@ -547,15 +552,15 @@ class ContainerManager(QObject):
     #   \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)
+        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)
+            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()
+        global_stack = Application.getInstance().getGlobalContainerStack()
         if not global_stack:
             return False
 
@@ -572,7 +577,7 @@ class ContainerManager(QObject):
             container_registry.renameContainer(container.getId(), new_name, self._createUniqueId(stack_id, new_name))
 
         if not containers_to_rename:
-            UM.Logger.log("e", "Unable to rename %s, because we could not find the profile", quality_name)
+            Logger.log("e", "Unable to rename %s, because we could not find the profile", quality_name)
 
         self._machine_manager.activeQualityChanged.emit()
         return True
@@ -588,12 +593,12 @@ class ContainerManager(QObject):
     #   \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()
+        global_stack = Application.getInstance().getGlobalContainerStack()
         if not global_stack or not quality_name:
             return ""
         machine_definition = global_stack.getBottom()
 
-        active_stacks = cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
+        active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
         material_containers = [stack.findContainer(type="material") for stack in active_stacks]
 
         result = self._duplicateQualityOrQualityChangesForMachineType(quality_name, base_name,
@@ -609,16 +614,16 @@ class ContainerManager(QObject):
     #   \param material_instances \type{List[InstanceContainer]}
     #   \return \type{str} the name of the newly created container.
     def _duplicateQualityOrQualityChangesForMachineType(self, quality_name, base_name, machine_definition, material_instances):
-        UM.Logger.log("d", "Attempting to duplicate the quality %s", quality_name)
+        Logger.log("d", "Attempting to duplicate the quality %s", quality_name)
 
         if base_name is None:
             base_name = quality_name
         # Try to find a Quality with the name.
         container = QualityManager.getInstance().findQualityByName(quality_name, machine_definition, material_instances)
         if container:
-            UM.Logger.log("d", "We found a quality to duplicate.")
+            Logger.log("d", "We found a quality to duplicate.")
             return self._duplicateQualityForMachineType(container, base_name, machine_definition)
-        UM.Logger.log("d", "We found a quality_changes to duplicate.")
+        Logger.log("d", "We found a quality_changes to duplicate.")
         # Assume it is a quality changes.
         return self._duplicateQualityChangesForMachineType(quality_name, base_name, machine_definition)
 
@@ -665,11 +670,11 @@ class ContainerManager(QObject):
     def duplicateMaterial(self, material_id):
         containers = self._container_registry.findInstanceContainers(id=material_id)
         if not containers:
-            UM.Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", material_id)
+            Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", material_id)
             return ""
 
         # Ensure all settings are saved.
-        UM.Application.getInstance().saveSettings()
+        Application.getInstance().saveSettings()
 
         # Create a new ID & container to hold the data.
         new_id = self._container_registry.uniqueName(material_id)
@@ -692,7 +697,7 @@ class ContainerManager(QObject):
             ContainerManager.__instance = cls()
         return ContainerManager.__instance
 
-    __instance = None
+    __instance = None   # type: "ContainerManager"
 
     # Factory function, used by QML
     @staticmethod
@@ -713,14 +718,14 @@ class ContainerManager(QObject):
 
     def _updateContainerNameFilters(self):
         self._container_name_filters = {}
-        for plugin_id, container_type in UM.Settings.ContainerRegistry.getContainerTypes():
+        for plugin_id, container_type in self._container_registry.getContainerTypes():
             # Ignore default container types since those are not plugins
-            if container_type in (UM.Settings.InstanceContainer, UM.Settings.ContainerStack, UM.Settings.DefinitionContainer):
+            if container_type in (InstanceContainer, ContainerStack, DefinitionContainer):
                 continue
 
             serialize_type = ""
             try:
-                plugin_metadata = UM.PluginRegistry.getInstance().getMetaData(plugin_id)
+                plugin_metadata = PluginRegistry.getInstance().getMetaData(plugin_id)
                 if plugin_metadata:
                     serialize_type = plugin_metadata["settings_container"]["type"]
                 else:
@@ -728,7 +733,7 @@ class ContainerManager(QObject):
             except KeyError as e:
                 continue
 
-            mime_type = UM.Settings.ContainerRegistry.getMimeTypeForContainer(container_type)
+            mime_type = self._container_registry.getMimeTypeForContainer(container_type)
 
             entry = {
                 "type": serialize_type,
@@ -791,7 +796,7 @@ class ContainerManager(QObject):
         base_id = machine_definition.getId() if extruder_id is None else extruder_id
 
         # Create a new quality_changes container for the quality.
-        quality_changes = UM.Settings.InstanceContainer(self._createUniqueId(base_id, new_name))
+        quality_changes = InstanceContainer(self._createUniqueId(base_id, new_name))
         quality_changes.setName(new_name)
         quality_changes.addMetaDataEntry("type", "quality_changes")
         quality_changes.addMetaDataEntry("quality_type", quality_container.getMetaDataEntry("quality_type"))
@@ -826,7 +831,7 @@ class ContainerManager(QObject):
             if not path.endswith(".curaprofile"):
                 continue
 
-            single_result = UM.Settings.ContainerRegistry.getInstance().importProfile(path)
+            single_result = self._container_registry.importProfile(path)
             if single_result["status"] == "error":
                 status = "error"
             results[single_result["status"]].append(single_result["message"])
@@ -843,7 +848,7 @@ class ContainerManager(QObject):
         path = file_url.toLocalFile()
         if not path:
             return
-        return UM.Settings.ContainerRegistry.getInstance().importProfile(path)
+        return self._container_registry.importProfile(path)
 
     @pyqtSlot("QVariantList", QUrl, str)
     def exportProfile(self, instance_id, file_url, file_type):
@@ -852,4 +857,4 @@ class ContainerManager(QObject):
         path = file_url.toLocalFile()
         if not path:
             return
-        UM.Settings.ContainerRegistry.getInstance().exportProfile(instance_id, path, file_type)
+            self._container_registry.exportProfile(instance_id, path, file_type)

+ 77 - 63
cura/Settings/ExtruderManager.py

@@ -4,13 +4,16 @@
 from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant #For communicating data and events to Qt.
 from UM.FlameProfiler import pyqtSlot
 
-import UM.Application #To get the global container stack to find the current machine.
-import UM.Logger
-from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator #To find which extruders are used in the scene.
-from UM.Scene.SceneNode import SceneNode #To find which extruders are used in the scene.
-import UM.Settings.ContainerRegistry #Finding containers by ID.
-import UM.Settings.SettingFunction
-
+from UM.Application import Application #To get the global container stack to find the current machine.
+from UM.Logger import Logger
+from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
+from UM.Scene.SceneNode import SceneNode
+from UM.Settings.ContainerRegistry import ContainerRegistry #Finding containers by ID.
+from UM.Settings.InstanceContainer import InstanceContainer
+from UM.Settings.SettingFunction import SettingFunction
+from UM.Settings.ContainerStack import ContainerStack
+from UM.Settings.DefinitionContainer import DefinitionContainer
+from typing import Optional
 
 ##  Manages all existing extruder stacks.
 #
@@ -31,7 +34,7 @@ class ExtruderManager(QObject):
         super().__init__(parent)
         self._extruder_trains = { } #Per machine, a dictionary of extruder container stack IDs.
         self._active_extruder_index = 0
-        UM.Application.getInstance().globalContainerStackChanged.connect(self.__globalContainerStackChanged)
+        Application.getInstance().globalContainerStackChanged.connect(self.__globalContainerStackChanged)
         self._global_container_stack_definition_id = None
         self._addCurrentMachineExtruders()
 
@@ -42,34 +45,34 @@ class ExtruderManager(QObject):
     #
     #   \return The unique ID of the currently active extruder stack.
     @pyqtProperty(str, notify = activeExtruderChanged)
-    def activeExtruderStackId(self):
-        if not UM.Application.getInstance().getGlobalContainerStack():
+    def activeExtruderStackId(self) -> Optional[str]:
+        if not Application.getInstance().getGlobalContainerStack():
             return None # No active machine, so no active extruder.
         try:
-            return self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId()
+            return self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId()
         except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
             return None
 
     @pyqtProperty(int, notify = extrudersChanged)
     def extruderCount(self):
-        if not UM.Application.getInstance().getGlobalContainerStack():
+        if not Application.getInstance().getGlobalContainerStack():
             return 0  # No active machine, so no extruders.
         try:
-            return len(self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()])
+            return len(self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()])
         except KeyError:
             return 0
 
     @pyqtProperty("QVariantMap", notify=extrudersChanged)
     def extruderIds(self):
         map = {}
-        for position in self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()]:
-            map[position] = self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()][position].getId()
+        for position in self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()]:
+            map[position] = self._extruder_trains[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]
+    def getQualityChangesIdByExtruderStackId(self, id: str) -> str:
+        for position in self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()]:
+            extruder = self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][position]
             if extruder.getId() == id:
                 return extruder.findContainer(type = "quality_changes").getId()
 
@@ -86,7 +89,7 @@ class ExtruderManager(QObject):
     #
     #   \return The extruder manager.
     @classmethod
-    def getInstance(cls):
+    def getInstance(cls) -> "ExtruderManager":
         if not cls.__instance:
             cls.__instance = ExtruderManager()
         return cls.__instance
@@ -95,16 +98,27 @@ class ExtruderManager(QObject):
     #
     #   \param index The index of the new active extruder.
     @pyqtSlot(int)
-    def setActiveExtruderIndex(self, index):
+    def setActiveExtruderIndex(self, index: int) -> None:
         self._active_extruder_index = index
         self.activeExtruderChanged.emit()
 
     @pyqtProperty(int, notify = activeExtruderChanged)
-    def activeExtruderIndex(self):
+    def activeExtruderIndex(self) -> int:
         return self._active_extruder_index
 
-    def getActiveExtruderStack(self):
-        global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
+    ##  Gets the extruder name of an extruder of the currently active machine.
+    #
+    #   \param index The index of the extruder whose name to get.
+    @pyqtSlot(int, result = str)
+    def getExtruderName(self, index):
+        try:
+            return list(self.getActiveExtruderStacks())[index].getName()
+        except IndexError:
+            return ""
+
+    def getActiveExtruderStack(self) -> ContainerStack:
+        global_container_stack = Application.getInstance().getGlobalContainerStack()
+
         if global_container_stack:
             if global_container_stack.getId() in self._extruder_trains:
                 if str(self._active_extruder_index) in self._extruder_trains[global_container_stack.getId()]:
@@ -113,7 +127,7 @@ class ExtruderManager(QObject):
 
     ##  Get an extruder stack by index
     def getExtruderStack(self, index):
-        global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
+        global_container_stack = Application.getInstance().getGlobalContainerStack()
         if global_container_stack:
             if global_container_stack.getId() in self._extruder_trains:
                 if str(index) in self._extruder_trains[global_container_stack.getId()]:
@@ -125,19 +139,19 @@ class ExtruderManager(QObject):
     #
     #   \param machine_definition   The machine definition to add the extruders for.
     #   \param machine_id           The machine_id to add the extruders for.
-    def addMachineExtruders(self, machine_definition, machine_id):
+    def addMachineExtruders(self, machine_definition: DefinitionContainer, machine_id: str) -> None:
         changed = False
         machine_definition_id = machine_definition.getId()
         if machine_id not in self._extruder_trains:
             self._extruder_trains[machine_id] = { }
             changed = True
-        container_registry = UM.Settings.ContainerRegistry.getInstance()
+        container_registry = ContainerRegistry.getInstance()
         if container_registry:
             # Add the extruder trains that don't exist yet.
             for extruder_definition in container_registry.findDefinitionContainers(machine = machine_definition_id):
                 position = extruder_definition.getMetaDataEntry("position", None)
                 if not position:
-                    UM.Logger.log("w", "Extruder definition %s specifies no position metadata entry.", extruder_definition.getId())
+                    Logger.log("w", "Extruder definition %s specifies no position metadata entry.", extruder_definition.getId())
                 if not container_registry.findContainerStacks(machine = machine_id, position = position): # Doesn't exist yet.
                     self.createExtruderTrain(extruder_definition, machine_definition, position, machine_id)
                     changed = True
@@ -148,7 +162,7 @@ class ExtruderManager(QObject):
                 self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
 
                 # regardless of what the next stack is, we have to set it again, because of signal routing.
-                extruder_train.setNextStack(UM.Application.getInstance().getGlobalContainerStack())
+                extruder_train.setNextStack(Application.getInstance().getGlobalContainerStack())
                 changed = True
         if changed:
             self.extrudersChanged.emit(machine_id)
@@ -177,14 +191,15 @@ class ExtruderManager(QObject):
     #   \param machine_definition   The machine that the extruder train belongs to.
     #   \param position             The position of this extruder train in the extruder slots of the machine.
     #   \param machine_id           The id of the "global" stack this extruder is linked to.
-    def createExtruderTrain(self, extruder_definition, machine_definition, position, machine_id):
+    def createExtruderTrain(self, extruder_definition: DefinitionContainer, machine_definition: DefinitionContainer,
+                            position, machine_id: str) -> None:
         # Cache some things.
-        container_registry = UM.Settings.ContainerRegistry.getInstance()
-        machine_definition_id = UM.Application.getInstance().getMachineManager().getQualityDefinitionId(machine_definition)
+        container_registry = ContainerRegistry.getInstance()
+        machine_definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_definition)
 
         # Create a container stack for this extruder.
         extruder_stack_id = container_registry.uniqueName(extruder_definition.getId())
-        container_stack = UM.Settings.ContainerStack(extruder_stack_id)
+        container_stack = ContainerStack(extruder_stack_id)
         container_stack.setName(extruder_definition.getName())  # Take over the display name to display the stack with.
         container_stack.addMetaDataEntry("type", "extruder_train")
         container_stack.addMetaDataEntry("machine", machine_id)
@@ -204,7 +219,7 @@ class ExtruderManager(QObject):
                 if len(preferred_variants) >= 1:
                     variant = preferred_variants[0]
                 else:
-                    UM.Logger.log("w", "The preferred variant \"%s\" of machine %s doesn't exist or is not a variant profile.", preferred_variant_id, machine_id)
+                    Logger.log("w", "The preferred variant \"%s\" of machine %s doesn't exist or is not a variant profile.", preferred_variant_id, machine_id)
                     # And leave it at the default variant.
         container_stack.addContainer(variant)
 
@@ -234,7 +249,7 @@ class ExtruderManager(QObject):
                 if len(preferred_materials) >= 1:
                     material = preferred_materials[0]
                 else:
-                    UM.Logger.log("w", "The preferred material \"%s\" of machine %s doesn't exist or is not a material profile.", preferred_material_id, machine_id)
+                    Logger.log("w", "The preferred material \"%s\" of machine %s doesn't exist or is not a material profile.", preferred_material_id, machine_id)
                     # And leave it at the default material.
         container_stack.addContainer(material)
 
@@ -253,11 +268,11 @@ class ExtruderManager(QObject):
         if preferred_quality:
             search_criteria["id"] = preferred_quality
 
-        containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
+        containers = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
         if not containers and preferred_quality:
-            UM.Logger.log("w", "The preferred quality \"%s\" of machine %s doesn't exist or is not a quality profile.", preferred_quality, machine_id)
+            Logger.log("w", "The preferred quality \"%s\" of machine %s doesn't exist or is not a quality profile.", preferred_quality, machine_id)
             search_criteria.pop("id", None)
-            containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
+            containers = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
         if containers:
             quality = containers[0]
 
@@ -270,7 +285,7 @@ class ExtruderManager(QObject):
         if user_profile: # There was already a user profile, loaded from settings.
             user_profile = user_profile[0]
         else:
-            user_profile = UM.Settings.InstanceContainer(extruder_stack_id + "_current_settings")  # Add an empty user profile.
+            user_profile = InstanceContainer(extruder_stack_id + "_current_settings")  # Add an empty user profile.
             user_profile.addMetaDataEntry("type", "user")
             user_profile.addMetaDataEntry("extruder", extruder_stack_id)
             user_profile.setDefinition(machine_definition)
@@ -278,7 +293,7 @@ class ExtruderManager(QObject):
         container_stack.addContainer(user_profile)
 
         # regardless of what the next stack is, we have to set it again, because of signal routing.
-        container_stack.setNextStack(UM.Application.getInstance().getGlobalContainerStack())
+        container_stack.setNextStack(Application.getInstance().getGlobalContainerStack())
 
         container_registry.addContainer(container_stack)
 
@@ -291,14 +306,14 @@ class ExtruderManager(QObject):
     #   \param property  \type{str} The property to get.
     #   \return \type{List} the list of results
     def getAllExtruderSettings(self, setting_key, property):
-        global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
+        global_container_stack = Application.getInstance().getGlobalContainerStack()
         if global_container_stack.getProperty("machine_extruder_count", "value") <= 1:
             return [global_container_stack.getProperty(setting_key, property)]
 
         result = []
         for index in self.extruderIds:
             extruder_stack_id = self.extruderIds[str(index)]
-            stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
+            stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
             result.append(stack.getProperty(setting_key, property))
         return result
 
@@ -313,8 +328,8 @@ class ExtruderManager(QObject):
     #
     #   \return A list of extruder stacks.
     def getUsedExtruderStacks(self):
-        global_stack = UM.Application.getInstance().getGlobalContainerStack()
-        container_registry = UM.Settings.ContainerRegistry.getInstance()
+        global_stack = Application.getInstance().getGlobalContainerStack()
+        container_registry = ContainerRegistry.getInstance()
 
         if global_stack.getProperty("machine_extruder_count", "value") <= 1: #For single extrusion.
             return [global_stack]
@@ -324,7 +339,7 @@ class ExtruderManager(QObject):
         #Get the extruders of all meshes in the scene.
         support_enabled = False
         support_interface_enabled = False
-        scene_root = UM.Application.getInstance().getController().getScene().getRoot()
+        scene_root = Application.getInstance().getController().getScene().getRoot()
         meshes = [node for node in DepthFirstIterator(scene_root) if type(node) is SceneNode and node.isSelectable()] #Only use the nodes that will be printed.
         for mesh in meshes:
             extruder_stack_id = mesh.callDecoration("getActiveExtruder")
@@ -355,7 +370,7 @@ class ExtruderManager(QObject):
         try:
             return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids]
         except IndexError:  # One or more of the extruders was not found.
-            UM.Logger.log("e", "Unable to find one or more of the extruders in %s", used_extruder_stack_ids)
+            Logger.log("e", "Unable to find one or more of the extruders in %s", used_extruder_stack_ids)
             return []
 
     ##  Removes the container stack and user profile for the extruders for a specific machine.
@@ -363,27 +378,26 @@ class ExtruderManager(QObject):
     #   \param machine_id The machine to remove the extruders for.
     def removeMachineExtruders(self, machine_id):
         for extruder in self.getMachineExtruders(machine_id):
-            containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "user", extruder = extruder.getId())
+            containers = ContainerRegistry.getInstance().findInstanceContainers(type = "user", extruder = extruder.getId())
             for container in containers:
-                UM.Settings.ContainerRegistry.getInstance().removeContainer(container.getId())
-            UM.Settings.ContainerRegistry.getInstance().removeContainer(extruder.getId())
+                ContainerRegistry.getInstance().removeContainer(container.getId())
+            ContainerRegistry.getInstance().removeContainer(extruder.getId())
 
     ##  Returns extruders for a specific machine.
     #
     #   \param machine_id The machine to get the extruders of.
     def getMachineExtruders(self, machine_id):
         if machine_id not in self._extruder_trains:
-            UM.Logger.log("w", "Tried to get the extruder trains for machine %s, which doesn't exist.", machine_id)
-            return
-        for name in self._extruder_trains[machine_id]:
-            yield self._extruder_trains[machine_id][name]
+            Logger.log("w", "Tried to get the extruder trains for machine %s, which doesn't exist.", machine_id)
+            return []
+        return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]]
 
     ##  Returns a list containing the global stack and active extruder stacks.
     #
     #   The first element is the global container stack, followed by any extruder stacks.
     #   \return \type{List[ContainerStack]}
     def getActiveGlobalAndExtruderStacks(self):
-        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        global_stack = Application.getInstance().getGlobalContainerStack()
         if not global_stack:
             return None
 
@@ -395,7 +409,7 @@ class ExtruderManager(QObject):
     #
     #   \return \type{List[ContainerStack]} a list of
     def getActiveExtruderStacks(self):
-        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        global_stack = Application.getInstance().getGlobalContainerStack()
 
         result = []
         if global_stack:
@@ -403,17 +417,17 @@ class ExtruderManager(QObject):
                 result.append(self._extruder_trains[global_stack.getId()][extruder])
         return result
 
-    def __globalContainerStackChanged(self):
+    def __globalContainerStackChanged(self) -> None:
         self._addCurrentMachineExtruders()
-        global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
+        global_container_stack = Application.getInstance().getGlobalContainerStack()
         if global_container_stack and global_container_stack.getBottom() and global_container_stack.getBottom().getId() != self._global_container_stack_definition_id:
             self._global_container_stack_definition_id = global_container_stack.getBottom().getId()
             self.globalContainerStackDefinitionChanged.emit()
         self.activeExtruderChanged.emit()
 
     ##  Adds the extruders of the currently active machine.
-    def _addCurrentMachineExtruders(self):
-        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+    def _addCurrentMachineExtruders(self) -> None:
+        global_stack = Application.getInstance().getGlobalContainerStack()
         if global_stack and global_stack.getBottom():
             self.addMachineExtruders(global_stack.getBottom(), global_stack.getId())
 
@@ -427,7 +441,7 @@ class ExtruderManager(QObject):
     #           If no extruder has the value, the list will contain the global value.
     @staticmethod
     def getExtruderValues(key):
-        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        global_stack = Application.getInstance().getGlobalContainerStack()
 
         result = []
         for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
@@ -436,7 +450,7 @@ class ExtruderManager(QObject):
             if value is None:
                 continue
 
-            if isinstance(value, UM.Settings.SettingFunction):
+            if isinstance(value, SettingFunction):
                 value = value(extruder)
 
             result.append(value)
@@ -472,10 +486,10 @@ class ExtruderManager(QObject):
 
         if extruder:
             value = extruder.getRawProperty(key, "value")
-            if isinstance(value, UM.Settings.SettingFunction):
+            if isinstance(value, SettingFunction):
                 value = value(extruder)
         else: #Just a value from global.
-            value = UM.Application.getInstance().getGlobalContainerStack().getProperty(key, "value")
+            value = Application.getInstance().getGlobalContainerStack().getProperty(key, "value")
 
         return value
 
@@ -488,7 +502,7 @@ class ExtruderManager(QObject):
     #   \return The effective value
     @staticmethod
     def getResolveOrValue(key):
-        global_stack = UM.Application.getInstance().getGlobalContainerStack()
+        global_stack = Application.getInstance().getGlobalContainerStack()
 
         resolved_value = global_stack.getProperty(key, "resolve")
         if resolved_value is not None:

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