Browse Source

WIP: Make application initialization and start up more clear

 - Create SingleInstance class to handling single instance stuff.
 - Instead of calling getInstance() everywhere, initialize each object
   explicitly in order when application starts and getInstance()s do not
   create instances any more and they merely return the created
   instances.
 - Only set initial values in construtor functions __init__(). Move the
   initialization of context-aware (i.e. things that depend on other
   things) to separate functions.
 - Split application creation and initialziation into several steps and
   them should be called explicitly in the correct order.
Lipu Fei 6 years ago
parent
commit
051dd7a6e9

+ 26 - 24
cura/BuildVolume.py

@@ -1,14 +1,16 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
+import math
+from typing import List, Optional
+
+import numpy
+
+from PyQt5.QtCore import QTimer
 
 
-from cura.Scene.CuraSceneNode import CuraSceneNode
-from cura.Settings.ExtruderManager import ExtruderManager
-from UM.Settings.ContainerRegistry import ContainerRegistry
 from UM.i18n import i18nCatalog
 from UM.i18n import i18nCatalog
 from UM.Scene.Platform import Platform
 from UM.Scene.Platform import Platform
 from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
 from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
 from UM.Scene.SceneNode import SceneNode
 from UM.Scene.SceneNode import SceneNode
-from UM.Application import Application
 from UM.Resources import Resources
 from UM.Resources import Resources
 from UM.Mesh.MeshBuilder import MeshBuilder
 from UM.Mesh.MeshBuilder import MeshBuilder
 from UM.Math.Vector import Vector
 from UM.Math.Vector import Vector
@@ -18,15 +20,13 @@ 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.Message import Message
 from UM.Signal import Signal
 from UM.Signal import Signal
-from PyQt5.QtCore import QTimer
 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 math
+from cura.Scene.CuraSceneNode import CuraSceneNode
+from cura.Settings.ExtruderManager import ExtruderManager
 
 
-from typing import List, Optional
+catalog = i18nCatalog("cura")
 
 
 # Setting for clearance around the prime
 # Setting for clearance around the prime
 PRIME_CLEARANCE = 6.5
 PRIME_CLEARANCE = 6.5
@@ -36,8 +36,10 @@ PRIME_CLEARANCE = 6.5
 class BuildVolume(SceneNode):
 class BuildVolume(SceneNode):
     raftThicknessChanged = Signal()
     raftThicknessChanged = Signal()
 
 
-    def __init__(self, parent = None):
+    def __init__(self, application, parent = None):
         super().__init__(parent)
         super().__init__(parent)
+        self._application = application
+        self._machine_manager = self._application.getMachineManager()
 
 
         self._volume_outline_color = None
         self._volume_outline_color = None
         self._x_axis_color = None
         self._x_axis_color = None
@@ -80,14 +82,14 @@ class BuildVolume(SceneNode):
             " with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
             " with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
 
 
         self._global_container_stack = None
         self._global_container_stack = None
-        Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged)
+        self._application.globalContainerStackChanged.connect(self._onStackChanged)
         self._onStackChanged()
         self._onStackChanged()
 
 
         self._engine_ready = False
         self._engine_ready = False
-        Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
+        self._application.engineCreatedSignal.connect(self._onEngineCreated)
 
 
         self._has_errors = False
         self._has_errors = False
-        Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
+        self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
 
 
         #Objects loaded at the moment. We are connected to the property changed events of these objects.
         #Objects loaded at the moment. We are connected to the property changed events of these objects.
         self._scene_objects = set()
         self._scene_objects = set()
@@ -105,14 +107,14 @@ class BuildVolume(SceneNode):
         # Must be after setting _build_volume_message, apparently that is used in getMachineManager.
         # Must be after setting _build_volume_message, apparently that is used in getMachineManager.
         # activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
         # activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
         # Therefore this works.
         # Therefore this works.
-        Application.getInstance().getMachineManager().activeQualityChanged.connect(self._onStackChanged)
+        self._machine_manager.activeQualityChanged.connect(self._onStackChanged)
 
 
         # This should also ways work, and it is semantically more correct,
         # This should also ways work, and it is semantically more correct,
         # but it does not update the disallowed areas after material change
         # but it does not update the disallowed areas after material change
-        Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged)
+        self._machine_manager.activeStackChanged.connect(self._onStackChanged)
 
 
         # Enable and disable extruder
         # Enable and disable extruder
-        Application.getInstance().getMachineManager().extruderChanged.connect(self.updateNodeBoundaryCheck)
+        self._machine_manager.extruderChanged.connect(self.updateNodeBoundaryCheck)
 
 
         # list of settings which were updated
         # list of settings which were updated
         self._changed_settings_since_last_rebuild = []
         self._changed_settings_since_last_rebuild = []
@@ -122,7 +124,7 @@ class BuildVolume(SceneNode):
             self._scene_change_timer.start()
             self._scene_change_timer.start()
 
 
     def _onSceneChangeTimerFinished(self):
     def _onSceneChangeTimerFinished(self):
-        root = Application.getInstance().getController().getScene().getRoot()
+        root = self._application.getController().getScene().getRoot()
         new_scene_objects = set(node for node in BreadthFirstIterator(root) if node.callDecoration("isSliceable"))
         new_scene_objects = set(node for node in BreadthFirstIterator(root) if node.callDecoration("isSliceable"))
         if new_scene_objects != self._scene_objects:
         if new_scene_objects != self._scene_objects:
             for node in new_scene_objects - self._scene_objects: #Nodes that were added to the scene.
             for node in new_scene_objects - self._scene_objects: #Nodes that were added to the scene.
@@ -181,7 +183,7 @@ class BuildVolume(SceneNode):
         if not self._shader:
         if not self._shader:
             self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader"))
             self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader"))
             self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader"))
             self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader"))
-            theme = Application.getInstance().getTheme()
+            theme = self._application.getTheme()
             self._grid_shader.setUniformValue("u_plateColor", Color(*theme.getColor("buildplate").getRgb()))
             self._grid_shader.setUniformValue("u_plateColor", Color(*theme.getColor("buildplate").getRgb()))
             self._grid_shader.setUniformValue("u_gridColor0", Color(*theme.getColor("buildplate_grid").getRgb()))
             self._grid_shader.setUniformValue("u_gridColor0", Color(*theme.getColor("buildplate_grid").getRgb()))
             self._grid_shader.setUniformValue("u_gridColor1", Color(*theme.getColor("buildplate_grid_minor").getRgb()))
             self._grid_shader.setUniformValue("u_gridColor1", Color(*theme.getColor("buildplate_grid_minor").getRgb()))
@@ -201,7 +203,7 @@ class BuildVolume(SceneNode):
     ##  For every sliceable node, update node._outside_buildarea
     ##  For every sliceable node, update node._outside_buildarea
     #
     #
     def updateNodeBoundaryCheck(self):
     def updateNodeBoundaryCheck(self):
-        root = Application.getInstance().getController().getScene().getRoot()
+        root = self._application.getController().getScene().getRoot()
         nodes = list(BreadthFirstIterator(root))
         nodes = list(BreadthFirstIterator(root))
         group_nodes = []
         group_nodes = []
 
 
@@ -289,11 +291,11 @@ class BuildVolume(SceneNode):
         if not self._width or not self._height or not self._depth:
         if not self._width or not self._height or not self._depth:
             return
             return
 
 
-        if not Application.getInstance()._engine:
+        if not self._application._qml_engine:
             return
             return
 
 
         if not self._volume_outline_color:
         if not self._volume_outline_color:
-            theme = Application.getInstance().getTheme()
+            theme = self._application.getTheme()
             self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb())
             self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb())
             self._x_axis_color = Color(*theme.getColor("x_axis").getRgb())
             self._x_axis_color = Color(*theme.getColor("x_axis").getRgb())
             self._y_axis_color = Color(*theme.getColor("y_axis").getRgb())
             self._y_axis_color = Color(*theme.getColor("y_axis").getRgb())
@@ -465,7 +467,7 @@ class BuildVolume(SceneNode):
             maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
             maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
         )
         )
 
 
