Просмотр исходного кода

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 лет назад
Родитель
Сommit
051dd7a6e9

+ 26 - 24
cura/BuildVolume.py

@@ -1,14 +1,16 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # 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.Scene.Platform import Platform
 from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
 from UM.Scene.SceneNode import SceneNode
-from UM.Application import Application
 from UM.Resources import Resources
 from UM.Mesh.MeshBuilder import MeshBuilder
 from UM.Math.Vector import Vector
@@ -18,15 +20,13 @@ from UM.Math.AxisAlignedBox import AxisAlignedBox
 from UM.Math.Polygon import Polygon
 from UM.Message import Message
 from UM.Signal import Signal
-from PyQt5.QtCore import QTimer
 from UM.View.RenderBatch import RenderBatch
 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
 PRIME_CLEARANCE = 6.5
@@ -36,8 +36,10 @@ PRIME_CLEARANCE = 6.5
 class BuildVolume(SceneNode):
     raftThicknessChanged = Signal()
 
-    def __init__(self, parent = None):
+    def __init__(self, application, parent = None):
         super().__init__(parent)
+        self._application = application
+        self._machine_manager = self._application.getMachineManager()
 
         self._volume_outline_color = None
         self._x_axis_color = None
@@ -80,14 +82,14 @@ class BuildVolume(SceneNode):
             " with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
 
         self._global_container_stack = None
-        Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged)
+        self._application.globalContainerStackChanged.connect(self._onStackChanged)
         self._onStackChanged()
 
         self._engine_ready = False
-        Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
+        self._application.engineCreatedSignal.connect(self._onEngineCreated)
 
         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.
         self._scene_objects = set()
@@ -105,14 +107,14 @@ class BuildVolume(SceneNode):
         # Must be after setting _build_volume_message, apparently that is used in getMachineManager.
         # activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
         # 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,
         # 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
-        Application.getInstance().getMachineManager().extruderChanged.connect(self.updateNodeBoundaryCheck)
+        self._machine_manager.extruderChanged.connect(self.updateNodeBoundaryCheck)
 
         # list of settings which were updated
         self._changed_settings_since_last_rebuild = []
@@ -122,7 +124,7 @@ class BuildVolume(SceneNode):
             self._scene_change_timer.start()
 
     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"))
         if new_scene_objects != self._scene_objects:
             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:
             self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.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_gridColor0", Color(*theme.getColor("buildplate_grid").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
     #
     def updateNodeBoundaryCheck(self):
-        root = Application.getInstance().getController().getScene().getRoot()
+        root = self._application.getController().getScene().getRoot()
         nodes = list(BreadthFirstIterator(root))
         group_nodes = []
 
@@ -289,11 +291,11 @@ class BuildVolume(SceneNode):
         if not self._width or not self._height or not self._depth:
             return
 
