diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 98e087707a..f249c3513d 100755 --- a/cura/BuildVolume.py +++ b/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,16 +20,14 @@ 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 + +from cura.Scene.CuraSceneNode import CuraSceneNode +from cura.Settings.ExtruderManager import ExtruderManager + catalog = i18nCatalog("cura") -import numpy -import math - -from typing import List, Optional - # 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() diff --git a/cura/CuraActions.py b/cura/CuraActions.py index 75338f17b6..019893957f 100644 --- a/cura/CuraActions.py +++ b/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 diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index d490b1cfbf..47b2dd25bf 100755 --- a/cura/CuraApplication.py +++ b/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") - ## Initialise the version upgrade manager with Cura's storage paths. - # Needs to be here to prevent circular dependencies. - import UM.VersionUpgradeManager + Resources.addType(self.ResourceTypes.QmlFiles, "qml") + Resources.addType(self.ResourceTypes.Firmware, "firmware") - UM.VersionUpgradeManager.VersionUpgradeManager.getInstance().setCurrentVersions( + # 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 + + 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 + + # 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() diff --git a/cura/MachineActionManager.py b/cura/MachineActionManager.py index 27b08ba8a1..65eb33b54c 100644 --- a/cura/MachineActionManager.py +++ b/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 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 +from UM.Logger import Logger +from UM.PluginRegistry import PluginRegistry # So MachineAction can be added as plugin type +from UM.Settings.DefinitionContainer import DefinitionContainer + ## 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 diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 22f7ea5407..8c897230d5 100644 --- a/cura/PrintInformation.py +++ b/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 math -import os.path -import unicodedata import json +import math +import os +import unicodedata 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.Logger import Logger -from UM.Qt.Duration import Duration -from UM.Preferences import Preferences -from UM.Scene.SceneNode import SceneNode from UM.i18n import i18nCatalog +from UM.Logger import Logger +from UM.Preferences import Preferences +from UM.Qt.Duration import Duration +from UM.Scene.SceneNode import SceneNode 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 diff --git a/cura/Scene/ConvexHullNode.py b/cura/Scene/ConvexHullNode.py index 1131958627..4c79c7d5dc 100644 --- a/cura/Scene/ConvexHullNode.py +++ b/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) diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 0dc26207df..ea2821ce25 100644 --- a/cura/Settings/ContainerManager.py +++ b/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 diff --git a/cura/Settings/CuraStackBuilder.py b/cura/Settings/CuraStackBuilder.py index 640489adb3..e401cad1ae 100644 --- a/cura/Settings/CuraStackBuilder.py +++ b/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 diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index c2fe929957..5fecb7aa17 100755 --- a/cura/Settings/ExtruderManager.py +++ b/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 diff --git a/cura/SingleInstance.py b/cura/SingleInstance.py new file mode 100644 index 0000000000..a664204d79 --- /dev/null +++ b/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() diff --git a/cura_app.py b/cura_app.py index 8a04b8fe09..18f9b6365f 100755 --- a/cura_app.py +++ b/cura_app.py @@ -1,51 +1,36 @@ #!/usr/bin/env python3 -# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import argparse +import faulthandler import os import sys from UM.Platform import Platform -parser = argparse.ArgumentParser(prog = "cura", - add_help = False) -parser.add_argument('--debug', - action='store_true', - default = False, - help = "Turn on the debug mode by setting this option." - ) -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." - ) -known_args = vars(parser.parse_known_args()[0]) -if not known_args["debug"]: - def get_cura_dir_path(): - if Platform.isWindows(): - return os.path.expanduser("~/AppData/Roaming/cura/") - elif Platform.isLinux(): - return os.path.expanduser("~/.local/share/cura") - elif Platform.isOSX(): - return os.path.expanduser("~/Library/Logs/cura") +# Gets the directory for stdout and stderr +def get_cura_dir_for_stdoutputs() -> str: + if Platform.isWindows(): + return os.path.expanduser("~/AppData/Roaming/cura/") + elif Platform.isLinux(): + return os.path.expanduser("~/.local/share/cura") + elif Platform.isOSX(): + return os.path.expanduser("~/Library/Logs/cura") - if hasattr(sys, "frozen"): - dirpath = get_cura_dir_path() - os.makedirs(dirpath, exist_ok = True) - sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w", encoding = "utf-8") - sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w", encoding = "utf-8") -import platform -import faulthandler +# Change stdout and stderr to files if Cura is running as a packaged application. +if hasattr(sys, "frozen"): + dir_path = get_cura_dir_for_stdoutputs() + os.makedirs(dir_path, exist_ok = True) + sys.stdout = open(os.path.join(dir_path, "stdout.log"), "w", encoding = "utf-8") + sys.stderr = open(os.path.join(dir_path, "stderr.log"), "w", encoding = "utf-8") -#WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612 + +# WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612 if Platform.isLinux(): # Needed for platform.linux_distribution, which is not available on Windows and OSX # For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 - linux_distro_name = platform.linux_distribution()[0].lower() # The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix. try: import ctypes @@ -79,6 +64,7 @@ if "PYTHONPATH" in os.environ.keys(): # If PYTHONPATH is u sys.path.remove(PATH_real) sys.path.insert(1, PATH_real) # Insert it at 1 after os.curdir, which is 0. + def exceptHook(hook_type, value, traceback): from cura.CrashHandler import CrashHandler from cura.CuraApplication import CuraApplication @@ -121,25 +107,25 @@ def exceptHook(hook_type, value, traceback): _crash_handler.early_crash_dialog.show() sys.exit(application.exec_()) -if not known_args["debug"]: - sys.excepthook = exceptHook + +# Set exception hook to use the crash dialog handler +sys.excepthook = exceptHook +# Enable dumping traceback for all threads +faulthandler.enable(all_threads = True) # Workaround for a race condition on certain systems where there # is a race condition between Arcus and PyQt. Importing Arcus # first seems to prevent Sip from going into a state where it # tries to create PyQt objects on a non-main thread. import Arcus #@UnusedImport -import cura.CuraApplication -import cura.Settings.CuraContainerRegistry +from cura.CuraApplication import CuraApplication -faulthandler.enable() +app = CuraApplication() +app.addCommandLineOptions() +app.parseCliOptions() +app.initialize() -# Force an instance of CuraContainerRegistry to be created and reused later. -cura.Settings.CuraContainerRegistry.CuraContainerRegistry.getInstance() +app.startSlashWindowPhase() +app.startPostSlashWindowPhase() -# This pre-start up check is needed to determine if we should start the application at all. -if not cura.CuraApplication.CuraApplication.preStartUp(parser = parser, parsed_command_line = known_args): - sys.exit(0) - -app = cura.CuraApplication.CuraApplication.getInstance(parser = parser, parsed_command_line = known_args) app.run() diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 654c1024bb..111b253dd8 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -275,7 +275,7 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(0) Logger.log("d", "Attempting to kill the engine process") - if Application.getInstance().getCommandLineOption("external-backend", False): + if Application.getInstance().getUseExternalBackend(): return if self._process is not None: diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index 6375c92879..5fc657e50f 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -1,10 +1,16 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from UM.Signal import Signal, signalemitter -from UM.Application import Application -from UM.Resources import Resources +import threading +import platform +import time +import serial.tools.list_ports + +from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal + from UM.Logger import Logger +from UM.Resources import Resources +from UM.Signal import Signal, signalemitter from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin from UM.i18n import i18nCatalog @@ -12,12 +18,6 @@ from cura.PrinterOutputDevice import ConnectionState from cura.CuraApplication import CuraApplication from . import USBPrinterOutputDevice -from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal - -import threading -import platform -import time -import serial.tools.list_ports i18n_catalog = i18nCatalog("cura") @@ -28,8 +28,14 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): addUSBOutputDeviceSignal = Signal() progressChanged = pyqtSignal() - def __init__(self, parent = None): + def __init__(self, application, parent = None): + if USBPrinterOutputDeviceManager.__instance is not None: + raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) + USBPrinterOutputDeviceManager.__instance = self + super().__init__(parent = parent) + self._application = application + self._serial_port_list = [] self._usb_output_devices = {} self._usb_output_devices_model = None @@ -38,11 +44,11 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): self._check_updates = True - Application.getInstance().applicationShuttingDown.connect(self.stop) + self._application.applicationShuttingDown.connect(self.stop) # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. self.addUSBOutputDeviceSignal.connect(self.addOutputDevice) - Application.getInstance().globalContainerStackChanged.connect(self.updateUSBPrinterOutputDevices) + self._application.globalContainerStackChanged.connect(self.updateUSBPrinterOutputDevices) # The method updates/reset the USB settings for all connected USB devices def updateUSBPrinterOutputDevices(self): @@ -69,7 +75,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): def _updateThread(self): while self._check_updates: - container_stack = Application.getInstance().getGlobalContainerStack() + container_stack = self._application.getGlobalContainerStack() if container_stack is None: time.sleep(5) continue @@ -80,19 +86,10 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): self._addRemovePorts(port_list) time.sleep(5) - ## Return the singleton instance of the USBPrinterManager - @classmethod - def getInstance(cls, engine = None, script_engine = None): - # Note: Explicit use of class name to prevent issues with inheritance. - if USBPrinterOutputDeviceManager._instance is None: - USBPrinterOutputDeviceManager._instance = cls() - - return USBPrinterOutputDeviceManager._instance - @pyqtSlot(result = str) def getDefaultFirmwareName(self): # Check if there is a valid global container stack - global_container_stack = Application.getInstance().getGlobalContainerStack() + global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: Logger.log("e", "There is no global container stack. Can not update firmware.") self._firmware_view.close() @@ -181,4 +178,8 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): return list(base_list) - _instance = None # type: "USBPrinterOutputDeviceManager" + __instance = None + + @classmethod + def getInstance(cls, *args, **kwargs) -> "USBPrinterOutputDeviceManager": + return cls.__instance diff --git a/plugins/USBPrinting/__init__.py b/plugins/USBPrinting/__init__.py index 7bf5853c10..fd5488eead 100644 --- a/plugins/USBPrinting/__init__.py +++ b/plugins/USBPrinting/__init__.py @@ -15,4 +15,4 @@ def register(app): # We are violating the QT API here (as we use a factory, which is technically not allowed). # but we don't really have another means for doing this (and it seems to you know -work-) qmlRegisterSingletonType(USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager, "Cura", 1, 0, "USBPrinterManager", USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance) - return {"output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance()} + return {"output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager(app)}