-        Application.getInstance().getController().getScene()._maximum_bounds = scale_to_max_bounds
+        self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
 
 
         self.updateNodeBoundaryCheck()
         self.updateNodeBoundaryCheck()
 
 
@@ -518,7 +520,7 @@ class BuildVolume(SceneNode):
             for extruder in extruders:
             for extruder in extruders:
                 extruder.propertyChanged.disconnect(self._onSettingPropertyChanged)
                 extruder.propertyChanged.disconnect(self._onSettingPropertyChanged)
 
 
-        self._global_container_stack = Application.getInstance().getGlobalContainerStack()
+        self._global_container_stack = self._application.getGlobalContainerStack()
 
 
         if self._global_container_stack:
         if self._global_container_stack:
             self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
             self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
@@ -561,7 +563,7 @@ class BuildVolume(SceneNode):
 
 
             if setting_key == "print_sequence":
             if setting_key == "print_sequence":
                 machine_height = self._global_container_stack.getProperty("machine_height", "value")
                 machine_height = self._global_container_stack.getProperty("machine_height", "value")
-                if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
+                if self._application.getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
                     self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
                     self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
                     if self._height < machine_height:
                     if self._height < machine_height:
                         self._build_volume_message.show()
                         self._build_volume_message.show()

+ 0 - 1
cura/CuraActions.py

@@ -12,7 +12,6 @@ from UM.Scene.Selection import Selection
 from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
 from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
 from UM.Operations.GroupedOperation import GroupedOperation
 from UM.Operations.GroupedOperation import GroupedOperation
 from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
 from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
-from UM.Operations.SetTransformOperation import SetTransformOperation
 from UM.Operations.TranslateOperation import TranslateOperation
 from UM.Operations.TranslateOperation import TranslateOperation
 
 
 from cura.Operations.SetParentOperation import SetParentOperation
 from cura.Operations.SetParentOperation import SetParentOperation

+ 247 - 280
cura/CuraApplication.py

@@ -1,9 +1,19 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
 
 
-from PyQt5.QtCore import QObject, QTimer
+import copy
+import json
+import os
+import sys
+import time
+
+import numpy
+
+from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
 from PyQt5.QtNetwork import QLocalServer
 from PyQt5.QtNetwork import QLocalServer
-from PyQt5.QtNetwork import QLocalSocket
+from PyQt5.QtGui import QColor, QIcon
+from PyQt5.QtWidgets import QMessageBox
+from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
 
 
 from UM.Qt.QtApplication import QtApplication
 from UM.Qt.QtApplication import QtApplication
 from UM.Scene.SceneNode import SceneNode
 from UM.Scene.SceneNode import SceneNode
@@ -74,6 +84,7 @@ from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
 
 
 from cura.Machines.VariantManager import VariantManager
 from cura.Machines.VariantManager import VariantManager
 
 
+from .SingleInstance import SingleInstance
 from . import PlatformPhysics
 from . import PlatformPhysics
 from . import BuildVolume
 from . import BuildVolume
 from . import CameraAnimation
 from . import CameraAnimation
@@ -93,22 +104,10 @@ from cura.Settings.ContainerManager import ContainerManager
 
 
 from cura.ObjectsModel import ObjectsModel
 from cura.ObjectsModel import ObjectsModel
 
 
-from PyQt5.QtCore import QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
 from UM.FlameProfiler import pyqtSlot
 from UM.FlameProfiler import pyqtSlot
-from PyQt5.QtGui import QColor, QIcon
-from PyQt5.QtWidgets import QMessageBox
-from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
-
-import sys
-import numpy
-import copy
-import os
-import argparse
-import json
-import time
 
 
 
 
-numpy.seterr(all="ignore")
+numpy.seterr(all = "ignore")
 
 
 MYPY = False
 MYPY = False
 if not MYPY:
 if not MYPY:
@@ -143,19 +142,164 @@ class CuraApplication(QtApplication):
 
 
     Q_ENUMS(ResourceTypes)
     Q_ENUMS(ResourceTypes)
 
 
-    def __init__(self, **kwargs):
+    def __init__(self, *args, **kwargs):
+        super().__init__(name = "cura",
+                         version = CuraVersion,
+                         buildtype = CuraBuildType,
+                         is_debug_mode = CuraDebugMode,
+                         tray_icon_name = "cura-icon-32.png",
+                         **kwargs)
+
+        self.default_theme = "cura-light"
+
         self._boot_loading_time = time.time()
         self._boot_loading_time = time.time()
+
+        self._currently_loading_files = []
+        self._non_sliceable_extensions = []
+
+        # Variables set from CLI
+        self._files_to_open = []
+        self._use_single_instance = False
+        self._trigger_early_crash = False  # For debug only
+
+        self._single_instance = None
+
+        self._cura_package_manager = None
+
+        self._machine_action_manager = None
+
+        self.empty_container = None
+        self.empty_definition_changes_container = None
+        self.empty_variant_container = None
+        self.empty_material_container = None
+        self.empty_quality_container = None
+        self.empty_quality_changes_container = None
+
+        self._variant_manager = None
+        self._material_manager = None
+        self._quality_manager = None
+        self._machine_manager = None
+        self._extruder_manager = None
+        self._container_manager = None
+
+        self._object_manager = None
+        self._build_plate_model = None
+        self._multi_build_plate_model = None
+        self._setting_visibility_presets_model = None
+        self._setting_inheritance_manager = None
+        self._simple_mode_settings_manager = None
+        self._cura_scene_controller = None
+        self._machine_error_checker = None
+
+        self._quality_profile_drop_down_menu_model = None
+        self._custom_quality_profile_drop_down_menu_model = None
+
+        self._physics = None
+        self._volume = None
+        self._output_devices = {}
+        self._print_information = None
+        self._previous_active_tool = None
+        self._platform_activity = False
+        self._scene_bounding_box = AxisAlignedBox.Null
+
+        self._job_name = None
+        self._center_after_select = False
+        self._camera_animation = None
+        self._cura_actions = None
+        self.started = False
+
+        self._message_box_callback = None
+        self._message_box_callback_arguments = []
+        self._preferred_mimetype = ""
+        self._i18n_catalog = None
+
+        self._currently_loading_files = []
+        self._non_sliceable_extensions = []
+        self._additional_components = {}  # Components to add to certain areas in the interface
+
+        self._open_file_queue = []  # A list of files to open (after the application has started)
+
+        self._update_platform_activity_timer = None
+
+        self._need_to_show_user_agreement = True
+
+        from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
+        self._container_registry_class = CuraContainerRegistry
+
+    # Adds command line options to the command line parser. This should be called after the application is created and
+    # before the pre-start.
+    def addCommandLineOptions(self):
+        super().addCommandLineOptions()
+        self._cli_parser.add_argument("--help", "-h",
+                                      action = "store_true",
+                                      default = False,
+                                      help = "Show this help message and exit.")
+        self._cli_parser.add_argument("--single-instance",
+                                      dest = "single_instance",
+                                      action = "store_true",
+                                      default = False)
+        # >> For debugging
+        # Trigger an early crash, i.e. a crash that happens before the application enters its event loop.
+        self._cli_parser.add_argument("--trigger-early-crash",
+                                      dest = "trigger_early_crash",
+                                      action = "store_true",
+                                      default = False,
+                                      help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog.")
+        self._cli_parser.add_argument("file", nargs = "*", help = "Files to load after starting the application.")
+
+    def parseCliOptions(self):
+        super().parseCliOptions()
+
+        if self._cli_args.help:
+            self._cli_parser.print_help()
+            sys.exit(0)
+
+        self._use_single_instance = self._cli_args.single_instance
+        self._trigger_early_crash = self._cli_args.trigger_early_crash
+        for filename in self._cli_args.file:
+            self._files_to_open.append(os.path.abspath(filename))
+
+    def initialize(self) -> None:
+        super().initialize()
+
+        # Initialize the package manager to remove and install scheduled packages.
+        from cura.CuraPackageManager import CuraPackageManager
+        self._cura_package_manager = CuraPackageManager(self)
+        self._cura_package_manager.initialize()
+
+        self.__sendCommandToSingleInstance()
+        self.__addExpectedResourceDirsAndSearchPaths()
+        self.__initializeSettingDefinitionsAndFunctions()
+        self.__addAllResourcesAndContainerResources()
+        self.__addAllEmptyContainers()
+        self.__setLatestResouceVersionsForVersionUpgrade()
+
+        self._machine_action_manager = MachineActionManager.MachineActionManager(self)
+        self._machine_action_manager.initialize()
+
+    def __sendCommandToSingleInstance(self):
+        self._single_instance = SingleInstance(self, self._files_to_open)
+
+        # If we use single instance, try to connect to the single instance server, send commands, and then exit.
+        # If we cannot find an existing single instance server, this is the only instance, so just keep going.
+        if self._use_single_instance:
+            if self._single_instance.startClient():
+                Logger.log("i", "Single instance commands were sent, exiting")
+                sys.exit(0)
+
+    # Adds expected directory names and search paths for Resources.
+    def __addExpectedResourceDirsAndSearchPaths(self):
         # this list of dir names will be used by UM to detect an old cura directory
         # this list of dir names will be used by UM to detect an old cura directory
         for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants"]:
         for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants"]:
             Resources.addExpectedDirNameInData(dir_name)
             Resources.addExpectedDirNameInData(dir_name)
 
 
-        Resources.addSearchPath(os.path.join(QtApplication.getInstallPrefix(), "share", "cura", "resources"))
+        Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
         if not hasattr(sys, "frozen"):
         if not hasattr(sys, "frozen"):
             Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources"))
             Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources"))
 
 
-        self._use_gui = True
-        self._open_file_queue = []  # Files to open when plug-ins are loaded.
-
+    # Adds custom property types, settings types, and extra operators (functions) that need to be registered in
+    # SettingDefinition and SettingFunction.
+    def __initializeSettingDefinitionsAndFunctions(self):
         # Need to do this before ContainerRegistry tries to load the machines
         # Need to do this before ContainerRegistry tries to load the machines
         SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True, read_only = True)
         SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True, read_only = True)
         SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True, read_only = True)
         SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True, read_only = True)