-        if not Application.getInstance()._engine:
+        if not self._application._qml_engine:
             return
 
         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._x_axis_color = Color(*theme.getColor("x_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)
         )
 
-        Application.getInstance().getController().getScene()._maximum_bounds = scale_to_max_bounds
+        self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
 
         self.updateNodeBoundaryCheck()
 
@@ -518,7 +520,7 @@ class BuildVolume(SceneNode):
             for extruder in extruders:
                 extruder.propertyChanged.disconnect(self._onSettingPropertyChanged)
 
-        self._global_container_stack = Application.getInstance().getGlobalContainerStack()
+        self._global_container_stack = self._application.getGlobalContainerStack()
 
         if self._global_container_stack:
             self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
@@ -561,7 +563,7 @@ class BuildVolume(SceneNode):
 
             if setting_key == "print_sequence":
                 machine_height = self._global_container_stack.getProperty("machine_height", "value")
-                if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" 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)
                     if self._height < machine_height:
                         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.Operations.GroupedOperation import GroupedOperation
 from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
-from UM.Operations.SetTransformOperation import SetTransformOperation
 from UM.Operations.TranslateOperation import TranslateOperation
 
 from cura.Operations.SetParentOperation import SetParentOperation

+ 247 - 280
cura/CuraApplication.py

@@ -1,9 +1,19 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # 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 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.Scene.SceneNode import SceneNode
@@ -74,6 +84,7 @@ from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
 
 from cura.Machines.VariantManager import VariantManager
 
+from .SingleInstance import SingleInstance
 from . import PlatformPhysics
 from . import BuildVolume
 from . import CameraAnimation
@@ -93,22 +104,10 @@ from cura.Settings.ContainerManager import ContainerManager
 
 from cura.ObjectsModel import ObjectsModel
 
-from PyQt5.QtCore import QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
 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
 if not MYPY:
@@ -143,19 +142,164 @@ class CuraApplication(QtApplication):
 
     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._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
         for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants"]:
             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"):
             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
         SettingDefinition.addSupportedProperty("settable_per_mesh", 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("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.QualityChangesInstanceContainer, "quality_changes")
         Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
@@ -191,20 +336,64 @@ class CuraApplication(QtApplication):
         Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
         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"),
                 ("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")))
 
@@ -289,23 +441,6 @@ class CuraApplication(QtApplication):
             "SelectionTool",
             "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._update_platform_activity_timer = QTimer()
@@ -318,53 +453,10 @@ class CuraApplication(QtApplication):
         self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
         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..."))
 
-        # 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
         preferences = Preferences.getInstance()
@@ -411,13 +503,10 @@ class CuraApplication(QtApplication):
 
         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
 
     def _onEngineCreated(self):
-        self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
+        self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
 
     @pyqtProperty(bool)
     def needToShowUserAgreement(self):
@@ -515,10 +604,6 @@ class CuraApplication(QtApplication):
     def setDefaultPath(self, 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 PluginRegistry
     def _loadPlugins(self):
@@ -543,127 +628,8 @@ class CuraApplication(QtApplication):
 
         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):
-        self.preRun()
-
-        container_registry = ContainerRegistry.getInstance()
+        container_registry = self._container_registry
 
         Logger.log("i", "Initializing variant manager")
         self._variant_manager = VariantManager(container_registry)
@@ -682,20 +648,25 @@ class CuraApplication(QtApplication):
         Logger.log("i", "Initializing machine manager")
         self._machine_manager = MachineManager(self)
 
+        Logger.log("i", "Initializing container manager")
+        self._container_manager = ContainerManager(self)
+
         Logger.log("i", "Initializing machine error checker")
         self._machine_error_checker = MachineErrorChecker(self)
         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
         root = self.getController().getScene().getRoot()
-        self._volume = BuildVolume.BuildVolume(self.getController().getScene().getRoot())
+        self._volume = BuildVolume.BuildVolume(self, root)
         Arrange.build_volume = self._volume
 
         # initialize info objects
-        self._print_information = PrintInformation.PrintInformation()
+        self._print_information = PrintInformation.PrintInformation(self)
         self._cura_actions = CuraActions.CuraActions(self)
 
         # Initialize setting visibility presets model
@@ -704,7 +675,7 @@ class CuraApplication(QtApplication):
         Preferences.getInstance().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
 
         # Detect in which mode to run and execute that mode
-        if self.getCommandLineOption("headless", False):
+        if self._is_headless:
             self.runWithoutGUI()
         else:
             self.runWithGUI()
@@ -713,7 +684,6 @@ class CuraApplication(QtApplication):
         self.initializationFinished.emit()
         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
         # initialized, for example opening files because they may show dialogs which can be closed due to incomplete
         # GUI initialization.
@@ -725,8 +695,12 @@ class CuraApplication(QtApplication):
 
         self.exec_()
 
+    def __setUpSingleInstanceServer(self):
+        if self._use_single_instance:
+            self._single_instance.startServer()
+
     def _onPostStart(self):
-        for file_name in self.getCommandLineOption("file", []):
+        for file_name in self._files_to_open:
             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.
             self.callLater(self._openFile, file_name)
@@ -735,13 +709,10 @@ class CuraApplication(QtApplication):
 
     ##  Run Cura without GUI elements and interaction (server mode).
     def runWithoutGUI(self):
-        self._use_gui = False
         self.closeSplash()
 
     ##  Run Cura with GUI (desktop mode).
     def runWithGUI(self):
-        self._use_gui = True
-
         self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
 
         controller = self.getController()
@@ -791,9 +762,6 @@ class CuraApplication(QtApplication):
         # Hide the splash screen
         self.closeSplash()
 
-    def hasGui(self):
-        return self._use_gui
-
     @pyqtSlot(result = QObject)
     def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
         return self._setting_visibility_presets_model
@@ -808,7 +776,7 @@ class CuraApplication(QtApplication):
 
     def getExtruderManager(self, *args):
         if self._extruder_manager is None:
-            self._extruder_manager = ExtruderManager.createExtruderManager()
+            self._extruder_manager = ExtruderManager()
         return self._extruder_manager
 
     @pyqtSlot(result = QObject)
@@ -932,7 +900,7 @@ class CuraApplication(QtApplication):
         qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
         qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
         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.
         actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
@@ -1520,8 +1488,7 @@ class CuraApplication(QtApplication):
                 # see GroupDecorator._onChildrenChanged
 
     def _createSplashScreen(self):
-        run_headless = self.getCommandLineOption("headless", False)
-        if run_headless:
+        if self._is_headless:
             return None
         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.
+
+from PyQt5.QtCore import QObject
+
+from UM.FlameProfiler import pyqtSlot
 from UM.Logger import Logger
 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 PyQt5.QtCore import QObject
-from UM.FlameProfiler import pyqtSlot
 
 ##  Raised when trying to add an unknown machine action as a required action
 class UnknownMachineActionError(Exception):
@@ -20,23 +20,27 @@ class NotUniqueMachineActionError(Exception):
 
 
 class MachineActionManager(QObject):
-    def __init__(self, parent = None):
+    def __init__(self, application, parent = None):
         super().__init__(parent)
+        self._application = application
 
         self._machine_actions = {}  # Dict of all known machine actions
         self._required_actions = {}  # Dict of all required 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
 
+    def initialize(self):
+        container_registry = self._application.getContainerRegistry()
+
         # Add machine_action as plugin type
         PluginRegistry.addType("machine_action", self.addMachineAction)
 
         # 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.
-        for container in ContainerRegistry.getInstance().findDefinitionContainers():
+        for container in container_registry.findDefinitionContainers():
             self._onContainerAdded(container)
 
-        ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
+        container_registry.containerAdded.connect(self._onContainerAdded)
 
     def _onContainerAdded(self, container):
         ## Ensure that the actions are added to this manager

+ 12 - 12
cura/PrintInformation.py

@@ -1,24 +1,24 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
-from typing import Dict
+import json
 import math
-import os.path
+import os
 import unicodedata
-import json
 import re  # To create abbreviations for printer names.
+from typing import Dict
 
 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.Qt.Duration import Duration
 from UM.Preferences import Preferences
+from UM.Qt.Duration import Duration
 from UM.Scene.SceneNode import SceneNode
-from UM.i18n import i18nCatalog
 
 catalog = i18nCatalog("cura")
 
+
 ##  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
@@ -47,8 +47,9 @@ class PrintInformation(QObject):
         ActiveMachineChanged = 3
         Other = 4
 
-    def __init__(self, parent = None):
+    def __init__(self, application, parent = None):
         super().__init__(parent)
+        self._application = application
 
         self.initializeCuraMessagePrintTimeProperties()
 
@@ -59,10 +60,10 @@ class PrintInformation(QObject):
 
         self._pre_sliced = False
 
-        self._backend = Application.getInstance().getBackend()
+        self._backend = self._application.getBackend()
         if self._backend:
             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._abbr_machine = ""
@@ -71,7 +72,6 @@ class PrintInformation(QObject):
         self._active_build_plate = 0
         self._initVariablesWithBuildPlate(self._active_build_plate)
 
-        self._application = Application.getInstance()
         self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
 
         self._application.globalContainerStackChanged.connect(self._updateJobName)
@@ -199,7 +199,7 @@ class PrintInformation(QObject):
         self._current_print_time[build_plate_number].setDuration(total_estimated_time)
 
     def _calculateInformation(self, build_plate_number):
-        global_stack = Application.getInstance().getGlobalContainerStack()
+        global_stack = self._application.getGlobalContainerStack()
         if global_stack is None:
             return
 
@@ -358,7 +358,7 @@ class PrintInformation(QObject):
     ##  Created an acronymn-like abbreviated machine name from the currently active machine name
     #   Called each time the global stack is switched
     def _setAbbreviatedMachineName(self):
-        global_container_stack = Application.getInstance().getGlobalContainerStack()
+        global_container_stack = self._application.getGlobalContainerStack()
         if not global_container_stack:
             self._abbr_machine = ""
             return

+ 1 - 1
cura/Scene/ConvexHullNode.py

@@ -24,7 +24,7 @@ class ConvexHullNode(SceneNode):
         self._original_parent = parent
 
         # 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())
         else:
             self._color = Color(0, 0, 0)

+ 33 - 39
cura/Settings/ContainerManager.py

@@ -1,32 +1,25 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
-import os.path
+import os
 import urllib.parse
 import uuid
 from typing import Dict, Union
 
 from PyQt5.QtCore import QObject, QUrl, QVariant
-from UM.FlameProfiler import pyqtSlot
 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.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.DefinitionContainer import DefinitionContainer
 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")
 
 
@@ -36,11 +29,17 @@ catalog = i18nCatalog("cura")
 #   from within QML. We want to be able to trigger things like removing a container
 #   when a certain action happens. This can be done through this class.
 class ContainerManager(QObject):
-    def __init__(self, parent = None):
-        super().__init__(parent)
 
-        self._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._material_manager = self._application.getMaterialManager()
         self._container_name_filters = {}
@@ -129,7 +128,7 @@ class ContainerManager(QObject):
         container.setProperty(setting_key, property_name, property_value)
 
         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:
                 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.
     @pyqtSlot(result = bool)
     def updateQualityChanges(self):
-        global_stack = Application.getInstance().getGlobalContainerStack()
+        global_stack = self._machine_manager.activeMachine
         if not global_stack:
             return False
 
         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.
             quality_changes = stack.qualityChanges
             if not quality_changes or self._container_registry.isReadOnly(quality_changes.getId()):
@@ -334,13 +335,15 @@ class ContainerManager(QObject):
         send_emits_containers = []
 
         # 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.clear()
             send_emits_containers.append(container)
 
         # 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:
             container.sendPostponedEmits()
@@ -381,21 +384,6 @@ class ContainerManager(QObject):
         if container is not None:
             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):
         if merge == merge_into:
             return
