diff --git a/cura/AutoSave.py b/cura/AutoSave.py index 71e889a62b..1639868d6a 100644 --- a/cura/AutoSave.py +++ b/cura/AutoSave.py @@ -3,21 +3,20 @@ from PyQt5.QtCore import QTimer -from UM.Preferences import Preferences from UM.Logger import Logger class AutoSave: def __init__(self, application): self._application = application - Preferences.getInstance().preferenceChanged.connect(self._triggerTimer) + self._application.getPreferences().preferenceChanged.connect(self._triggerTimer) self._global_stack = None - Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10) + self._application.getPreferences().addPreference("cura/autosave_delay", 1000 * 10) self._change_timer = QTimer() - self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay")) + self._change_timer.setInterval(self._application.getPreferences().getValue("cura/autosave_delay")) self._change_timer.setSingleShot(True) self._saving = False diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 85a6acfb90..d0563a5352 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -3,12 +3,10 @@ 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 @@ -37,8 +35,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 @@ -82,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() @@ -107,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 = [] @@ -124,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. @@ -186,7 +186,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())) @@ -206,7 +206,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 = [] @@ -294,11 +294,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()) @@ -470,7 +470,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() @@ -523,7 +523,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) @@ -566,7 +566,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 92c0e8ae1c..8544438f3a 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 34b6b5cde1..085e3b9b47 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1,9 +1,18 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import QObject, QTimer -from PyQt5.QtNetwork import QLocalServer -from PyQt5.QtNetwork import QLocalSocket +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.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 +83,7 @@ from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager from cura.Machines.VariantManager import VariantManager +from .SingleInstance import SingleInstance from .AutoSave import AutoSave from . import PlatformPhysics from . import BuildVolume @@ -94,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: @@ -144,20 +142,166 @@ 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() + + # 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 + + # Backups + self._auto_save = None + self._save_data_enabled = 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() + + self.__sendCommandToSingleInstance() + self.__addExpectedResourceDirsAndSearchPaths() + self.__initializeSettingDefinitionsAndFunctions() + self.__addAllResourcesAndContainerResources() + self.__addAllEmptyContainers() + self.__setLatestResouceVersionsForVersionUpgrade() + + # 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._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"): resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources") Resources.addSearchPath(resource_path) - 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) @@ -182,7 +326,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") @@ -193,20 +338,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", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"), ("quality_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityChangesInstanceContainer, "application/x-uranium-instancecontainer"), @@ -219,48 +408,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._auto_save = None - self._save_data_enabled = True - - 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 startSplashWindowPhase(self): + super().startSplashWindowPhase() self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png"))) @@ -294,23 +444,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() @@ -323,56 +456,13 @@ 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() + preferences = self.getPreferences() preferences.addPreference("metadata/setting_version", 0) preferences.setValue("metadata/setting_version", self.SettingVersion) #Don't make it equal to the default so that the setting version always gets written to the file. @@ -398,7 +488,7 @@ class CuraApplication(QtApplication): preferences.addPreference("view/filter_current_build_plate", False) preferences.addPreference("cura/sidebar_collapsed", False) - self._need_to_show_user_agreement = not Preferences.getInstance().getValue("general/accepted_user_agreement") + self._need_to_show_user_agreement = not self.getPreferences().getValue("general/accepted_user_agreement") for key in [ "dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin @@ -417,13 +507,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): @@ -463,14 +550,14 @@ class CuraApplication(QtApplication): def discardOrKeepProfileChanges(self): has_user_interaction = False - choice = Preferences.getInstance().getValue("cura/choice_on_profile_override") + choice = self.getPreferences().getValue("cura/choice_on_profile_override") if choice == "always_discard": # don't show dialog and DISCARD the profile self.discardOrKeepProfileChangesClosed("discard") elif choice == "always_keep": # don't show dialog and KEEP the profile self.discardOrKeepProfileChangesClosed("keep") - elif self._use_gui: + elif not self._is_headless: # ALWAYS ask whether to keep or discard the profile self.showDiscardOrKeepProfileChanges.emit() has_user_interaction = True @@ -509,24 +596,19 @@ class CuraApplication(QtApplication): # Do not do saving during application start or when data should not be safed on quit. return ContainerRegistry.getInstance().saveDirtyContainers() - Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences, - self._application_name + ".cfg")) + self.savePreferences() def saveStack(self, stack): ContainerRegistry.getInstance().saveContainer(stack) @pyqtSlot(str, result = QUrl) def getDefaultPath(self, key): - default_path = Preferences.getInstance().getValue("local_file/%s" % key) + default_path = self.getPreferences().getValue("local_file/%s" % key) return QUrl.fromLocalFile(default_path) @pyqtSlot(str, str) def setDefaultPath(self, key, default_path): - Preferences.getInstance().setValue("local_file/%s" % key, QUrl(default_path).toLocalFile()) - - @classmethod - def getStaticVersion(cls): - return CuraVersion + self.getPreferences().setValue("local_file/%s" % key, QUrl(default_path).toLocalFile()) ## Handle loading of all plugin types (and the backend explicitly) # \sa PluginRegistry @@ -552,127 +634,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) @@ -691,29 +654,34 @@ 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 self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self) default_visibility_profile = self._setting_visibility_presets_model.getItem(0) - Preferences.getInstance().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"])) + self.getPreferences().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() @@ -722,7 +690,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. @@ -737,8 +704,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) @@ -747,13 +718,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() @@ -803,9 +771,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 @@ -820,7 +785,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) @@ -947,7 +912,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"))) @@ -981,7 +946,7 @@ class CuraApplication(QtApplication): # Default self.getController().setActiveTool("TranslateTool") - if Preferences.getInstance().getValue("view/center_on_select"): + if self.getPreferences().getValue("view/center_on_select"): self._center_after_select = True else: if self.getController().getActiveTool(): @@ -1328,15 +1293,15 @@ class CuraApplication(QtApplication): categories = list(set(categories)) categories.sort() joined = ";".join(categories) - if joined != Preferences.getInstance().getValue("cura/categories_expanded"): - Preferences.getInstance().setValue("cura/categories_expanded", joined) + if joined != self.getPreferences().getValue("cura/categories_expanded"): + self.getPreferences().setValue("cura/categories_expanded", joined) self.expandedCategoriesChanged.emit() expandedCategoriesChanged = pyqtSignal() @pyqtProperty("QStringList", notify = expandedCategoriesChanged) def expandedCategories(self): - return Preferences.getInstance().getValue("cura/categories_expanded").split(";") + return self.getPreferences().getValue("cura/categories_expanded").split(";") @pyqtSlot() def mergeSelected(self): @@ -1487,8 +1452,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() @@ -1607,8 +1571,8 @@ class CuraApplication(QtApplication): self.fileLoaded.emit(filename) arrange_objects_on_load = ( - not Preferences.getInstance().getValue("cura/use_multi_build_plate") or - not Preferences.getInstance().getValue("cura/not_arrange_objects_on_load")) + not self.getPreferences().getValue("cura/use_multi_build_plate") or + not self.getPreferences().getValue("cura/not_arrange_objects_on_load")) target_build_plate = self.getMultiBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1 root = self.getController().getScene().getRoot() diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index bc2e020037..52f31020e6 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -27,8 +27,8 @@ class CuraPackageManager(QObject): self._plugin_registry = self._application.getPluginRegistry() #JSON files that keep track of all installed packages. - self._user_package_management_file_path = None - self._bundled_package_management_file_path = None + self._user_package_management_file_path = None #type: str + self._bundled_package_management_file_path = None #type: str for search_path in Resources.getSearchPaths(): candidate_bundled_path = os.path.join(search_path, "bundled_packages.json") if os.path.exists(candidate_bundled_path): 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/Machines/Models/SettingVisibilityPresetsModel.py b/cura/Machines/Models/SettingVisibilityPresetsModel.py index 8880ac5ce1..3062e83889 100644 --- a/cura/Machines/Models/SettingVisibilityPresetsModel.py +++ b/cura/Machines/Models/SettingVisibilityPresetsModel.py @@ -8,9 +8,9 @@ from configparser import ConfigParser from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot +from UM.Application import Application from UM.Logger import Logger from UM.Qt.ListModel import ListModel -from UM.Preferences import Preferences from UM.Resources import Resources from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError @@ -33,7 +33,7 @@ class SettingVisibilityPresetsModel(ListModel): basic_item = self.items[1] basic_visibile_settings = ";".join(basic_item["settings"]) - self._preferences = Preferences.getInstance() + self._preferences = Application.getInstance().getPreferences() # Preference to store which preset is currently selected self._preferences.addPreference("cura/active_setting_visibility_preset", "basic") # Preference that stores the "custom" set so it can always be restored (even after a restart) diff --git a/cura/ObjectsModel.py b/cura/ObjectsModel.py index cfe4320e28..f3c703d424 100644 --- a/cura/ObjectsModel.py +++ b/cura/ObjectsModel.py @@ -8,7 +8,6 @@ from UM.Qt.ListModel import ListModel from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode from UM.Scene.Selection import Selection -from UM.Preferences import Preferences from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") @@ -20,7 +19,7 @@ class ObjectsModel(ListModel): super().__init__() Application.getInstance().getController().getScene().sceneChanged.connect(self._updateDelayed) - Preferences.getInstance().preferenceChanged.connect(self._updateDelayed) + Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed) self._update_timer = QTimer() self._update_timer.setInterval(100) @@ -38,7 +37,7 @@ class ObjectsModel(ListModel): def _update(self, *args): nodes = [] - filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate") + filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate") active_build_plate_number = self._build_plate_number group_nr = 1 for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index b22552d8c5..6b539a4574 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -8,7 +8,6 @@ from UM.Scene.SceneNode import SceneNode from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Math.Vector import Vector from UM.Scene.Selection import Selection -from UM.Preferences import Preferences from cura.Scene.ConvexHullDecorator import ConvexHullDecorator @@ -36,8 +35,8 @@ class PlatformPhysics: self._max_overlap_checks = 10 # How many times should we try to find a new spot per tick? self._minimum_gap = 2 # It is a minimum distance (in mm) between two models, applicable for small models - Preferences.getInstance().addPreference("physics/automatic_push_free", False) - Preferences.getInstance().addPreference("physics/automatic_drop_down", True) + Application.getInstance().getPreferences().addPreference("physics/automatic_push_free", False) + Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True) def _onSceneChanged(self, source): if not source.getMeshData(): @@ -71,7 +70,7 @@ class PlatformPhysics: # Move it downwards if bottom is above platform move_vector = Vector() - if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down + if Application.getInstance().getPreferences().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0 move_vector = move_vector.set(y = -bbox.bottom + z_offset) @@ -80,7 +79,7 @@ class PlatformPhysics: node.addDecorator(ConvexHullDecorator()) # only push away objects if this node is a printing mesh - if not node.callDecoration("isNonPrintingMesh") and Preferences.getInstance().getValue("physics/automatic_push_free"): + if not node.callDecoration("isNonPrintingMesh") and Application.getInstance().getPreferences().getValue("physics/automatic_push_free"): # Check for collisions between convex hulls for other_node in BreadthFirstIterator(root): # Ignore root, ourselves and anything that is not a normal SceneNode. diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 9239dec8b7..efe1b2795c 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -1,25 +1,25 @@ # 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.i18n import i18nCatalog 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.MimeTypeDatabase import MimeTypeDatabase 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 @@ -48,8 +48,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() @@ -60,10 +61,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._is_user_specified_job_name = False self._base_name = "" @@ -73,7 +74,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) @@ -82,7 +82,7 @@ class PrintInformation(QObject): self._application.workspaceLoaded.connect(self.setProjectName) self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged) - Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) + self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged) self._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged) self._onActiveMaterialsChanged() @@ -201,7 +201,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 @@ -210,7 +210,7 @@ class PrintInformation(QObject): self._material_costs[build_plate_number] = [] self._material_names[build_plate_number] = [] - material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings")) + material_preference_values = json.loads(self._application.getInstance().getPreferences().getValue("cura/material_settings")) extruder_stacks = global_stack.extruders for position, extruder_stack in extruder_stacks.items(): @@ -311,7 +311,7 @@ class PrintInformation(QObject): if not self._is_user_specified_job_name: if self._pre_sliced: self._job_name = catalog.i18nc("@label", "Pre-sliced file {0}", base_name) - elif Preferences.getInstance().getValue("cura/jobname_prefix"): + elif self._application.getInstance().getPreferences().getValue("cura/jobname_prefix"): # Don't add abbreviation if it already has the exact same abbreviation. if base_name.startswith(self._abbr_machine + "_"): self._job_name = base_name @@ -372,7 +372,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 51e7e81fef..76f98c5a8a 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. @@ -747,3 +730,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/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 5c87b6ffba..cfc264c42d 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -17,7 +17,6 @@ from UM.FlameProfiler import pyqtSlot from UM import Util from UM.Application import Application -from UM.Preferences import Preferences from UM.Logger import Logger from UM.Message import Message @@ -98,12 +97,12 @@ class MachineManager(QObject): ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeStackChanged) self.activeStackChanged.connect(self.activeStackValueChanged) - Preferences.getInstance().addPreference("cura/active_machine", "") + self._application.getPreferences().addPreference("cura/active_machine", "") self._global_event_keys = set() self._printer_output_devices = [] # type: List[PrinterOutputDevice] - Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) + self._application.getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) # There might already be some output devices by the time the signal is connected self._onOutputDevicesChanged() @@ -164,14 +163,14 @@ class MachineManager(QObject): rootMaterialChanged = pyqtSignal() def setInitialActiveMachine(self) -> None: - active_machine_id = Preferences.getInstance().getValue("cura/active_machine") + active_machine_id = self._application.getPreferences().getValue("cura/active_machine") if active_machine_id != "" and ContainerRegistry.getInstance().findContainerStacksMetadata(id = active_machine_id): # An active machine was saved, so restore it. self.setActiveMachine(active_machine_id) def _onOutputDevicesChanged(self) -> None: self._printer_output_devices = [] - for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices(): + for printer_output_device in self._application.getOutputDeviceManager().getOutputDevices(): if isinstance(printer_output_device, PrinterOutputDevice): self._printer_output_devices.append(printer_output_device) @@ -238,7 +237,7 @@ class MachineManager(QObject): extruder_stack.containersChanged.disconnect(self._onContainersChanged) # Update the local global container stack reference - self._global_container_stack = Application.getInstance().getGlobalContainerStack() + self._global_container_stack = self._application.getGlobalContainerStack() if self._global_container_stack: self.updateDefaultExtruder() self.updateNumberExtrudersEnabled() @@ -246,7 +245,7 @@ class MachineManager(QObject): # after switching the global stack we reconnect all the signals and set the variant and material references if self._global_container_stack: - Preferences.getInstance().setValue("cura/active_machine", self._global_container_stack.getId()) + self._application.getPreferences().setValue("cura/active_machine", self._global_container_stack.getId()) self._global_container_stack.nameChanged.connect(self._onMachineNameChanged) self._global_container_stack.containersChanged.connect(self._onContainersChanged) @@ -270,7 +269,7 @@ class MachineManager(QObject): if self._global_container_stack.getId() in self.machine_extruder_material_update_dict: for func in self.machine_extruder_material_update_dict[self._global_container_stack.getId()]: - Application.getInstance().callLater(func) + self._application.callLater(func) del self.machine_extruder_material_update_dict[self._global_container_stack.getId()] self.activeQualityGroupChanged.emit() @@ -364,7 +363,7 @@ class MachineManager(QObject): return # We're done here ExtruderManager.getInstance().setActiveExtruderIndex(0) # Switch to first extruder self._global_container_stack = global_stack - Application.getInstance().setGlobalContainerStack(global_stack) + self._application.setGlobalContainerStack(global_stack) ExtruderManager.getInstance()._globalContainerStackChanged() self._initMachineState(containers[0]) self._onGlobalContainerChanged() @@ -838,7 +837,7 @@ class MachineManager(QObject): ## Set the amount of extruders on the active machine (global stack) # \param extruder_count int the number of extruders to set def setActiveMachineExtruderCount(self, extruder_count: int) -> None: - extruder_manager = Application.getInstance().getExtruderManager() + extruder_manager = self._application.getExtruderManager() definition_changes_container = self._global_container_stack.definitionChanges if not self._global_container_stack or definition_changes_container == self._empty_definition_changes_container: @@ -855,7 +854,7 @@ class MachineManager(QObject): self.correctExtruderSettings() # Check to see if any objects are set to print with an extruder that will no longer exist - root_node = Application.getInstance().getController().getScene().getRoot() + root_node = self._application.getController().getScene().getRoot() for node in DepthFirstIterator(root_node): if node.getMeshData(): extruder_nr = node.callDecoration("getActiveExtruderPosition") @@ -888,7 +887,7 @@ class MachineManager(QObject): global_user_container.removeInstance(setting_key) # Signal that the global stack has changed - Application.getInstance().globalContainerStackChanged.emit() + self._application.globalContainerStackChanged.emit() self.forceUpdateAllSettings() @pyqtSlot(int, result = QObject) @@ -1120,7 +1119,7 @@ class MachineManager(QObject): def _setGlobalVariant(self, container_node): self._global_container_stack.variant = container_node.getContainer() if not self._global_container_stack.variant: - self._global_container_stack.variant = Application.getInstance().empty_variant_container + self._global_container_stack.variant = self._application.empty_variant_container def _setMaterial(self, position, container_node = None): if container_node and container_node.getContainer(): @@ -1370,7 +1369,7 @@ class MachineManager(QObject): self._setQualityGroup(quality_group) # See if we need to show the Discard or Keep changes screen - if not no_dialog and self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1: + if not no_dialog and self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1: self._application.discardOrKeepProfileChanges() @pyqtProperty(QObject, fset = setQualityGroup, notify = activeQualityGroupChanged) @@ -1384,7 +1383,7 @@ class MachineManager(QObject): self._setQualityChangesGroup(quality_changes_group) # See if we need to show the Discard or Keep changes screen - if not no_dialog and self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1: + if not no_dialog and self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1: self._application.discardOrKeepProfileChanges() @pyqtSlot() 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..1495a9e526 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.startSplashWindowPhase() +app.startPostSplashWindowPhase() -# 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/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 9eec09b202..2ecea82691 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -44,8 +44,8 @@ except ImportError: ## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes! class ThreeMFReader(MeshReader): - def __init__(self): - super().__init__() + def __init__(self, application): + super().__init__(application) self._supported_extensions = [".3mf"] self._root = None self._base_name = "" @@ -158,7 +158,7 @@ class ThreeMFReader(MeshReader): um_node.addDecorator(sliceable_decorator) return um_node - def read(self, file_name): + def _read(self, file_name): result = [] self._object_count = 0 # Used to name objects as there is no node name yet. # The base object of 3mf is a zipped archive. diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 5cd0ef5ced..8eecc3b5d9 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -456,7 +456,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): num_visible_settings = len(visible_settings_string.split(";")) active_mode = temp_preferences.getValue("cura/active_mode") if not active_mode: - active_mode = Preferences.getInstance().getValue("cura/active_mode") + active_mode = Application.getInstance().getPreferences().getValue("cura/active_mode") except KeyError: # If there is no preferences file, it's not a workspace, so notify user of failure. Logger.log("w", "File %s is not a valid workspace.", file_name) @@ -575,7 +575,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): temp_preferences.deserialize(serialized) # Copy a number of settings from the temp preferences to the global - global_preferences = Preferences.getInstance() + global_preferences = application.getInstance().getPreferences() visible_settings = temp_preferences.getValue("general/visible_settings") if visible_settings is None: diff --git a/plugins/3MFReader/__init__.py b/plugins/3MFReader/__init__.py index 9da54586a3..ddaf76e01e 100644 --- a/plugins/3MFReader/__init__.py +++ b/plugins/3MFReader/__init__.py @@ -42,7 +42,7 @@ def getMetaData() -> Dict: def register(app): if "3MFReader.ThreeMFReader" in sys.modules: - return {"mesh_reader": ThreeMFReader.ThreeMFReader(), + return {"mesh_reader": ThreeMFReader.ThreeMFReader(app), "workspace_reader": ThreeMFWorkspaceReader.ThreeMFWorkspaceReader()} else: return {} diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index e948f62337..33df0bfe90 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -51,7 +51,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): self._writeContainerToArchive(container, archive) # Write preferences to archive - original_preferences = Preferences.getInstance() #Copy only the preferences that we use to the workspace. + original_preferences = Application.getInstance().getPreferences() #Copy only the preferences that we use to the workspace. temp_preferences = Preferences() for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded"}: temp_preferences.addPreference(preference, None) diff --git a/plugins/ChangeLogPlugin/ChangeLog.py b/plugins/ChangeLogPlugin/ChangeLog.py index 030d854d3f..e93d5c4395 100644 --- a/plugins/ChangeLogPlugin/ChangeLog.py +++ b/plugins/ChangeLogPlugin/ChangeLog.py @@ -3,7 +3,6 @@ from UM.i18n import i18nCatalog from UM.Extension import Extension -from UM.Preferences import Preferences from UM.Application import Application from UM.PluginRegistry import PluginRegistry from UM.Version import Version @@ -29,7 +28,7 @@ class ChangeLog(Extension, QObject,): self._change_logs = None Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated) - Preferences.getInstance().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium + Application.getInstance().getPreferences().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog) def getChangeLogs(self): @@ -79,12 +78,12 @@ class ChangeLog(Extension, QObject,): if not self._current_app_version: return #We're on dev branch. - if Preferences.getInstance().getValue("general/latest_version_changelog_shown") == "master": + if Application.getInstance().getPreferences().getValue("general/latest_version_changelog_shown") == "master": latest_version_shown = Version("0.0.0") else: - latest_version_shown = Version(Preferences.getInstance().getValue("general/latest_version_changelog_shown")) + latest_version_shown = Version(Application.getInstance().getPreferences().getValue("general/latest_version_changelog_shown")) - Preferences.getInstance().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion()) + Application.getInstance().getPreferences().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion()) # Do not show the changelog when there is no global container stack # This implies we are running Cura for the first time. diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 654c1024bb..d298ff1aca 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -4,7 +4,6 @@ from UM.Backend.Backend import Backend, BackendState from UM.Application import Application from UM.Scene.SceneNode import SceneNode -from UM.Preferences import Preferences from UM.Signal import Signal from UM.Logger import Logger from UM.Message import Message @@ -72,7 +71,7 @@ class CuraEngineBackend(QObject, Backend): Logger.log("i", "Found CuraEngine at: %s", default_engine_location) default_engine_location = os.path.abspath(default_engine_location) - Preferences.getInstance().addPreference("backend/location", default_engine_location) + Application.getInstance().getPreferences().addPreference("backend/location", default_engine_location) # Workaround to disable layer view processing if layer view is not active. self._layer_view_active = False @@ -121,7 +120,7 @@ class CuraEngineBackend(QObject, Backend): self._slice_start_time = None self._is_disabled = False - Preferences.getInstance().addPreference("general/auto_slice", False) + Application.getInstance().getPreferences().addPreference("general/auto_slice", False) self._use_timer = False # When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired. @@ -131,7 +130,7 @@ class CuraEngineBackend(QObject, Backend): self._change_timer.setSingleShot(True) self._change_timer.setInterval(500) self.determineAutoSlicing() - Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) + Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged) self._application.initializationFinished.connect(self.initialize) @@ -170,7 +169,7 @@ class CuraEngineBackend(QObject, Backend): # \return list of commands and args / parameters. def getEngineCommand(self): json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json") - return [Preferences.getInstance().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""] + return [Application.getInstance().getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""] ## Emitted when we get a message containing print duration and material amount. # This also implies the slicing has finished. @@ -275,7 +274,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: @@ -408,7 +407,7 @@ class CuraEngineBackend(QObject, Backend): enable_timer = True self._is_disabled = False - if not Preferences.getInstance().getValue("general/auto_slice"): + if not Application.getInstance().getPreferences().getValue("general/auto_slice"): enable_timer = False for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("isBlockSlicing"): diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index cbc097bb33..3bd6d79198 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -6,7 +6,6 @@ import gc from UM.Job import Job from UM.Application import Application from UM.Mesh.MeshData import MeshData -from UM.Preferences import Preferences from UM.View.GL.OpenGLContext import OpenGLContext from UM.Message import Message @@ -199,7 +198,7 @@ class ProcessSlicedLayersJob(Job): material_color_map[0, :] = color # We have to scale the colors for compatibility mode - if OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")): + if OpenGLContext.isLegacyOpenGL() or bool(Application.getInstance().getPreferences().getValue("view/force_layer_view_compatibility_mode")): line_type_brightness = 0.5 # for compatibility mode else: line_type_brightness = 1.0 diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py index 23a040f2e2..982f9d0b24 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py @@ -5,8 +5,7 @@ from PyQt5.QtCore import QUrl from PyQt5.QtGui import QDesktopServices from UM.Extension import Extension -from UM.Preferences import Preferences -from UM.Logger import Logger +from UM.Application import Application from UM.i18n import i18nCatalog from cura.Settings.GlobalStack import GlobalStack @@ -27,12 +26,12 @@ class FirmwareUpdateChecker(Extension): # Initialize the Preference called `latest_checked_firmware` that stores the last version # checked for the UM3. In the future if we need to check other printers' firmware - Preferences.getInstance().addPreference("info/latest_checked_firmware", "") + Application.getInstance().getPreferences().addPreference("info/latest_checked_firmware", "") # Listen to a Signal that indicates a change in the list of printers, just if the user has enabled the # 'check for updates' option - Preferences.getInstance().addPreference("info/automatic_update_check", True) - if Preferences.getInstance().getValue("info/automatic_update_check"): + Application.getInstance().getPreferences().addPreference("info/automatic_update_check", True) + if Application.getInstance().getPreferences().getValue("info/automatic_update_check"): ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded) self._download_url = None diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py index ce4fb8c92f..eadacf2c02 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py @@ -1,7 +1,6 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from UM.Preferences import Preferences from UM.Application import Application from UM.Message import Message from UM.Logger import Logger @@ -51,11 +50,11 @@ class FirmwareUpdateCheckerJob(Job): current_version = reader(current_version_file).readline().rstrip() # If it is the first time the version is checked, the checked_version is '' - checked_version = Preferences.getInstance().getValue("info/latest_checked_firmware") + checked_version = Application.getInstance().getPreferences().getValue("info/latest_checked_firmware") # If the checked_version is '', it's because is the first time we check firmware and in this case # we will not show the notification, but we will store it for the next time - Preferences.getInstance().setValue("info/latest_checked_firmware", current_version) + Application.getInstance().getPreferences().setValue("info/latest_checked_firmware", current_version) Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s", machine_name, checked_version, current_version) # The first time we want to store the current version, the notification will not be shown, diff --git a/plugins/GCodeGzReader/GCodeGzReader.py b/plugins/GCodeGzReader/GCodeGzReader.py index 86c0af89a2..7a6a76d4a5 100644 --- a/plugins/GCodeGzReader/GCodeGzReader.py +++ b/plugins/GCodeGzReader/GCodeGzReader.py @@ -12,11 +12,11 @@ from UM.PluginRegistry import PluginRegistry # If you're zipping g-code, you might as well use gzip! class GCodeGzReader(MeshReader): - def __init__(self): - super().__init__() + def __init__(self, application): + super().__init__(application) self._supported_extensions = [".gcode.gz"] - def read(self, file_name): + def _read(self, file_name): with open(file_name, "rb") as file: file_data = file.read() uncompressed_gcode = gzip.decompress(file_data).decode("utf-8") diff --git a/plugins/GCodeGzReader/__init__.py b/plugins/GCodeGzReader/__init__.py index 6e720b1ed1..e6bae6615e 100644 --- a/plugins/GCodeGzReader/__init__.py +++ b/plugins/GCodeGzReader/__init__.py @@ -21,4 +21,4 @@ def getMetaData(): def register(app): app.addNonSliceableExtension(".gz") - return { "mesh_reader": GCodeGzReader.GCodeGzReader() } + return { "mesh_reader": GCodeGzReader.GCodeGzReader(app) } diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py index 9a043f4961..696b3b180b 100644 --- a/plugins/GCodeReader/FlavorParser.py +++ b/plugins/GCodeReader/FlavorParser.py @@ -10,7 +10,6 @@ from UM.Math.Vector import Vector from UM.Message import Message from cura.Scene.CuraSceneNode import CuraSceneNode from UM.i18n import i18nCatalog -from UM.Preferences import Preferences catalog = i18nCatalog("cura") @@ -47,7 +46,7 @@ class FlavorParser: self._current_layer_thickness = 0.2 # default self._filament_diameter = 2.85 # default - Preferences.getInstance().addPreference("gcodereader/show_caution", True) + Application.getInstance().getPreferences().addPreference("gcodereader/show_caution", True) def _clearValues(self) -> None: self._extruder_number = 0 @@ -462,7 +461,7 @@ class FlavorParser: Logger.log("d", "GCode loading finished") - if Preferences.getInstance().getValue("gcodereader/show_caution"): + if Application.getInstance().getPreferences().getValue("gcodereader/show_caution"): caution_message = Message(catalog.i18nc( "@info:generic", "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."), diff --git a/plugins/GCodeReader/GCodeReader.py b/plugins/GCodeReader/GCodeReader.py index 80a6bea98a..c51fc9c8a7 100755 --- a/plugins/GCodeReader/GCodeReader.py +++ b/plugins/GCodeReader/GCodeReader.py @@ -4,7 +4,7 @@ from UM.FileHandler.FileReader import FileReader from UM.Mesh.MeshReader import MeshReader from UM.i18n import i18nCatalog -from UM.Preferences import Preferences +from UM.Application import Application from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType catalog = i18nCatalog("cura") @@ -27,12 +27,12 @@ class GCodeReader(MeshReader): _flavor_readers_dict = {"RepRap" : RepRapFlavorParser.RepRapFlavorParser(), "Marlin" : MarlinFlavorParser.MarlinFlavorParser()} - def __init__(self): - super(GCodeReader, self).__init__() + def __init__(self, application): + super(GCodeReader, self).__init__(application) self._supported_extensions = [".gcode", ".g"] self._flavor_reader = None - Preferences.getInstance().addPreference("gcodereader/show_caution", True) + Application.getInstance().getPreferences().addPreference("gcodereader/show_caution", True) def preReadFromStream(self, stream, *args, **kwargs): for line in stream.split("\n"): @@ -57,7 +57,7 @@ class GCodeReader(MeshReader): def readFromStream(self, stream): return self._flavor_reader.processGCodeStream(stream) - def read(self, file_name): + def _read(self, file_name): with open(file_name, "r", encoding = "utf-8") as file: file_data = file.read() return self.readFromStream(file_data) diff --git a/plugins/GCodeReader/__init__.py b/plugins/GCodeReader/__init__.py index 999dd37a37..399de8a90e 100644 --- a/plugins/GCodeReader/__init__.py +++ b/plugins/GCodeReader/__init__.py @@ -23,4 +23,4 @@ def getMetaData(): def register(app): app.addNonSliceableExtension(".gcode") app.addNonSliceableExtension(".g") - return { "mesh_reader": GCodeReader.GCodeReader() } + return { "mesh_reader": GCodeReader.GCodeReader(app) } diff --git a/plugins/ImageReader/ImageReader.py b/plugins/ImageReader/ImageReader.py index 3a98abccf5..89f163cfca 100644 --- a/plugins/ImageReader/ImageReader.py +++ b/plugins/ImageReader/ImageReader.py @@ -17,8 +17,8 @@ from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode class ImageReader(MeshReader): - def __init__(self): - super(ImageReader, self).__init__() + def __init__(self, application): + super(ImageReader, self).__init__(application) self._supported_extensions = [".jpg", ".jpeg", ".bmp", ".gif", ".png"] self._ui = ImageReaderUI(self) @@ -44,7 +44,7 @@ class ImageReader(MeshReader): return MeshReader.PreReadResult.cancelled return MeshReader.PreReadResult.accepted - def read(self, file_name): + def _read(self, file_name): size = max(self._ui.getWidth(), self._ui.getDepth()) return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.image_color_invert) diff --git a/plugins/ImageReader/__init__.py b/plugins/ImageReader/__init__.py index ea6ff8cceb..f30fdb83b0 100644 --- a/plugins/ImageReader/__init__.py +++ b/plugins/ImageReader/__init__.py @@ -33,4 +33,4 @@ def getMetaData(): } def register(app): - return { "mesh_reader": ImageReader.ImageReader() } + return { "mesh_reader": ImageReader.ImageReader(app) } diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py index d2db5ff420..baa700165c 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py @@ -5,7 +5,6 @@ from UM.Tool import Tool from UM.Scene.Selection import Selection from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Application import Application -from UM.Preferences import Preferences from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator from cura.Settings.ExtruderManager import ExtruderManager from UM.Settings.SettingInstance import SettingInstance @@ -27,7 +26,7 @@ class PerObjectSettingsTool(Tool): Selection.selectionChanged.connect(self.propertyChanged) - Preferences.getInstance().preferenceChanged.connect(self._onPreferenceChanged) + Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferenceChanged) self._onPreferenceChanged("cura/active_mode") Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged) @@ -106,7 +105,7 @@ class PerObjectSettingsTool(Tool): def _onPreferenceChanged(self, preference): if preference == "cura/active_mode": - self._advanced_mode = Preferences.getInstance().getValue(preference) == 1 + self._advanced_mode = Application.getInstance().getPreferences().getValue(preference) == 1 self._updateEnabled() def _onGlobalContainerChanged(self): diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 85849efb2f..44643dbf1c 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -16,7 +16,6 @@ from UM.Mesh.MeshBuilder import MeshBuilder from UM.Message import Message from UM.Platform import Platform from UM.PluginRegistry import PluginRegistry -from UM.Preferences import Preferences from UM.Resources import Resources from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Selection import Selection @@ -81,30 +80,30 @@ class SimulationView(View): self._show_travel_moves = False self._nozzle_node = None - Preferences.getInstance().addPreference("view/top_layer_count", 5) - Preferences.getInstance().addPreference("view/only_show_top_layers", False) - Preferences.getInstance().addPreference("view/force_layer_view_compatibility_mode", False) + Application.getInstance().getPreferences().addPreference("view/top_layer_count", 5) + Application.getInstance().getPreferences().addPreference("view/only_show_top_layers", False) + Application.getInstance().getPreferences().addPreference("view/force_layer_view_compatibility_mode", False) - Preferences.getInstance().addPreference("layerview/layer_view_type", 0) - Preferences.getInstance().addPreference("layerview/extruder_opacities", "") + Application.getInstance().getPreferences().addPreference("layerview/layer_view_type", 0) + Application.getInstance().getPreferences().addPreference("layerview/extruder_opacities", "") - Preferences.getInstance().addPreference("layerview/show_travel_moves", False) - Preferences.getInstance().addPreference("layerview/show_helpers", True) - Preferences.getInstance().addPreference("layerview/show_skin", True) - Preferences.getInstance().addPreference("layerview/show_infill", True) + Application.getInstance().getPreferences().addPreference("layerview/show_travel_moves", False) + Application.getInstance().getPreferences().addPreference("layerview/show_helpers", True) + Application.getInstance().getPreferences().addPreference("layerview/show_skin", True) + Application.getInstance().getPreferences().addPreference("layerview/show_infill", True) - Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) + Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged) self._updateWithPreferences() - self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count")) - self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers")) + self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count")) + self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers")) self._compatibility_mode = self._evaluateCompatibilityMode() self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"), title = catalog.i18nc("@info:title", "Simulation View")) def _evaluateCompatibilityMode(self): - return OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")) + return OpenGLContext.isLegacyOpenGL() or bool(Application.getInstance().getPreferences().getValue("view/force_layer_view_compatibility_mode")) def _resetSettings(self): self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness @@ -543,23 +542,23 @@ class SimulationView(View): self._top_layers_job = None def _updateWithPreferences(self): - self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count")) - self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers")) + self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count")) + self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers")) self._compatibility_mode = self._evaluateCompatibilityMode() - self.setSimulationViewType(int(float(Preferences.getInstance().getValue("layerview/layer_view_type")))); + self.setSimulationViewType(int(float(Application.getInstance().getPreferences().getValue("layerview/layer_view_type")))); - for extruder_nr, extruder_opacity in enumerate(Preferences.getInstance().getValue("layerview/extruder_opacities").split("|")): + for extruder_nr, extruder_opacity in enumerate(Application.getInstance().getPreferences().getValue("layerview/extruder_opacities").split("|")): try: opacity = float(extruder_opacity) except ValueError: opacity = 1.0 self.setExtruderOpacity(extruder_nr, opacity) - self.setShowTravelMoves(bool(Preferences.getInstance().getValue("layerview/show_travel_moves"))) - self.setShowHelpers(bool(Preferences.getInstance().getValue("layerview/show_helpers"))) - self.setShowSkin(bool(Preferences.getInstance().getValue("layerview/show_skin"))) - self.setShowInfill(bool(Preferences.getInstance().getValue("layerview/show_infill"))) + self.setShowTravelMoves(bool(Application.getInstance().getPreferences().getValue("layerview/show_travel_moves"))) + self.setShowHelpers(bool(Application.getInstance().getPreferences().getValue("layerview/show_helpers"))) + self.setShowSkin(bool(Application.getInstance().getPreferences().getValue("layerview/show_skin"))) + self.setShowInfill(bool(Application.getInstance().getPreferences().getValue("layerview/show_infill"))) self._startUpdateTopLayers() self.preferencesChanged.emit() diff --git a/plugins/SliceInfoPlugin/SliceInfo.py b/plugins/SliceInfoPlugin/SliceInfo.py index b0525bdbfa..fe17af89eb 100755 --- a/plugins/SliceInfoPlugin/SliceInfo.py +++ b/plugins/SliceInfoPlugin/SliceInfo.py @@ -10,7 +10,6 @@ from PyQt5.QtCore import pyqtSlot, QObject from UM.Extension import Extension from UM.Application import Application -from UM.Preferences import Preferences from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Message import Message from UM.i18n import i18nCatalog @@ -34,13 +33,13 @@ class SliceInfo(QObject, Extension): QObject.__init__(self, parent) Extension.__init__(self) Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._onWriteStarted) - Preferences.getInstance().addPreference("info/send_slice_info", True) - Preferences.getInstance().addPreference("info/asked_send_slice_info", False) + Application.getInstance().getPreferences().addPreference("info/send_slice_info", True) + Application.getInstance().getPreferences().addPreference("info/asked_send_slice_info", False) self._more_info_dialog = None self._example_data_content = None - if not Preferences.getInstance().getValue("info/asked_send_slice_info"): + if not Application.getInstance().getPreferences().getValue("info/asked_send_slice_info"): self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymized usage statistics."), lifetime = 0, dismissable = False, @@ -63,7 +62,7 @@ class SliceInfo(QObject, Extension): ## Perform action based on user input. # Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it. def messageActionTriggered(self, message_id, action_id): - Preferences.getInstance().setValue("info/asked_send_slice_info", True) + Application.getInstance().getPreferences().setValue("info/asked_send_slice_info", True) if action_id == "MoreInfo": self.showMoreInfoDialog() self.send_slice_info_message.hide() @@ -89,11 +88,11 @@ class SliceInfo(QObject, Extension): @pyqtSlot(bool) def setSendSliceInfo(self, enabled: bool): - Preferences.getInstance().setValue("info/send_slice_info", enabled) + Application.getInstance().getPreferences().setValue("info/send_slice_info", enabled) def _onWriteStarted(self, output_device): try: - if not Preferences.getInstance().getValue("info/send_slice_info"): + if not Application.getInstance().getPreferences().getValue("info/send_slice_info"): Logger.log("d", "'info/send_slice_info' is turned off.") return # Do nothing, user does not want to send data @@ -108,7 +107,7 @@ class SliceInfo(QObject, Extension): data["schema_version"] = 0 data["cura_version"] = application.getVersion() - active_mode = Preferences.getInstance().getValue("cura/active_mode") + active_mode = Application.getInstance().getPreferences().getValue("cura/active_mode") if active_mode == 0: data["active_mode"] = "recommended" else: @@ -123,7 +122,7 @@ class SliceInfo(QObject, Extension): machine_settings_changed_by_user = True data["machine_settings_changed_by_user"] = machine_settings_changed_by_user - data["language"] = Preferences.getInstance().getValue("general/language") + data["language"] = Application.getInstance().getPreferences().getValue("general/language") data["os"] = {"type": platform.system(), "version": platform.version()} data["active_machine"] = {"definition_id": global_stack.definition.getId(), diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index 8892ddb138..b9ad5c8829 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -6,7 +6,6 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Selection import Selection from UM.Resources import Resources from UM.Application import Application -from UM.Preferences import Preferences from UM.View.RenderBatch import RenderBatch from UM.Settings.Validator import ValidatorState from UM.Math.Color import Color @@ -23,7 +22,7 @@ class SolidView(View): def __init__(self): super().__init__() - Preferences.getInstance().addPreference("view/show_overhang", True) + Application.getInstance().getPreferences().addPreference("view/show_overhang", True) self._enabled_shader = None self._disabled_shader = None @@ -65,7 +64,7 @@ class SolidView(View): support_extruder_nr = global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr") support_angle_stack = Application.getInstance().getExtruderManager().getExtruderStack(support_extruder_nr) - if support_angle_stack is not None and Preferences.getInstance().getValue("view/show_overhang"): + if support_angle_stack is not None and Application.getInstance().getPreferences().getValue("view/show_overhang"): angle = support_angle_stack.getProperty("support_angle", "value") # Make sure the overhang angle is valid before passing it to the shader # Note: if the overhang angle is set to its default value, it does not need to get validated (validationState = None) diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index bf5ac96197..57b1e23c68 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -5,7 +5,6 @@ from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin from UM.Logger import Logger from UM.Application import Application from UM.Signal import Signal, signalemitter -from UM.Preferences import Preferences from UM.Version import Version from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice @@ -54,7 +53,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/" # Get list of manual instances from preferences - self._preferences = Preferences.getInstance() + self._preferences = Application.getInstance().getPreferences() self._preferences.addPreference("um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index f72afd9521..abf3b9ece2 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 @@ -81,19 +87,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() @@ -182,4 +179,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)} diff --git a/plugins/UserAgreement/UserAgreement.py b/plugins/UserAgreement/UserAgreement.py index 915c1761fa..4ea1ccf9bb 100644 --- a/plugins/UserAgreement/UserAgreement.py +++ b/plugins/UserAgreement/UserAgreement.py @@ -1,28 +1,26 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from UM.Extension import Extension -from UM.Preferences import Preferences -from UM.Application import Application -from UM.PluginRegistry import PluginRegistry -from UM.Logger import Logger - -from cura.CuraApplication import CuraApplication +import os from PyQt5.QtCore import QObject, pyqtSlot -import os.path +from UM.Extension import Extension +from UM.Logger import Logger + class UserAgreement(QObject, Extension): - def __init__(self): + def __init__(self, application): super(UserAgreement, self).__init__() + self._application = application self._user_agreement_window = None self._user_agreement_context = None - Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated) - Preferences.getInstance().addPreference("general/accepted_user_agreement", False) + self._application.engineCreatedSignal.connect(self._onEngineCreated) + + self._application.getPreferences().addPreference("general/accepted_user_agreement", False) def _onEngineCreated(self): - if not Preferences.getInstance().getValue("general/accepted_user_agreement"): + if not self._application.getPreferences().getValue("general/accepted_user_agreement"): self.showUserAgreement() def showUserAgreement(self): @@ -35,14 +33,14 @@ class UserAgreement(QObject, Extension): def didAgree(self, user_choice): if user_choice: Logger.log("i", "User agreed to the user agreement") - Preferences.getInstance().setValue("general/accepted_user_agreement", True) + self._application.getPreferences().setValue("general/accepted_user_agreement", True) self._user_agreement_window.hide() else: Logger.log("i", "User did NOT agree to the user agreement") - Preferences.getInstance().setValue("general/accepted_user_agreement", False) - CuraApplication.getInstance().quit() - CuraApplication.getInstance().setNeedToShowUserAgreement(False) + self._application.getPreferences().setValue("general/accepted_user_agreement", False) + self._application.quit() + self._application.setNeedToShowUserAgreement(False) def createUserAgreementWindow(self): - path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "UserAgreement.qml") - self._user_agreement_window = Application.getInstance().createQmlComponent(path, {"manager": self}) + path = os.path.join(self._application.getPluginRegistry().getPluginPath(self.getPluginId()), "UserAgreement.qml") + self._user_agreement_window = self._application.createQmlComponent(path, {"manager": self}) diff --git a/plugins/UserAgreement/__init__.py b/plugins/UserAgreement/__init__.py index 88cb151f9e..3cf81c64f4 100644 --- a/plugins/UserAgreement/__init__.py +++ b/plugins/UserAgreement/__init__.py @@ -7,4 +7,4 @@ def getMetaData(): return {} def register(app): - return {"extension": UserAgreement.UserAgreement()} + return {"extension": UserAgreement.UserAgreement(app)} diff --git a/plugins/X3DReader/X3DReader.py b/plugins/X3DReader/X3DReader.py index b0b9e00a5b..e57ec524db 100644 --- a/plugins/X3DReader/X3DReader.py +++ b/plugins/X3DReader/X3DReader.py @@ -38,14 +38,14 @@ class Shape: self.name = name class X3DReader(MeshReader): - def __init__(self): - super().__init__() + def __init__(self, application): + super().__init__(application) self._supported_extensions = [".x3d"] self._namespaces = {} # Main entry point # Reads the file, returns a SceneNode (possibly with nested ones), or None - def read(self, file_name): + def _read(self, file_name): try: self.defs = {} self.shapes = [] diff --git a/plugins/X3DReader/__init__.py b/plugins/X3DReader/__init__.py index 9e0e2df91c..03ed4ae301 100644 --- a/plugins/X3DReader/__init__.py +++ b/plugins/X3DReader/__init__.py @@ -16,4 +16,4 @@ def getMetaData(): } def register(app): - return { "mesh_reader": X3DReader.X3DReader() } + return { "mesh_reader": X3DReader.X3DReader(app) }