@@ -180,7 +324,8 @@ class CuraApplication(QtApplication):
         SettingFunction.registerOperator("extruderValue", ExtruderManager.getExtruderValue)
         SettingFunction.registerOperator("extruderValue", ExtruderManager.getExtruderValue)
         SettingFunction.registerOperator("resolveOrValue", ExtruderManager.getResolveOrValue)
         SettingFunction.registerOperator("resolveOrValue", ExtruderManager.getResolveOrValue)
 
 
-        ## Add the 4 types of profiles to storage.
+    # Adds all resources and container related resources.
+    def __addAllResourcesAndContainerResources(self) -> None:
         Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
         Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
         Resources.addStorageType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
         Resources.addStorageType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
         Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
         Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
@@ -191,20 +336,64 @@ class CuraApplication(QtApplication):
         Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
         Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
         Resources.addStorageType(self.ResourceTypes.SettingVisibilityPreset, "setting_visibility")
         Resources.addStorageType(self.ResourceTypes.SettingVisibilityPreset, "setting_visibility")
 
 
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality")
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.VariantInstanceContainer, "variant")
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MaterialInstanceContainer, "material")
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.UserInstanceContainer, "user")
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.ExtruderStack, "extruder_train")
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MachineStack, "machine")
-        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
+        self._container_registry.addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality")
+        self._container_registry.addResourceType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
+        self._container_registry.addResourceType(self.ResourceTypes.VariantInstanceContainer, "variant")
+        self._container_registry.addResourceType(self.ResourceTypes.MaterialInstanceContainer, "material")
+        self._container_registry.addResourceType(self.ResourceTypes.UserInstanceContainer, "user")
+        self._container_registry.addResourceType(self.ResourceTypes.ExtruderStack, "extruder_train")
+        self._container_registry.addResourceType(self.ResourceTypes.MachineStack, "machine")
+        self._container_registry.addResourceType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
+
+        Resources.addType(self.ResourceTypes.QmlFiles, "qml")
+        Resources.addType(self.ResourceTypes.Firmware, "firmware")
+
+    # Adds all empty containers.
+    def __addAllEmptyContainers(self) -> None:
+        # Add empty variant, material and quality containers.
+        # Since they are empty, they should never be serialized and instead just programmatically created.
+        # We need them to simplify the switching between materials.
+        empty_container = self._container_registry.getEmptyInstanceContainer()
+        self.empty_container = empty_container
+
+        empty_definition_changes_container = copy.deepcopy(empty_container)
+        empty_definition_changes_container.setMetaDataEntry("id", "empty_definition_changes")
+        empty_definition_changes_container.addMetaDataEntry("type", "definition_changes")
+        self._container_registry.addContainer(empty_definition_changes_container)
+        self.empty_definition_changes_container = empty_definition_changes_container
+
+        empty_variant_container = copy.deepcopy(empty_container)
+        empty_variant_container.setMetaDataEntry("id", "empty_variant")
+        empty_variant_container.addMetaDataEntry("type", "variant")
+        self._container_registry.addContainer(empty_variant_container)
+        self.empty_variant_container = empty_variant_container
+
+        empty_material_container = copy.deepcopy(empty_container)
+        empty_material_container.setMetaDataEntry("id", "empty_material")
+        empty_material_container.addMetaDataEntry("type", "material")
+        self._container_registry.addContainer(empty_material_container)
+        self.empty_material_container = empty_material_container
+
+        empty_quality_container = copy.deepcopy(empty_container)
+        empty_quality_container.setMetaDataEntry("id", "empty_quality")
+        empty_quality_container.setName("Not Supported")
+        empty_quality_container.addMetaDataEntry("quality_type", "not_supported")
+        empty_quality_container.addMetaDataEntry("type", "quality")
+        empty_quality_container.addMetaDataEntry("supported", False)
+        self._container_registry.addContainer(empty_quality_container)
+        self.empty_quality_container = empty_quality_container
 
 
-        ##  Initialise the version upgrade manager with Cura's storage paths.
-        #   Needs to be here to prevent circular dependencies.
-        import UM.VersionUpgradeManager
+        empty_quality_changes_container = copy.deepcopy(empty_container)
+        empty_quality_changes_container.setMetaDataEntry("id", "empty_quality_changes")
+        empty_quality_changes_container.addMetaDataEntry("type", "quality_changes")
+        empty_quality_changes_container.addMetaDataEntry("quality_type", "not_supported")
+        self._container_registry.addContainer(empty_quality_changes_container)
+        self.empty_quality_changes_container = empty_quality_changes_container
 
 
-        UM.VersionUpgradeManager.VersionUpgradeManager.getInstance().setCurrentVersions(
+    # Initializes the version upgrade manager with by providing the paths for each resource type and the latest
+    # versions.
+    def __setLatestResouceVersionsForVersionUpgrade(self):
+        self._version_upgrade_manager.setCurrentVersions(
             {
             {
                 ("quality_changes", InstanceContainer.Version * 1000000 + self.SettingVersion):    (self.ResourceTypes.QualityChangesInstanceContainer, "application/x-uranium-instancecontainer"),
                 ("quality_changes", InstanceContainer.Version * 1000000 + self.SettingVersion):    (self.ResourceTypes.QualityChangesInstanceContainer, "application/x-uranium-instancecontainer"),
                 ("machine_stack", ContainerStack.Version * 1000000 + self.SettingVersion):         (self.ResourceTypes.MachineStack, "application/x-cura-globalstack"),
                 ("machine_stack", ContainerStack.Version * 1000000 + self.SettingVersion):         (self.ResourceTypes.MachineStack, "application/x-cura-globalstack"),
@@ -216,46 +405,9 @@ class CuraApplication(QtApplication):
             }
             }
         )
         )
 
 