@@ -415,7 +403,7 @@ class ContainerManager(QObject):
 
             serialize_type = ""
             try:
-                plugin_metadata = PluginRegistry.getInstance().getMetaData(plugin_id)
+                plugin_metadata = self._plugin_registry.getMetaData(plugin_id)
                 if plugin_metadata:
                     serialize_type = plugin_metadata["settings_container"]["type"]
                 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]
         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.Settings.Interfaces import DefinitionContainerInterface
 from UM.Settings.InstanceContainer import InstanceContainer
-from UM.Settings.ContainerRegistry import ContainerRegistry
 
 from cura.Machines.VariantManager import VariantType
 from .GlobalStack import GlobalStack
@@ -29,7 +28,7 @@ class CuraStackBuilder:
         variant_manager = application.getVariantManager()
         material_manager = application.getMaterialManager()
         quality_manager = application.getQualityManager()
-        registry = ContainerRegistry.getInstance()
+        registry = application.getContainerRegistry()
 
         definitions = registry.findDefinitionContainers(id = definition_id)
         if not definitions:
@@ -142,6 +141,7 @@ class CuraStackBuilder:
                             variant_container, material_container, quality_container, global_stack) -> ExtruderStack:
         from cura.CuraApplication import CuraApplication
         application = CuraApplication.getInstance()