-        self._currently_loading_files = []
-        self._non_sliceable_extensions = []
-
-        self._machine_action_manager = MachineActionManager.MachineActionManager()
-        self._machine_manager = None    # This is initialized on demand.
-        self._extruder_manager = None
-        self._material_manager = None
-        self._quality_manager = None
-        self._object_manager = None
-        self._build_plate_model = None
-        self._multi_build_plate_model = None
-        self._setting_visibility_presets_model = None
-        self._setting_inheritance_manager = None
-        self._simple_mode_settings_manager = None
-        self._cura_scene_controller = None
-        self._machine_error_checker = None
-
-        self._additional_components = {} # Components to add to certain areas in the interface
-
-        super().__init__(name = "cura",
-                         version = CuraVersion,
-                         buildtype = CuraBuildType,
-                         is_debug_mode = CuraDebugMode,
-                         tray_icon_name = "cura-icon-32.png",
-                         **kwargs)
-
-        # Initialize the package manager to remove and install scheduled packages.
-        from cura.CuraPackageManager import CuraPackageManager
-        self._cura_package_manager = CuraPackageManager(self)
-        self._cura_package_manager.initialize()
-
-        self.initialize()
-
-        # FOR TESTING ONLY
-        if kwargs["parsed_command_line"].get("trigger_early_crash", False):
-            assert not "This crash is triggered by the trigger_early_crash command line argument."
-
-        self._variant_manager = None
-
-        self.default_theme = "cura-light"
+    # Runs preparations that needs to be done before the starting process.
+    def startSlashWindowPhase(self):
+        super().startSlashWindowPhase()
 
 
         self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
         self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
 
 
@@ -289,23 +441,6 @@ class CuraApplication(QtApplication):
             "SelectionTool",
             "SelectionTool",
             "TranslateTool"
             "TranslateTool"
         ])
         ])
-        self._physics = None
-        self._volume = None
-        self._output_devices = {}
-        self._print_information = None
-        self._previous_active_tool = None
-        self._platform_activity = False
-        self._scene_bounding_box = AxisAlignedBox.Null
-
-        self._job_name = None
-        self._center_after_select = False
-        self._camera_animation = None
-        self._cura_actions = None
-        self.started = False
-
-        self._message_box_callback = None
-        self._message_box_callback_arguments = []
-        self._preferred_mimetype = ""
         self._i18n_catalog = i18nCatalog("cura")
         self._i18n_catalog = i18nCatalog("cura")
 
 
         self._update_platform_activity_timer = QTimer()
         self._update_platform_activity_timer = QTimer()
@@ -318,53 +453,10 @@ class CuraApplication(QtApplication):
         self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
         self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
         self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivityDelayed)
         self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivityDelayed)
 
 
-        Resources.addType(self.ResourceTypes.QmlFiles, "qml")
-        Resources.addType(self.ResourceTypes.Firmware, "firmware")
-
         self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading machines..."))
         self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading machines..."))
 
 
-        # Add empty variant, material and quality containers.
-        # Since they are empty, they should never be serialized and instead just programmatically created.
-        # We need them to simplify the switching between materials.
-        empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer()
-        self.empty_container = empty_container
-
-        empty_definition_changes_container = copy.deepcopy(empty_container)
-        empty_definition_changes_container.setMetaDataEntry("id", "empty_definition_changes")
-        empty_definition_changes_container.addMetaDataEntry("type", "definition_changes")
-        ContainerRegistry.getInstance().addContainer(empty_definition_changes_container)
-        self.empty_definition_changes_container = empty_definition_changes_container
-
-        empty_variant_container = copy.deepcopy(empty_container)
-        empty_variant_container.setMetaDataEntry("id", "empty_variant")
-        empty_variant_container.addMetaDataEntry("type", "variant")
-        ContainerRegistry.getInstance().addContainer(empty_variant_container)
-        self.empty_variant_container = empty_variant_container
-
-        empty_material_container = copy.deepcopy(empty_container)
-        empty_material_container.setMetaDataEntry("id", "empty_material")
-        empty_material_container.addMetaDataEntry("type", "material")
-        ContainerRegistry.getInstance().addContainer(empty_material_container)
-        self.empty_material_container = empty_material_container
-
-        empty_quality_container = copy.deepcopy(empty_container)
-        empty_quality_container.setMetaDataEntry("id", "empty_quality")
-        empty_quality_container.setName("Not Supported")
-        empty_quality_container.addMetaDataEntry("quality_type", "not_supported")
-        empty_quality_container.addMetaDataEntry("type", "quality")
-        empty_quality_container.addMetaDataEntry("supported", False)
-        ContainerRegistry.getInstance().addContainer(empty_quality_container)
-        self.empty_quality_container = empty_quality_container
-
-        empty_quality_changes_container = copy.deepcopy(empty_container)
-        empty_quality_changes_container.setMetaDataEntry("id", "empty_quality_changes")
-        empty_quality_changes_container.addMetaDataEntry("type", "quality_changes")
-        empty_quality_changes_container.addMetaDataEntry("quality_type", "not_supported")
-        ContainerRegistry.getInstance().addContainer(empty_quality_changes_container)
-        self.empty_quality_changes_container = empty_quality_changes_container
-
-        with ContainerRegistry.getInstance().lockFile():
-            ContainerRegistry.getInstance().loadAllMetadata()
+        with self._container_registry.lockFile():
+            self._container_registry.loadAllMetadata()
 
 
         # set the setting version for Preferences
         # set the setting version for Preferences
         preferences = Preferences.getInstance()
         preferences = Preferences.getInstance()
@@ -411,13 +503,10 @@ class CuraApplication(QtApplication):
 
 
         self.getCuraSceneController().setActiveBuildPlate(0)  # Initialize
         self.getCuraSceneController().setActiveBuildPlate(0)  # Initialize
 
 
-        self._quality_profile_drop_down_menu_model = None
-        self._custom_quality_profile_drop_down_menu_model = None
-
         CuraApplication.Created = True
         CuraApplication.Created = True
 
 
     def _onEngineCreated(self):
     def _onEngineCreated(self):
-        self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
+        self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
 
 
     @pyqtProperty(bool)
     @pyqtProperty(bool)
     def needToShowUserAgreement(self):
     def needToShowUserAgreement(self):
@@ -515,10 +604,6 @@ class CuraApplication(QtApplication):
     def setDefaultPath(self, key, default_path):
     def setDefaultPath(self, key, default_path):
         Preferences.getInstance().setValue("local_file/%s" % key, QUrl(default_path).toLocalFile())
         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)
     ##  Handle loading of all plugin types (and the backend explicitly)
     #   \sa PluginRegistry
     #   \sa PluginRegistry
     def _loadPlugins(self):
     def _loadPlugins(self):
@@ -543,127 +628,8 @@ class CuraApplication(QtApplication):
 
 
         self._plugins_loaded = True
         self._plugins_loaded = True
 
 
-    @classmethod
-    def addCommandLineOptions(cls, parser, parsed_command_line = None):
-        if parsed_command_line is None:
-            parsed_command_line = {}
-        super().addCommandLineOptions(parser, parsed_command_line = parsed_command_line)
-        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, parser = None, parsed_command_line = None):
-        if parsed_command_line is None:
-            parsed_command_line = {}
-
-        # Peek the arguments and look for the 'single-instance' flag.
-        if not parser:
-            parser = argparse.ArgumentParser(prog = "cura", add_help = False)  # pylint: disable=bad-whitespace
-        CuraApplication.addCommandLineOptions(parser, parsed_command_line = parsed_command_line)
-        # Important: It is important to keep this line here!
-        #            In Uranium we allow to pass unknown arguments to the final executable or script.
-        parsed_command_line.update(vars(parser.parse_known_args()[0]))
-
-        if 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 preRun(self):
-        # Last check for unknown commandline arguments
-        parser = self.getCommandlineParser()
-        parser.add_argument("--help", "-h",
-                            action='store_true',
-                            default = False,
-                            help = "Show this help message and exit."
-                            )
-        parsed_args = vars(parser.parse_args()) # This won't allow unknown arguments
-        if parsed_args["help"]:
-            parser.print_help()
-            sys.exit(0)
-
     def run(self):
     def run(self):