+        registry = application.getContainerRegistry()
 
         stack = ExtruderStack(new_stack_id, parent = global_stack)
         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
         # properties. This makes the create operation more transactional, since any problems
         # setting properties will not result in incomplete containers being added.
-        ContainerRegistry.getInstance().addContainer(user_container)
+        registry.addContainer(user_container)
 
         return stack
 
@@ -178,6 +178,7 @@ class CuraStackBuilder:
                           variant_container, material_container, quality_container) -> GlobalStack:
         from cura.CuraApplication import CuraApplication
         application = CuraApplication.getInstance()
+        registry = application.getContainerRegistry()
 
         stack = GlobalStack(new_stack_id)
         stack.setDefinition(definition)
@@ -193,7 +194,7 @@ class CuraStackBuilder:
         stack.qualityChanges = application.empty_quality_changes_container
         stack.userChanges = user_container
 
-        ContainerRegistry.getInstance().addContainer(user_container)
+        registry.addContainer(user_container)
 
         return stack
 
@@ -201,8 +202,10 @@ class CuraStackBuilder:
     def createUserChangesContainer(cls, container_name: str, definition_id: str, stack_id: str,
                                    is_global_stack: bool) -> "InstanceContainer":
         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.setDefinition(definition_id)
@@ -217,15 +220,17 @@ class CuraStackBuilder:
     @classmethod
     def createDefinitionChangesContainer(cls, container_stack, container_name):
         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.setDefinition(container_stack.getBottom().getId())
         definition_changes_container.addMetaDataEntry("type", "definition_changes")
         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
 
         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.ContainerStack import ContainerStack
 from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
+
 from typing import Optional, List, TYPE_CHECKING, Union
 
 if TYPE_CHECKING:
@@ -29,6 +30,10 @@ class ExtruderManager(QObject):
 
     ##  Registers listeners and such to listen to changes to the extruders.
     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)
 
         self._application = Application.getInstance()
@@ -92,28 +97,6 @@ class ExtruderManager(QObject):
             if extruder.getId() == extruder_stack_id:
                 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.
     #
     #   \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)
 
         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()

Некоторые файлы не были показаны из-за большого количества измененных файлов