-        self.preRun()
-
-        container_registry = ContainerRegistry.getInstance()
+        container_registry = self._container_registry
 
 
         Logger.log("i", "Initializing variant manager")
         Logger.log("i", "Initializing variant manager")
         self._variant_manager = VariantManager(container_registry)
         self._variant_manager = VariantManager(container_registry)
@@ -682,20 +648,25 @@ class CuraApplication(QtApplication):
         Logger.log("i", "Initializing machine manager")
         Logger.log("i", "Initializing machine manager")
         self._machine_manager = MachineManager(self)
         self._machine_manager = MachineManager(self)
 
 
+        Logger.log("i", "Initializing container manager")
+        self._container_manager = ContainerManager(self)
+
         Logger.log("i", "Initializing machine error checker")
         Logger.log("i", "Initializing machine error checker")
         self._machine_error_checker = MachineErrorChecker(self)
         self._machine_error_checker = MachineErrorChecker(self)
         self._machine_error_checker.initialize()
         self._machine_error_checker.initialize()
 
 
-        # Check if we should run as single instance or not
-        self._setUpSingleInstanceServer()
+        # Check if we should run as single instance or not. If so, set up a local socket server which listener which
+        # coordinates multiple Cura instances and accepts commands.
+        if self._use_single_instance:
+            self.__setUpSingleInstanceServer()
 
 
         # Setup scene and build volume
         # Setup scene and build volume
         root = self.getController().getScene().getRoot()
         root = self.getController().getScene().getRoot()
-        self._volume = BuildVolume.BuildVolume(self.getController().getScene().getRoot())
+        self._volume = BuildVolume.BuildVolume(self, root)
         Arrange.build_volume = self._volume
         Arrange.build_volume = self._volume
 
 
         # initialize info objects
         # initialize info objects
-        self._print_information = PrintInformation.PrintInformation()
+        self._print_information = PrintInformation.PrintInformation(self)
         self._cura_actions = CuraActions.CuraActions(self)
         self._cura_actions = CuraActions.CuraActions(self)
 
 
         # Initialize setting visibility presets model
         # Initialize setting visibility presets model
@@ -704,7 +675,7 @@ class CuraApplication(QtApplication):
         Preferences.getInstance().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
         Preferences.getInstance().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
 
 
         # Detect in which mode to run and execute that mode
         # Detect in which mode to run and execute that mode
-        if self.getCommandLineOption("headless", False):
+        if self._is_headless:
             self.runWithoutGUI()
             self.runWithoutGUI()
         else:
         else:
             self.runWithGUI()
             self.runWithGUI()
@@ -713,7 +684,6 @@ class CuraApplication(QtApplication):
         self.initializationFinished.emit()
         self.initializationFinished.emit()
         Logger.log("d", "Booting Cura took %s seconds", time.time() - self._boot_loading_time)
         Logger.log("d", "Booting Cura took %s seconds", time.time() - self._boot_loading_time)
 
 
-
         # For now use a timer to postpone some things that need to be done after the application and GUI are
         # For now use a timer to postpone some things that need to be done after the application and GUI are
         # initialized, for example opening files because they may show dialogs which can be closed due to incomplete
         # initialized, for example opening files because they may show dialogs which can be closed due to incomplete
         # GUI initialization.
         # GUI initialization.
@@ -725,8 +695,12 @@ class CuraApplication(QtApplication):
 
 
         self.exec_()
         self.exec_()
 
 
+    def __setUpSingleInstanceServer(self):
+        if self._use_single_instance:
+            self._single_instance.startServer()
+
     def _onPostStart(self):
     def _onPostStart(self):
-        for file_name in self.getCommandLineOption("file", []):
+        for file_name in self._files_to_open:
             self.callLater(self._openFile, file_name)
             self.callLater(self._openFile, file_name)
         for file_name in self._open_file_queue:  # Open all the files that were queued up while plug-ins were loading.
         for file_name in self._open_file_queue:  # Open all the files that were queued up while plug-ins were loading.
             self.callLater(self._openFile, file_name)
             self.callLater(self._openFile, file_name)
@@ -735,13 +709,10 @@ class CuraApplication(QtApplication):
 
 
     ##  Run Cura without GUI elements and interaction (server mode).
     ##  Run Cura without GUI elements and interaction (server mode).
     def runWithoutGUI(self):
     def runWithoutGUI(self):
-        self._use_gui = False
         self.closeSplash()
         self.closeSplash()
 
 
     ##  Run Cura with GUI (desktop mode).
     ##  Run Cura with GUI (desktop mode).
     def runWithGUI(self):
     def runWithGUI(self):
-        self._use_gui = True
-
         self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
         self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
 
 
         controller = self.getController()
         controller = self.getController()
@@ -791,9 +762,6 @@ class CuraApplication(QtApplication):
         # Hide the splash screen
         # Hide the splash screen
         self.closeSplash()
         self.closeSplash()
 
 
-    def hasGui(self):
-        return self._use_gui
-
     @pyqtSlot(result = QObject)
     @pyqtSlot(result = QObject)
     def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
     def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
         return self._setting_visibility_presets_model
         return self._setting_visibility_presets_model
@@ -808,7 +776,7 @@ class CuraApplication(QtApplication):
 
 
     def getExtruderManager(self, *args):
     def getExtruderManager(self, *args):
         if self._extruder_manager is None:
         if self._extruder_manager is None:
-            self._extruder_manager = ExtruderManager.createExtruderManager()
+            self._extruder_manager = ExtruderManager()
         return self._extruder_manager
         return self._extruder_manager
 
 
     @pyqtSlot(result = QObject)
     @pyqtSlot(result = QObject)
@@ -932,7 +900,7 @@ class CuraApplication(QtApplication):
         qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
         qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
         qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
         qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
         qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
         qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
-        qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.createContainerManager)
+        qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
 
 
         # As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
         # 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")))
         actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
@@ -1520,8 +1488,7 @@ class CuraApplication(QtApplication):
                 # see GroupDecorator._onChildrenChanged
                 # see GroupDecorator._onChildrenChanged
 
 
     def _createSplashScreen(self):
     def _createSplashScreen(self):
-        run_headless = self.getCommandLineOption("headless", False)
-        if run_headless:
+        if self._is_headless:
             return None
             return None
         return CuraSplashScreen.CuraSplashScreen()
         return CuraSplashScreen.CuraSplashScreen()
 
 

+ 12 - 8
cura/MachineActionManager.py

@@ -1,13 +1,13 @@
-# Copyright (c) 2016 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
+
+from PyQt5.QtCore import QObject
+
+from UM.FlameProfiler import pyqtSlot
 from UM.Logger import Logger
 from UM.Logger import Logger
 from UM.PluginRegistry import PluginRegistry  # So MachineAction can be added as plugin type
 from UM.PluginRegistry import PluginRegistry  # So MachineAction can be added as plugin type
-
-from UM.Settings.ContainerRegistry import ContainerRegistry
 from UM.Settings.DefinitionContainer import DefinitionContainer
 from UM.Settings.DefinitionContainer import DefinitionContainer
 
 
-from PyQt5.QtCore import QObject
-from UM.FlameProfiler import pyqtSlot
 
 
 ##  Raised when trying to add an unknown machine action as a required action
 ##  Raised when trying to add an unknown machine action as a required action
 class UnknownMachineActionError(Exception):
 class UnknownMachineActionError(Exception):
@@ -20,23 +20,27 @@ class NotUniqueMachineActionError(Exception):
 
 
 
 
 class MachineActionManager(QObject):
 class MachineActionManager(QObject):
-    def __init__(self, parent = None):
+    def __init__(self, application, parent = None):
         super().__init__(parent)
         super().__init__(parent)
+        self._application = application
 
 
         self._machine_actions = {}  # Dict of all known machine actions
         self._machine_actions = {}  # Dict of all known machine actions
         self._required_actions = {}  # Dict of all required actions by definition ID
         self._required_actions = {}  # Dict of all required actions by definition ID
         self._supported_actions = {}  # Dict of all supported actions by definition ID
         self._supported_actions = {}  # Dict of all supported actions by definition ID
         self._first_start_actions = {}  # Dict of all actions that need to be done when first added by definition ID
         self._first_start_actions = {}  # Dict of all actions that need to be done when first added by definition ID
 
 
+    def initialize(self):
+        container_registry = self._application.getContainerRegistry()
+
         # Add machine_action as plugin type
         # Add machine_action as plugin type
         PluginRegistry.addType("machine_action", self.addMachineAction)
         PluginRegistry.addType("machine_action", self.addMachineAction)
 
 
         # Ensure that all containers that were registered before creation of this registry are also handled.
         # Ensure that all containers that were registered before creation of this registry are also handled.
         # This should not have any effect, but it makes it safer if we ever refactor the order of things.
         # This should not have any effect, but it makes it safer if we ever refactor the order of things.
-        for container in ContainerRegistry.getInstance().findDefinitionContainers():
+        for container in container_registry.findDefinitionContainers():
             self._onContainerAdded(container)
             self._onContainerAdded(container)
 
 
-        ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
+        container_registry.containerAdded.connect(self._onContainerAdded)
 
 
     def _onContainerAdded(self, container):
     def _onContainerAdded(self, container):
         ## Ensure that the actions are added to this manager
         ## Ensure that the actions are added to this manager

+ 12 - 12
cura/PrintInformation.py

@@ -1,24 +1,24 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
 
 
-from typing import Dict
+import json
 import math
 import math
-import os.path
+import os
 import unicodedata
 import unicodedata
-import json
 import re  # To create abbreviations for printer names.
 import re  # To create abbreviations for printer names.
+from typing import Dict
 
 
 from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
 from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
 
 
-from UM.Application import Application
+from UM.i18n import i18nCatalog
 from UM.Logger import Logger
 from UM.Logger import Logger
-from UM.Qt.Duration import Duration
 from UM.Preferences import Preferences
 from UM.Preferences import Preferences
+from UM.Qt.Duration import Duration
 from UM.Scene.SceneNode import SceneNode
 from UM.Scene.SceneNode import SceneNode
-from UM.i18n import i18nCatalog
 
 
 catalog = i18nCatalog("cura")
 catalog = i18nCatalog("cura")
 
 
+
 ##  A class for processing and calculating minimum, current and maximum print time as well as managing the job name
 ##  A class for processing and calculating minimum, current and maximum print time as well as managing the job name
 #
 #
 #   This class contains all the logic relating to calculation and slicing for the
 #   This class contains all the logic relating to calculation and slicing for the
@@ -47,8 +47,9 @@ class PrintInformation(QObject):
         ActiveMachineChanged = 3
         ActiveMachineChanged = 3
         Other = 4
         Other = 4
 
 
-    def __init__(self, parent = None):
+    def __init__(self, application, parent = None):
         super().__init__(parent)
         super().__init__(parent)
+        self._application = application
 
 
         self.initializeCuraMessagePrintTimeProperties()
         self.initializeCuraMessagePrintTimeProperties()
 
 
@@ -59,10 +60,10 @@ class PrintInformation(QObject):
 
 
         self._pre_sliced = False
         self._pre_sliced = False
 
 
-        self._backend = Application.getInstance().getBackend()
+        self._backend = self._application.getBackend()
         if self._backend:
         if self._backend:
             self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
             self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
-        Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
+        self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
 
 
         self._base_name = ""
         self._base_name = ""
         self._abbr_machine = ""
         self._abbr_machine = ""
@@ -71,7 +72,6 @@ class PrintInformation(QObject):
         self._active_build_plate = 0
         self._active_build_plate = 0
         self._initVariablesWithBuildPlate(self._active_build_plate)
         self._initVariablesWithBuildPlate(self._active_build_plate)
 
 
-        self._application = Application.getInstance()
         self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
         self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
 
 
         self._application.globalContainerStackChanged.connect(self._updateJobName)
         self._application.globalContainerStackChanged.connect(self._updateJobName)
@@ -199,7 +199,7 @@ class PrintInformation(QObject):
         self._current_print_time[build_plate_number].setDuration(total_estimated_time)
         self._current_print_time[build_plate_number].setDuration(total_estimated_time)
 
 
     def _calculateInformation(self, build_plate_number):
     def _calculateInformation(self, build_plate_number):
-        global_stack = Application.getInstance().getGlobalContainerStack()
+        global_stack = self._application.getGlobalContainerStack()
         if global_stack is None:
         if global_stack is None:
             return
             return
 
 
@@ -358,7 +358,7 @@ class PrintInformation(QObject):
     ##  Created an acronymn-like abbreviated machine name from the currently active machine name
     ##  Created an acronymn-like abbreviated machine name from the currently active machine name
     #   Called each time the global stack is switched
     #   Called each time the global stack is switched
     def _setAbbreviatedMachineName(self):
     def _setAbbreviatedMachineName(self):
-        global_container_stack = Application.getInstance().getGlobalContainerStack()
+        global_container_stack = self._application.getGlobalContainerStack()
         if not global_container_stack:
         if not global_container_stack:
             self._abbr_machine = ""
             self._abbr_machine = ""
             return
             return

+ 1 - 1
cura/Scene/ConvexHullNode.py

@@ -24,7 +24,7 @@ class ConvexHullNode(SceneNode):
         self._original_parent = parent
         self._original_parent = parent
 
 
         # Color of the drawn convex hull
         # Color of the drawn convex hull
-        if Application.getInstance().hasGui():
+        if not Application.getInstance().getIsHeadLess():
             self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
             self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
         else:
         else:
             self._color = Color(0, 0, 0)
             self._color = Color(0, 0, 0)

+ 33 - 39
cura/Settings/ContainerManager.py

@@ -1,32 +1,25 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
 
 
-import os.path
+import os
 import urllib.parse
 import urllib.parse
 import uuid
 import uuid
 from typing import Dict, Union
 from typing import Dict, Union
 
 
 from PyQt5.QtCore import QObject, QUrl, QVariant
 from PyQt5.QtCore import QObject, QUrl, QVariant
-from UM.FlameProfiler import pyqtSlot
 from PyQt5.QtWidgets import QMessageBox
 from PyQt5.QtWidgets import QMessageBox
 
 
-from UM.PluginRegistry import PluginRegistry
-from UM.SaveFile import SaveFile
-from UM.Platform import Platform
-from UM.MimeTypeDatabase import MimeTypeDatabase
-
+from UM.i18n import i18nCatalog
+from UM.FlameProfiler import pyqtSlot
 from UM.Logger import Logger
 from UM.Logger import Logger
-from UM.Application import Application
+from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
+from UM.Platform import Platform
+from UM.SaveFile import SaveFile
+from UM.Settings.ContainerFormatError import ContainerFormatError
 from UM.Settings.ContainerStack import ContainerStack
 from UM.Settings.ContainerStack import ContainerStack
 from UM.Settings.DefinitionContainer import DefinitionContainer
 from UM.Settings.DefinitionContainer import DefinitionContainer
 from UM.Settings.InstanceContainer import InstanceContainer
 from UM.Settings.InstanceContainer import InstanceContainer
 
 
-from UM.MimeTypeDatabase import MimeTypeNotFoundError
-from UM.Settings.ContainerFormatError import ContainerFormatError
-from UM.Settings.ContainerRegistry import ContainerRegistry
-from cura.Settings.ExtruderManager import ExtruderManager
-from UM.i18n import i18nCatalog
-
 catalog = i18nCatalog("cura")
 catalog = i18nCatalog("cura")
 
 
 
 
@@ -36,11 +29,17 @@ catalog = i18nCatalog("cura")
 #   from within QML. We want to be able to trigger things like removing a container
 #   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.
 #   when a certain action happens. This can be done through this class.
 class ContainerManager(QObject):
 class ContainerManager(QObject):
-    def __init__(self, parent = None):
-        super().__init__(parent)
 
 
-        self._application = Application.getInstance()
-        self._container_registry = ContainerRegistry.getInstance()
+    def __init__(self, application):
+        if ContainerManager.__instance is not None:
+            raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
+        ContainerManager.__instance = self
+
+        super().__init__(parent = application)
+
+        self._application = application
+        self._plugin_registry = self._application.getPluginRegistry()
+        self._container_registry = self._application.getContainerRegistry()
         self._machine_manager = self._application.getMachineManager()
         self._machine_manager = self._application.getMachineManager()
         self._material_manager = self._application.getMaterialManager()
         self._material_manager = self._application.getMaterialManager()
         self._container_name_filters = {}
         self._container_name_filters = {}
@@ -129,7 +128,7 @@ class ContainerManager(QObject):
         container.setProperty(setting_key, property_name, property_value)
         container.setProperty(setting_key, property_name, property_value)
 
 
         basefile = container.getMetaDataEntry("base_file", container_id)
         basefile = container.getMetaDataEntry("base_file", container_id)
-        for sibbling_container in ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
+        for sibbling_container in self._container_registry.findInstanceContainers(base_file = basefile):
             if sibbling_container != container:
             if sibbling_container != container:
                 sibbling_container.setProperty(setting_key, property_name, property_value)
                 sibbling_container.setProperty(setting_key, property_name, property_value)
 
 
@@ -307,13 +306,15 @@ class ContainerManager(QObject):
     #   \return \type{bool} True if successful, False if not.
     #   \return \type{bool} True if successful, False if not.
     @pyqtSlot(result = bool)
     @pyqtSlot(result = bool)
     def updateQualityChanges(self):
     def updateQualityChanges(self):
-        global_stack = Application.getInstance().getGlobalContainerStack()
+        global_stack = self._machine_manager.activeMachine
         if not global_stack:
         if not global_stack:
             return False
             return False
 
 
         self._machine_manager.blurSettings.emit()
         self._machine_manager.blurSettings.emit()
 
 
-        for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
+        global_stack = self._machine_manager.activeMachine
+        extruder_stacks = list(global_stack.extruders.values())
+        for stack in [global_stack] + extruder_stacks:
             # Find the quality_changes container for this stack and merge the contents of the top container into it.
             # Find the quality_changes container for this stack and merge the contents of the top container into it.
             quality_changes = stack.qualityChanges
             quality_changes = stack.qualityChanges
             if not quality_changes or self._container_registry.isReadOnly(quality_changes.getId()):
             if not quality_changes or self._container_registry.isReadOnly(quality_changes.getId()):
@@ -334,13 +335,15 @@ class ContainerManager(QObject):
         send_emits_containers = []
         send_emits_containers = []
 
 
         # Go through global and extruder stacks and clear their topmost container (the user settings).
         # Go through global and extruder stacks and clear their topmost container (the user settings).
-        for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
+        global_stack = self._machine_manager.activeMachine
+        extruder_stacks = list(global_stack.extruders.values())
+        for stack in [global_stack] + extruder_stacks:
             container = stack.userChanges
             container = stack.userChanges
             container.clear()
             container.clear()
             send_emits_containers.append(container)
             send_emits_containers.append(container)
 
 
         # user changes are possibly added to make the current setup match the current enabled extruders
         # user changes are possibly added to make the current setup match the current enabled extruders
-        Application.getInstance().getMachineManager().correctExtruderSettings()
+        self._machine_manager.correctExtruderSettings()
 
 
         for container in send_emits_containers:
         for container in send_emits_containers:
             container.sendPostponedEmits()
             container.sendPostponedEmits()
@@ -381,21 +384,6 @@ class ContainerManager(QObject):
         if container is not None:
         if container is not None:
             container.setMetaDataEntry("GUID", new_guid)
             container.setMetaDataEntry("GUID", new_guid)
 
 
-    ##  Get the singleton instance for this class.
-    @classmethod
-    def getInstance(cls) -> "ContainerManager":
-        # Note: Explicit use of class name to prevent issues with inheritance.
-        if ContainerManager.__instance is None:
-            ContainerManager.__instance = cls()
-        return ContainerManager.__instance
-
-    __instance = None   # type: "ContainerManager"
-
-    # Factory function, used by QML
-    @staticmethod
-    def createContainerManager(engine, js_engine):
-        return ContainerManager.getInstance()
-
     def _performMerge(self, merge_into, merge, clear_settings = True):
     def _performMerge(self, merge_into, merge, clear_settings = True):
         if merge == merge_into:
         if merge == merge_into:
             return
             return
@@ -415,7 +403,7 @@ class ContainerManager(QObject):
 
 
             serialize_type = ""
             serialize_type = ""
             try:
             try:
-                plugin_metadata = PluginRegistry.getInstance().getMetaData(plugin_id)
+                plugin_metadata = self._plugin_registry.getMetaData(plugin_id)
                 if plugin_metadata:
                 if plugin_metadata:
                     serialize_type = plugin_metadata["settings_container"]["type"]
                     serialize_type = plugin_metadata["settings_container"]["type"]
                 else:
                 else:
@@ -470,3 +458,9 @@ class ContainerManager(QObject):
 
 
         container_list = [n.getContainer() for n in quality_changes_group.getAllNodes() if n.getContainer() is not None]
         container_list = [n.getContainer() for n in quality_changes_group.getAllNodes() if n.getContainer() is not None]
         self._container_registry.exportQualityProfile(container_list, path, file_type)
         self._container_registry.exportQualityProfile(container_list, path, file_type)
+
+    __instance = None
+
+    @classmethod
+    def getInstance(cls, *args, **kwargs) -> "ContainerManager":
+        return cls.__instance

+ 12 - 7
cura/Settings/CuraStackBuilder.py

@@ -7,7 +7,6 @@ from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
 from UM.Logger import Logger
 from UM.Logger import Logger
 from UM.Settings.Interfaces import DefinitionContainerInterface
 from UM.Settings.Interfaces import DefinitionContainerInterface
 from UM.Settings.InstanceContainer import InstanceContainer
 from UM.Settings.InstanceContainer import InstanceContainer
-from UM.Settings.ContainerRegistry import ContainerRegistry
 
 
 from cura.Machines.VariantManager import VariantType
 from cura.Machines.VariantManager import VariantType
 from .GlobalStack import GlobalStack
 from .GlobalStack import GlobalStack
@@ -29,7 +28,7 @@ class CuraStackBuilder:
         variant_manager = application.getVariantManager()
         variant_manager = application.getVariantManager()
         material_manager = application.getMaterialManager()
         material_manager = application.getMaterialManager()
         quality_manager = application.getQualityManager()
         quality_manager = application.getQualityManager()
-        registry = ContainerRegistry.getInstance()
+        registry = application.getContainerRegistry()
 
 
         definitions = registry.findDefinitionContainers(id = definition_id)
         definitions = registry.findDefinitionContainers(id = definition_id)
         if not definitions:
         if not definitions:
@@ -142,6 +141,7 @@ class CuraStackBuilder:
                             variant_container, material_container, quality_container, global_stack) -> ExtruderStack:
                             variant_container, material_container, quality_container, global_stack) -> ExtruderStack:
         from cura.CuraApplication import CuraApplication
         from cura.CuraApplication import CuraApplication
         application = CuraApplication.getInstance()
         application = CuraApplication.getInstance()
+        registry = application.getContainerRegistry()
 
 
         stack = ExtruderStack(new_stack_id, parent = global_stack)
         stack = ExtruderStack(new_stack_id, parent = global_stack)
         stack.setName(extruder_definition.getName())
         stack.setName(extruder_definition.getName())
@@ -162,7 +162,7 @@ class CuraStackBuilder:
         # Only add the created containers to the registry after we have set all the other
         # Only add the created containers to the registry after we have set all the other
         # properties. This makes the create operation more transactional, since any problems
         # properties. This makes the create operation more transactional, since any problems
         # setting properties will not result in incomplete containers being added.
         # setting properties will not result in incomplete containers being added.
-        ContainerRegistry.getInstance().addContainer(user_container)
+        registry.addContainer(user_container)
 
 
         return stack
         return stack
 
 
@@ -178,6 +178,7 @@ class CuraStackBuilder:
                           variant_container, material_container, quality_container) -> GlobalStack:
                           variant_container, material_container, quality_container) -> GlobalStack:
         from cura.CuraApplication import CuraApplication
         from cura.CuraApplication import CuraApplication
         application = CuraApplication.getInstance()
         application = CuraApplication.getInstance()
+        registry = application.getContainerRegistry()
 
 
         stack = GlobalStack(new_stack_id)
         stack = GlobalStack(new_stack_id)
         stack.setDefinition(definition)
         stack.setDefinition(definition)
@@ -193,7 +194,7 @@ class CuraStackBuilder:
         stack.qualityChanges = application.empty_quality_changes_container
         stack.qualityChanges = application.empty_quality_changes_container
         stack.userChanges = user_container
         stack.userChanges = user_container
 
 
-        ContainerRegistry.getInstance().addContainer(user_container)
+        registry.addContainer(user_container)
 
 
         return stack
         return stack
 
 
@@ -201,8 +202,10 @@ class CuraStackBuilder:
     def createUserChangesContainer(cls, container_name: str, definition_id: str, stack_id: str,
     def createUserChangesContainer(cls, container_name: str, definition_id: str, stack_id: str,
                                    is_global_stack: bool) -> "InstanceContainer":
                                    is_global_stack: bool) -> "InstanceContainer":
         from cura.CuraApplication import CuraApplication
         from cura.CuraApplication import CuraApplication
+        application = CuraApplication.getInstance()
+        registry = application.getContainerRegistry()
 
 
-        unique_container_name = ContainerRegistry.getInstance().uniqueName(container_name)
+        unique_container_name = registry.uniqueName(container_name)
 
 
         container = InstanceContainer(unique_container_name)
         container = InstanceContainer(unique_container_name)
         container.setDefinition(definition_id)
         container.setDefinition(definition_id)
@@ -217,15 +220,17 @@ class CuraStackBuilder:
     @classmethod
     @classmethod
     def createDefinitionChangesContainer(cls, container_stack, container_name):
     def createDefinitionChangesContainer(cls, container_stack, container_name):
         from cura.CuraApplication import CuraApplication
         from cura.CuraApplication import CuraApplication
+        application = CuraApplication.getInstance()
+        registry = application.getContainerRegistry()
 
 
-        unique_container_name = ContainerRegistry.getInstance().uniqueName(container_name)
+        unique_container_name = registry.uniqueName(container_name)
 
 
         definition_changes_container = InstanceContainer(unique_container_name)
         definition_changes_container = InstanceContainer(unique_container_name)
         definition_changes_container.setDefinition(container_stack.getBottom().getId())
         definition_changes_container.setDefinition(container_stack.getBottom().getId())
         definition_changes_container.addMetaDataEntry("type", "definition_changes")
         definition_changes_container.addMetaDataEntry("type", "definition_changes")
         definition_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
         definition_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
 
 
-        ContainerRegistry.getInstance().addContainer(definition_changes_container)
+        registry.addContainer(definition_changes_container)
         container_stack.definitionChanges = definition_changes_container
         container_stack.definitionChanges = definition_changes_container
 
 
         return definition_changes_container
         return definition_changes_container

+ 11 - 22
cura/Settings/ExtruderManager.py

@@ -15,6 +15,7 @@ from UM.Settings.SettingFunction import SettingFunction
 from UM.Settings.SettingInstance import SettingInstance
 from UM.Settings.SettingInstance import SettingInstance
 from UM.Settings.ContainerStack import ContainerStack
 from UM.Settings.ContainerStack import ContainerStack
 from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
 from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
+
 from typing import Optional, List, TYPE_CHECKING, Union
 from typing import Optional, List, TYPE_CHECKING, Union
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
@@ -29,6 +30,10 @@ class ExtruderManager(QObject):
 
 
     ##  Registers listeners and such to listen to changes to the extruders.
     ##  Registers listeners and such to listen to changes to the extruders.
     def __init__(self, parent = None):
     def __init__(self, parent = None):
+        if ExtruderManager.__instance is not None:
+            raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
+        ExtruderManager.__instance = self
+
         super().__init__(parent)
         super().__init__(parent)
 
 
         self._application = Application.getInstance()
         self._application = Application.getInstance()
@@ -92,28 +97,6 @@ class ExtruderManager(QObject):
             if extruder.getId() == extruder_stack_id:
             if extruder.getId() == extruder_stack_id:
                 return extruder.qualityChanges.getId()
                 return extruder.qualityChanges.getId()
 
 
-    ##  The instance of the singleton pattern.
-    #
-    #   It's None if the extruder manager hasn't been created yet.
-    __instance = None
-
-    @staticmethod
-    def createExtruderManager():
-        return ExtruderManager().getInstance()
-
-    ##  Gets an instance of the extruder manager, or creates one if no instance
-    #   exists yet.
-    #
-    #   This is an implementation of singleton. If an extruder manager already
-    #   exists, it is re-used.
-    #
-    #   \return The extruder manager.
-    @classmethod
-    def getInstance(cls) -> "ExtruderManager":
-        if not cls.__instance:
-            cls.__instance = ExtruderManager()
-        return cls.__instance
-
     ##  Changes the active extruder by index.
     ##  Changes the active extruder by index.
     #
     #
     #   \param index The index of the new active extruder.
     #   \param index The index of the new active extruder.
@@ -746,3 +729,9 @@ class ExtruderManager(QObject):
         resolved_value = global_stack.getProperty(key, "value", context = context)
         resolved_value = global_stack.getProperty(key, "value", context = context)
 
 
         return resolved_value
         return resolved_value
+
+    __instance = None
+
+    @classmethod
+    def getInstance(cls, *args, **kwargs) -> "ExtruderManager":
+        return cls.__instance

+ 103 - 0
cura/SingleInstance.py

@@ -0,0 +1,103 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+import json
+import os
+from typing import List, Optional
+
+from PyQt5.QtNetwork import QLocalServer, QLocalSocket
+
+from UM.Logger import Logger
+
+
+class SingleInstance:
+
+    def __init__(self, application, files_to_open: Optional[List[str]]):
+        self._application = application
+        self._files_to_open = files_to_open
+
+        self._single_instance_server = None
+
+    # Starts a client that checks for a single instance server and sends the files that need to opened if the server
+    # exists. Returns True if the single instance server is found, otherwise False.
+    def startClient(self) -> bool:
+        Logger.log("i", "Checking for the presence of an ready running Cura instance.")
+        single_instance_socket = QLocalSocket(self._application)
+        Logger.log("d", "Full single instance server name: %s", single_instance_socket.fullServerName())
+        single_instance_socket.connectToServer("ultimaker-cura")
+        single_instance_socket.waitForConnected(msecs = 3000)  # wait for 3 seconds
+
+        if single_instance_socket.state() != QLocalSocket.ConnectedState:
+            return False
+
+        # We only send the files that need to be opened.
+        if not self._files_to_open:
+            Logger.log("i", "No file need to be opened, do nothing.")
+            return True
+
+        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"))
+
+            for filename in self._files_to_open:
+                payload = {"command": "open", "filePath": os.path.abspath(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 True
+
+    def startServer(self) -> None:
+        self._single_instance_server = QLocalServer()
+        self._single_instance_server.newConnection.connect(self._onClientConnected)
+        self._single_instance_server.listen("ultimaker-cura")
+
+    def _onClientConnected(self):
+        Logger.log("i", "New connection recevied on our single-instance server")
+        connection = self._single_instance_server.nextPendingConnection()
+
+        if connection is not None:
+            connection.readyRead.connect(lambda c = connection: self.__readCommands(c))
+
+    def __readCommands(self, connection):
+        line = 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._application.callLater(lambda: self._application.deleteAll())
+
+                # Command: Load a model file
+                elif command == "open":
+                    self._application.callLater(lambda f = payload["filePath"]: self._application._openFile(f))
+
+                # 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._application.callLater(lambda: self._application.getMainWindow().alert(0))
+
+                # Command: Close the socket connection. We're done.
+                elif command == "close-connection":
+                    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 '%s': %s", line, repr(ex))
+            line = connection.readLine()

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