diff --git a/CMakeLists.txt b/CMakeLists.txt index 96efd68a2f..9e9bf4b538 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,10 @@ endif() set(CURA_VERSION "master" CACHE STRING "Version name of Cura") set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'") -set(CURA_PACKAGES_VERSION "" CACHE STRING "Packages version of Cura") +set(CURA_SDK_VERSION "" CACHE STRING "SDK version of Cura") +set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root") +set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version") + configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY) configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY) diff --git a/cura/Arranging/Arrange.py b/cura/Arranging/Arrange.py index 1027b39199..21ed45dbf1 100644 --- a/cura/Arranging/Arrange.py +++ b/cura/Arranging/Arrange.py @@ -3,6 +3,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Logger import Logger +from UM.Math.Polygon import Polygon from UM.Math.Vector import Vector from cura.Arranging.ShapeArray import ShapeArray from cura.Scene import ZOffsetDecorator @@ -45,7 +46,7 @@ class Arrange: # \param scene_root Root for finding all scene nodes # \param fixed_nodes Scene nodes to be placed @classmethod - def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250): + def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8): arranger = Arrange(x, y, x // 2, y // 2, scale = scale) arranger.centerFirst() @@ -58,9 +59,10 @@ class Arrange: # Place all objects fixed nodes for fixed_node in fixed_nodes: - vertices = fixed_node.callDecoration("getConvexHull") + vertices = fixed_node.callDecoration("getConvexHullHead") or fixed_node.callDecoration("getConvexHull") if not vertices: continue + vertices = vertices.getMinkowskiHull(Polygon.approximatedCircle(min_offset)) points = copy.deepcopy(vertices._points) shape_arr = ShapeArray.fromPolygon(points, scale = scale) arranger.place(0, 0, shape_arr) @@ -81,12 +83,12 @@ class Arrange: ## Find placement for a node (using offset shape) and place it (using hull shape) # return the nodes that should be placed # \param node - # \param offset_shape_arr ShapeArray with offset, used to find location - # \param hull_shape_arr ShapeArray without offset, for placing the shape + # \param offset_shape_arr ShapeArray with offset, for placing the shape + # \param hull_shape_arr ShapeArray without offset, used to find location def findNodePlacement(self, node, offset_shape_arr, hull_shape_arr, step = 1): new_node = copy.deepcopy(node) best_spot = self.bestSpot( - offset_shape_arr, start_prio = self._last_priority, step = step) + hull_shape_arr, start_prio = self._last_priority, step = step) x, y = best_spot.x, best_spot.y # Save the last priority. @@ -102,7 +104,7 @@ class Arrange: if x is not None: # We could find a place new_node.setPosition(Vector(x, center_y, y)) found_spot = True - self.place(x, y, hull_shape_arr) # place the object in arranger + self.place(x, y, offset_shape_arr) # place the object in arranger else: Logger.log("d", "Could not find spot!"), found_spot = False diff --git a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py index 252cef4e65..6abc553a4b 100644 --- a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py +++ b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py @@ -110,7 +110,7 @@ class ArrangeObjectsAllBuildPlatesJob(Job): arrange_array.add() arranger = arrange_array.get(current_build_plate_number) - best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority) + best_spot = arranger.bestSpot(hull_shape_arr, start_prio=start_priority) x, y = best_spot.x, best_spot.y node.removeDecorator(ZOffsetDecorator) if node.getBoundingBox(): @@ -118,7 +118,7 @@ class ArrangeObjectsAllBuildPlatesJob(Job): else: center_y = 0 if x is not None: # We could find a place - arranger.place(x, y, hull_shape_arr) # place the object in the arranger + arranger.place(x, y, offset_shape_arr) # place the object in the arranger node.callDecoration("setBuildPlateNumber", current_build_plate_number) grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True)) diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 01a91a3c22..5e982582fd 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -37,12 +37,15 @@ class ArrangeObjectsJob(Job): machine_width = global_container_stack.getProperty("machine_width", "value") machine_depth = global_container_stack.getProperty("machine_depth", "value") - arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes) + arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes, min_offset = self._min_offset) # Collect nodes to be placed nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) for node in self._nodes: offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset) + if offset_shape_arr is None: + Logger.log("w", "Node [%s] could not be converted to an array for arranging...", str(node)) + continue nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr)) # Sort the nodes with the biggest area first. @@ -63,7 +66,7 @@ class ArrangeObjectsJob(Job): start_priority = last_priority else: start_priority = 0 - best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority) + best_spot = arranger.bestSpot(hull_shape_arr, start_prio = start_priority) x, y = best_spot.x, best_spot.y node.removeDecorator(ZOffsetDecorator) if node.getBoundingBox(): @@ -74,7 +77,7 @@ class ArrangeObjectsJob(Job): last_size = size last_priority = best_spot.priority - arranger.place(x, y, hull_shape_arr) # take place before the next one + arranger.place(x, y, offset_shape_arr) # take place before the next one grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True)) else: Logger.log("d", "Arrange all: could not find spot!") 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 cef09750a0..e34d0a07ab 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,161 @@ 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._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: + self.__addExpectedResourceDirsAndSearchPaths() # Must be added before init of super + + super().initialize() + + self.__sendCommandToSingleInstance() + self.__initializeSettingDefinitionsAndFunctions() + self.__addAllResourcesAndContainerResources() + self.__addAllEmptyContainers() + self.__setLatestResouceVersionsForVersionUpgrade() + + self._machine_action_manager = MachineActionManager.MachineActionManager(self) + self._machine_action_manager.initialize() + + def __sendCommandToSingleInstance(self): + self._single_instance = SingleInstance(self, self._files_to_open) + + # If we use single instance, try to connect to the single instance server, send commands, and then exit. + # If we cannot find an existing single instance server, this is the only instance, so just keep going. + if self._use_single_instance: + if self._single_instance.startClient(): + Logger.log("i", "Single instance commands were sent, exiting") + sys.exit(0) + + # Adds expected directory names and search paths for Resources. + def __addExpectedResourceDirsAndSearchPaths(self): # this list of dir names will be used by UM to detect an old cura directory for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants"]: Resources.addExpectedDirNameInData(dir_name) - Resources.addSearchPath(os.path.join(QtApplication.getInstallPrefix(), "share", "cura", "resources")) + Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources")) if not hasattr(sys, "frozen"): 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) @@ -181,8 +320,10 @@ class CuraApplication(QtApplication): SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues) SettingFunction.registerOperator("extruderValue", ExtruderManager.getExtruderValue) SettingFunction.registerOperator("resolveOrValue", ExtruderManager.getResolveOrValue) + SettingFunction.registerOperator("defaultExtruderPosition", ExtruderManager.getDefaultExtruderPosition) - ## 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 +334,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,6 +404,7 @@ class CuraApplication(QtApplication): } ) + """ self._currently_loading_files = [] self._non_sliceable_extensions = [] @@ -254,6 +440,10 @@ class CuraApplication(QtApplication): 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"))) @@ -287,23 +477,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() @@ -316,56 +489,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. @@ -391,7 +521,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 @@ -410,13 +540,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): @@ -445,7 +572,7 @@ class CuraApplication(QtApplication): ## A reusable dialogbox # - showMessageBox = pyqtSignal(str, str, str, str, str, int, int, arguments = ["title", "footer", "text", "informativeText", "detailedText", "buttons", "icon"]) + showMessageBox = pyqtSignal(str, str, str, str, int, int, arguments = ["title", "text", "informativeText", "detailedText", "buttons", "icon"]) def messageBox(self, title, text, informativeText = "", detailedText = "", buttons = QMessageBox.Ok, icon = QMessageBox.NoIcon, callback = None, callback_arguments = []): self._message_box_callback = callback @@ -456,14 +583,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 @@ -502,24 +629,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 @@ -545,127 +667,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) @@ -684,29 +687,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() @@ -715,7 +723,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. @@ -730,8 +737,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) @@ -740,13 +751,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() @@ -796,9 +804,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 @@ -813,7 +818,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 def getVariantManager(self, *args): @@ -936,7 +941,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"))) @@ -970,7 +975,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(): @@ -1317,15 +1322,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): @@ -1476,8 +1481,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() @@ -1489,11 +1493,15 @@ class CuraApplication(QtApplication): def _reloadMeshFinished(self, job): # TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh! - mesh_data = job.getResult()[0].getMeshData() - if mesh_data: - job._node.setMeshData(mesh_data) - else: + job_result = job.getResult() + if len(job_result) == 0: + Logger.log("e", "Reloading the mesh failed.") + return + mesh_data = job_result[0].getMeshData() + if not mesh_data: Logger.log("w", "Could not find a mesh in reloaded node.") + return + job._node.setMeshData(mesh_data) def _openFile(self, filename): self.readLocalFile(QUrl.fromLocalFile(filename)) @@ -1557,10 +1565,11 @@ class CuraApplication(QtApplication): f = file.toLocalFile() extension = os.path.splitext(f)[1] + extension = extension.lower() filename = os.path.basename(f) if len(self._currently_loading_files) > 0: # If a non-slicable file is already being loaded, we prevent loading of any further non-slicable files - if extension.lower() in self._non_sliceable_extensions: + if extension in self._non_sliceable_extensions: message = Message( self._i18n_catalog.i18nc("@info:status", "Only one G-code file can be loaded at a time. Skipped importing {0}", @@ -1569,7 +1578,8 @@ class CuraApplication(QtApplication): return # If file being loaded is non-slicable file, then prevent loading of any other files extension = os.path.splitext(self._currently_loading_files[0])[1] - if extension.lower() in self._non_sliceable_extensions: + extension = extension.lower() + if extension in self._non_sliceable_extensions: message = Message( self._i18n_catalog.i18nc("@info:status", "Can't open any other file if G-code is loading. Skipped importing {0}", @@ -1592,8 +1602,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 0ce4799dad..49f095941a 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -11,4 +11,4 @@ class CuraPackageManager(PackageManager): super().__init__(parent) self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer) - self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer) \ No newline at end of file + self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer) diff --git a/cura/CuraVersion.py.in b/cura/CuraVersion.py.in index f45a24cae9..226b2183f2 100644 --- a/cura/CuraVersion.py.in +++ b/cura/CuraVersion.py.in @@ -4,4 +4,6 @@ CuraVersion = "@CURA_VERSION@" CuraBuildType = "@CURA_BUILDTYPE@" CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False -CuraPackagesVersion = "@CURA_PACKAGES_VERSION@" +CuraSDKVersion = "@CURA_SDK_VERSION@" +CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@" +CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@" 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/MaterialManager.py b/cura/Machines/MaterialManager.py index 8b74596667..ff666f392d 100644 --- a/cura/Machines/MaterialManager.py +++ b/cura/Machines/MaterialManager.py @@ -291,9 +291,10 @@ class MaterialManager(QObject): material_id_metadata_dict = dict() for node in nodes_to_check: if node is not None: + # Only exclude the materials that are explicitly specified in the "exclude_materials" field. + # Do not exclude other materials that are of the same type. for material_id, node in node.material_map.items(): - fallback_id = self.getFallbackMaterialIdByMaterialType(node.metadata["material"]) - if fallback_id in machine_exclude_materials: + if material_id in machine_exclude_materials: Logger.log("d", "Exclude material [%s] for machine [%s]", material_id, machine_definition.getId()) continue diff --git a/cura/Machines/Models/BaseMaterialsModel.py b/cura/Machines/Models/BaseMaterialsModel.py index 0a1337feeb..4759c8b5b0 100644 --- a/cura/Machines/Models/BaseMaterialsModel.py +++ b/cura/Machines/Models/BaseMaterialsModel.py @@ -39,6 +39,8 @@ class BaseMaterialsModel(ListModel): self._extruder_position = 0 self._extruder_stack = None + # Update the stack and the model data when the machine changes + self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack) def _updateExtruderStack(self): global_stack = self._machine_manager.activeMachine @@ -50,9 +52,11 @@ class BaseMaterialsModel(ListModel): self._extruder_stack = global_stack.extruders.get(str(self._extruder_position)) if self._extruder_stack is not None: self._extruder_stack.pyqtContainersChanged.connect(self._update) + # Force update the model when the extruder stack changes + self._update() def setExtruderPosition(self, position: int): - if self._extruder_position != position: + if self._extruder_stack is None or self._extruder_position != position: self._extruder_position = position self._updateExtruderStack() self.extruderPositionChanged.emit() 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/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index 46f7f56f8a..57db7734e7 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -1,6 +1,8 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import copy + from UM.Job import Job from UM.Operations.GroupedOperation import GroupedOperation from UM.Message import Message @@ -36,7 +38,7 @@ class MultiplyObjectsJob(Job): root = scene.getRoot() scale = 0.5 - arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale) + arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale, min_offset = self._min_offset) processed_nodes = [] nodes = [] @@ -64,6 +66,8 @@ class MultiplyObjectsJob(Job): # We do place the nodes one by one, as we want to yield in between. if not node_too_big: new_node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr) + else: + new_node = copy.deepcopy(node) if node_too_big or not solution_found: found_solution_for_all = False new_location = new_node.getPosition() 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 3260d55c74..1b8ba575db 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,11 +61,12 @@ 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 = "" self._abbr_machine = "" self._job_name = "" @@ -72,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) @@ -81,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() @@ -200,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 @@ -209,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(): @@ -281,10 +282,13 @@ class PrintInformation(QObject): # Manual override of job name should also set the base name so that when the printer prefix is updated, it the # prefix can be added to the manually added name, not the old base name - @pyqtSlot(str) - def setJobName(self, name): + @pyqtSlot(str, bool) + def setJobName(self, name, is_user_specified_job_name = False): + self._is_user_specified_job_name = is_user_specified_job_name self._job_name = name self._base_name = name.replace(self._abbr_machine + "_", "") + if name == "": + self._is_user_specified_job_name = False self.jobNameChanged.emit() jobNameChanged = pyqtSignal() @@ -295,22 +299,26 @@ class PrintInformation(QObject): def _updateJobName(self): if self._base_name == "": - self._job_name = "" + self._job_name = "unnamed" + self._is_user_specified_job_name = False self.jobNameChanged.emit() return base_name = self._stripAccents(self._base_name) self._setAbbreviatedMachineName() - if self._pre_sliced: - self._job_name = catalog.i18nc("@label", "Pre-sliced file {0}", base_name) - elif Preferences.getInstance().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 + + # Only update the job name when it's not user-specified. + 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 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 + else: + self._job_name = self._abbr_machine + "_" + base_name else: - self._job_name = self._abbr_machine + "_" + base_name - else: - self._job_name = base_name + self._job_name = base_name self.jobNameChanged.emit() @@ -321,6 +329,8 @@ class PrintInformation(QObject): baseNameChanged = pyqtSignal() def setBaseName(self, base_name: str, is_project_file: bool = False): + self._is_user_specified_job_name = False + # Ensure that we don't use entire path but only filename name = os.path.basename(base_name) @@ -341,17 +351,17 @@ class PrintInformation(QObject): if is_gcode or is_project_file or (is_empty or (self._base_name == "" and self._base_name != check_name)): # Only take the file name part, Note : file name might have 'dot' in name as well - data = '' + data = "" try: mime_type = MimeTypeDatabase.getMimeTypeForFile(name) data = mime_type.stripExtension(name) except: - Logger.log("w", "Unsupported Mime Type Database file extension") + Logger.log("w", "Unsupported Mime Type Database file extension %s", name) if data is not None and check_name is not None: self._base_name = data else: - self._base_name = '' + self._base_name = "" self._updateJobName() @@ -362,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/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py index 428a59f554..92f1d839fb 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from copy import deepcopy -from typing import List +from typing import List, Optional from UM.Application import Application from UM.Math.AxisAlignedBox import AxisAlignedBox @@ -13,9 +13,9 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator ## Scene nodes that are models are only seen when selecting the corresponding build plate # Note that many other nodes can just be UM SceneNode objects. class CuraSceneNode(SceneNode): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if "no_setting_override" not in kwargs: + def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False): + super().__init__(parent = parent, visible = visible, name = name) + if not no_setting_override: self.addDecorator(SettingOverrideDecorator()) # now we always have a getActiveExtruderPosition, unless explicitly disabled self._outside_buildarea = False 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/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 6d8ed7c037..f5036078be 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -475,7 +475,7 @@ class CuraContainerRegistry(ContainerRegistry): extruder_definition = extruder_definitions[0] unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id) if create_new_ids else machine.getName() + " " + new_extruder_id - extruder_stack = ExtruderStack.ExtruderStack(unique_name, parent = machine) + extruder_stack = ExtruderStack.ExtruderStack(unique_name) extruder_stack.setName(extruder_definition.getName()) extruder_stack.setDefinition(extruder_definition) extruder_stack.addMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) diff --git a/cura/Settings/CuraContainerStack.py b/cura/Settings/CuraContainerStack.py index 308a91bc76..e1f89eb725 100755 --- a/cura/Settings/CuraContainerStack.py +++ b/cura/Settings/CuraContainerStack.py @@ -39,8 +39,8 @@ from . import Exceptions # This also means that operations on the stack that modifies the container ordering is prohibited and # will raise an exception. class CuraContainerStack(ContainerStack): - def __init__(self, container_id: str, *args, **kwargs): - super().__init__(container_id, *args, **kwargs) + def __init__(self, container_id: str): + super().__init__(container_id) self._container_registry = ContainerRegistry.getInstance() diff --git a/cura/Settings/CuraStackBuilder.py b/cura/Settings/CuraStackBuilder.py index 640489adb3..85514006b5 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: @@ -99,8 +98,7 @@ class CuraStackBuilder: position = position, variant_container = extruder_variant_container, material_container = material_container, - quality_container = application.empty_quality_container, - global_stack = new_global_stack, + quality_container = application.empty_quality_container ) new_extruder.setNextStack(new_global_stack) new_global_stack.addExtruder(new_extruder) @@ -139,11 +137,12 @@ class CuraStackBuilder: @classmethod def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface, machine_definition_id: str, position: int, - variant_container, material_container, quality_container, global_stack) -> ExtruderStack: + variant_container, material_container, quality_container) -> ExtruderStack: from cura.CuraApplication import CuraApplication application = CuraApplication.getInstance() + registry = application.getContainerRegistry() - stack = ExtruderStack(new_stack_id, parent = global_stack) + stack = ExtruderStack(new_stack_id) stack.setName(extruder_definition.getName()) stack.setDefinition(extruder_definition) @@ -162,7 +161,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 +177,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 +193,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 +201,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 +219,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..0f8cb9ae23 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. @@ -557,6 +540,11 @@ class ExtruderManager(QObject): return result + ## Return the default extruder position from the machine manager + @staticmethod + def getDefaultExtruderPosition() -> str: + return Application.getInstance().getMachineManager().defaultExtruderPosition + ## Get all extruder values for a certain setting. # # This is exposed to qml for display purposes @@ -747,3 +735,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/ExtruderStack.py b/cura/Settings/ExtruderStack.py index 5e944b401f..b3f7d529a2 100644 --- a/cura/Settings/ExtruderStack.py +++ b/cura/Settings/ExtruderStack.py @@ -25,8 +25,8 @@ if TYPE_CHECKING: # # class ExtruderStack(CuraContainerStack): - def __init__(self, container_id: str, *args, **kwargs): - super().__init__(container_id, *args, **kwargs) + def __init__(self, container_id: str): + super().__init__(container_id) self.addMetaDataEntry("type", "extruder_train") # For backward compatibility diff --git a/cura/Settings/GlobalStack.py b/cura/Settings/GlobalStack.py index f76ac1ab3f..6d300954c2 100755 --- a/cura/Settings/GlobalStack.py +++ b/cura/Settings/GlobalStack.py @@ -23,8 +23,8 @@ from .CuraContainerStack import CuraContainerStack ## Represents the Global or Machine stack and its related containers. # class GlobalStack(CuraContainerStack): - def __init__(self, container_id: str, *args, **kwargs): - super().__init__(container_id, *args, **kwargs) + def __init__(self, container_id: str): + super().__init__(container_id) self.addMetaDataEntry("type", "machine") # For backward compatibility diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 78f462d8e9..20cc2b2eca 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() @@ -655,6 +654,15 @@ class MachineManager(QObject): return "" + @pyqtProperty(str, notify = activeVariantChanged) + def activeVariantId(self) -> str: + if self._active_container_stack: + variant = self._active_container_stack.variant + if variant: + return variant.getId() + + return "" + @pyqtProperty(str, notify = activeVariantChanged) def activeVariantBuildplateName(self) -> str: if self._global_container_stack: @@ -838,7 +846,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 +863,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 +896,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) @@ -982,6 +990,14 @@ class MachineManager(QObject): container = extruder.userChanges container.setProperty(setting_name, property_name, property_value) + ## Reset all setting properties of a setting for all extruders. + # \param setting_name The ID of the setting to reset. + @pyqtSlot(str) + def resetSettingForAllExtruders(self, setting_name: str) -> None: + for key, extruder in self._global_container_stack.extruders.items(): + container = extruder.userChanges + container.removeInstance(setting_name) + @pyqtProperty("QVariantList", notify = globalContainerChanged) def currentExtruderPositions(self) -> List[str]: if self._global_container_stack is None: @@ -1032,6 +1048,10 @@ class MachineManager(QObject): self.activeQualityChangesGroupChanged.emit() def _setQualityGroup(self, quality_group, empty_quality_changes: bool = True) -> None: + if quality_group is None: + self._setEmptyQuality() + return + if quality_group.node_for_global.getContainer() is None: return for node in quality_group.nodes_for_extruders.values(): @@ -1042,10 +1062,6 @@ class MachineManager(QObject): if empty_quality_changes: self._current_quality_changes_group = None - if quality_group is None: - self._setEmptyQuality() - return - # Set quality and quality_changes for the GlobalStack self._global_container_stack.quality = quality_group.node_for_global.getContainer() if empty_quality_changes: @@ -1120,7 +1136,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(): @@ -1280,6 +1296,10 @@ class MachineManager(QObject): self._global_container_stack.variant = self._empty_variant_container self._updateQualityWithMaterial() + # See if we need to show the Discard or Keep changes screen + if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1: + self._application.discardOrKeepProfileChanges() + ## Find all container stacks that has the pair 'key = value' in its metadata and replaces the value with 'new_value' def replaceContainersMetadata(self, key: str, value: str, new_value: str) -> None: machines = ContainerRegistry.getInstance().findContainerStacks(type = "machine") @@ -1331,6 +1351,10 @@ class MachineManager(QObject): self._setMaterial(position, container_node) self._updateQualityWithMaterial() + # See if we need to show the Discard or Keep changes screen + if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1: + self._application.discardOrKeepProfileChanges() + @pyqtSlot(str, str) def setVariantByName(self, position: str, variant_name: str) -> None: machine_definition_id = self._global_container_stack.definition.id @@ -1346,6 +1370,10 @@ class MachineManager(QObject): self._updateMaterialWithVariant(position) self._updateQualityWithMaterial() + # See if we need to show the Discard or Keep changes screen + if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1: + self._application.discardOrKeepProfileChanges() + @pyqtSlot(str) def setQualityGroupByQualityType(self, quality_type: str) -> None: if self._global_container_stack is None: @@ -1362,7 +1390,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) @@ -1376,7 +1404,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..2b823a25c9 100755 --- a/cura_app.py +++ b/cura_app.py @@ -1,9 +1,10 @@ #!/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 @@ -27,7 +28,7 @@ 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/") + return os.path.expanduser("~/AppData/Roaming/cura") elif Platform.isLinux(): return os.path.expanduser("~/.local/share/cura") elif Platform.isOSX(): @@ -39,13 +40,10 @@ if not known_args["debug"]: 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 -#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 +77,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 +120,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..a3a58b18ba 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -27,14 +27,6 @@ from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch MYPY = False -MimeTypeDatabase.addMimeType( - MimeType( - name = "application/x-cura-project-file", - comment = "Cura Project File", - suffixes = ["curaproject.3mf"] - ) -) - try: if not MYPY: import xml.etree.cElementTree as ET @@ -42,10 +34,20 @@ except ImportError: Logger.log("w", "Unable to load cElementTree, switching to slower version") import xml.etree.ElementTree as ET + ## 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) + + MimeTypeDatabase.addMimeType( + MimeType( + name = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", + comment="3MF", + suffixes=["3mf"] + ) + ) + self._supported_extensions = [".3mf"] self._root = None self._base_name = "" @@ -158,7 +160,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..1f950f275c 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -4,7 +4,6 @@ from configparser import ConfigParser import zipfile import os -import threading from typing import List, Tuple @@ -21,7 +20,7 @@ from UM.Settings.ContainerStack import ContainerStack from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.ContainerRegistry import ContainerRegistry -from UM.MimeTypeDatabase import MimeTypeDatabase +from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType from UM.Job import Job from UM.Preferences import Preferences @@ -84,6 +83,15 @@ class ExtruderInfo: class ThreeMFWorkspaceReader(WorkspaceReader): def __init__(self): super().__init__() + + MimeTypeDatabase.addMimeType( + MimeType( + name="application/x-cura-project-file", + comment="Cura Project File", + suffixes=["curaproject.3mf"] + ) + ) + self._supported_extensions = [".3mf"] self._dialog = WorkspaceDialog() self._3mf_mesh_reader = None @@ -456,7 +464,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 +583,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..feabf19818 100644 --- a/plugins/3MFReader/__init__.py +++ b/plugins/3MFReader/__init__.py @@ -13,8 +13,10 @@ from . import ThreeMFWorkspaceReader from UM.i18n import i18nCatalog from UM.Platform import Platform + catalog = i18nCatalog("cura") + def getMetaData() -> Dict: # Workarround for osx not supporting double file extensions correctly. if Platform.isOSX(): @@ -42,7 +44,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/ChangeLogPlugin/ChangeLog.txt b/plugins/ChangeLogPlugin/ChangeLog.txt index 9cb9a60e79..8da415df05 100755 --- a/plugins/ChangeLogPlugin/ChangeLog.txt +++ b/plugins/ChangeLogPlugin/ChangeLog.txt @@ -1,3 +1,127 @@ + + +[3.4.0] + +*Toolbox +The plugin browser has been remodeled into the Toolbox. Navigation now involves graphical elements such as tiles, which can be clicked for further details. + +*Upgradable bundled resources +It is now possible to have multiple versions of bundled resources installed: the bundled version and the downloaded upgrade. If an upgrade in the form of a package is present, the bundled version will not be loaded. If it's not present, Ultimaker Cura will revert to the bundled version. + +*Package manager recognizes bundled resources +Bundled packages are now made visible to the CuraPackageMangager. This means the resources are included by default, as well as the "wrapping" of a package, (e.g. package.json) so that the CuraPackageManger and Toolbox recognize them as being installed. + +*Retraction combing max distance +New setting for maximum combing travel distance. Combing travel moves longer than this value will use retraction. Contributed by smartavionics. + +*Infill support +When enabled, infill will be generated only where it is needed using a specialized support generation algorithm for the internal support structures of a part. Contributed by BagelOrb. + +*Print outside perimeter before holes +This prioritizes outside perimeters before printing holes. By printing holes as late as possible, there is a reduced risk of travel moves dislodging them from the build plate. This setting should only have an effect if printing outer before inner walls. Contributed by smartavionics. + +*Disable omitting retractions in support +Previous versions had no option to disable omitting retraction moves when printing supports, which could cause issues with third-party machines or materials. An option has been added to disable this. Contributed by BagelOrb. + +*Support wall line count +Added setting to configure how many walls to print around supports. Contributed by BagelOrb. + +*Maximum combing resolution +Combing travel moves are kept at least 1.5 mm long to prevent buffer underruns. + +*Avoid supports when traveling +Added setting to avoid supports when performing travel moves. This minimizes the risk of the print head hitting support material. + +*Rewrite cross infill +Experimental setting that allows you to input a path to an image to manipulate the cross infill density. This will overlay that image on your model. Contributed by BagelOrb. + +*Backup and restore +Added functionality to backup and restore settings and profiles to cloud using the Cura Backups plugin. + +*Auto-select model after import +User can now set preferences for the behavior of selecting a newly imported model or not. + +*Settings filter timeout +The settings filter is triggered on enter or after a 500ms timeout when typing a setting to filter. + +*Event measurements +Added time measurement to logs for occurrences, including startup time, file load time, number of items on the build plate when slicing, slicing time, and time and performance when moving items on the build plate, for benchmarking purposes. + +*Send anonymous data +Disable button on the ‘Send anonymous data’ popup has changed to a ‘more info’ button, with further options to enable/disable anonymous data messages. + +*Configuration error assistant +Detect and show potential configuration file errors to users, e.g. incorrect files and duplicate files in material or quality profiles, there are several places to check. Information is stored and communicated to the user to prevent crashing in future. + +*Disable ensure models are kept apart +Disable "Ensure models are kept apart" by default due to to a change in preference files. + +*Prepare and monitor QML files +Created two separate QML files for the Prepare and Monitor stages. + +*Hide bed temperature +Option to hide bed temperature when no heated bed is present. Contributed by ngraziano. + +*Reprap/Marlin GCODE flavor +RepRap firmware now lists values for all extruders in the "Filament used" GCODE comment. Contributed by smartavionics. + +*AutoDesk Inventor integration +Open AutoDesk inventor files (parts, assemblies, drawings) directly into Ultimaker Cura. Contributed by thopiekar. + +*Blender integration +Open Blender files directly into Ultimaker Cura. Contributed by thopiekar. + +*OpenSCAD integration +Open OpenSCAD files directly into Ultimaker Cura. Contributed by thopiekar. + +*FreeCAD integration +Open FreeCAD files directly into Ultimaker Cura. Contributed by thopiekar. + +*OctoPrint plugin +New version of the OctoPrint plugin for Ultimaker Cura. Contributed by fieldOfView. + +*Cura Backups +Backup and restore your configuration, including settings, materials and plugins, for use across different systems. + +*MakePrintable +New version of the MakePrintable plugin. + +*Compact Prepare sidebar +Plugin that replaces the sidebar with a more compact variation of the original sidebar. Nozzle and material dropdowns are combined into a single line, the “Check compatibility” link is removed, extruder selection buttons are downsized, recommended and custom mode selection buttons are moved to a combobox at the top, and margins are tweaked. Contributed by fieldOfView. + +*PauseAtHeight plugin +Bug fixes and improvements for PauseAtHeight plugin. Plugin now accounts for raft layers when choosing “Pause of layer no.” Now positions the nozzle at x and y values of the next layer when resuming. Contributed by JPFrancoia. + +*Bug fixes +- Prime tower purge fix. Prime tower purge now starts away from the center, minimizing the chance of overextrusion and nozzle obstructions. Contributed by BagelOrb. +- Extruder 2 temp via USB. Fixed a bug where temperatures can’t be read for a second extruder via USB. Contributed by kirilledelman. +- Move to next object position before bed heat. Print one at a time mode caused waiting for the bed temperature to reach the first layer temperature while the nozzle was still positioned on the top of the last part. This has been fixed so that the nozzle moves to the location of the next part before waiting for heat up. Contributed by smartavionics. +- Non-GCODE USB. Fixed a bug where the USB port doesn’t open if printer doesn't support GCODE. Contributed by ohrn. +- Improved wall overlap compensation. Minimizes unexpected behavior on overlap lines, providing smoother results. Contributed by BagelOrb. +- Configuration/sync. Fixes minor issues with the configuration/sync menu, such as text rendering on some OSX systems and untranslatable text. Contributed by fieldOfView. +- Print job name reslice. Fixed behavior where print job name changes back to origin when reslicing. +- Discard/keep. Customized settings don't give an 'discard or keep' dialog when changing material. +- Message box styling. Fixed bugs related to message box styling, such as the progress bar overlapping the button in the ‘Sending Data’ popup. +- Curaproject naming. Fixed bug related to two "curaprojects" in the file name when saving a project. +- No support on first layers. Fixed a bug related to no support generated causing failed prints when model is floating above build plate. +- False incompatible configuration. Fixed a bug where PrintCore and materials were flagged even though the configurations are compatible. +- Spiralize contour overlaps. Fixed a bug related to spiralize contour overlaps. +- Model saved outside build volume. Fixed a bug that would saved a model to file (GCODE) outside the build volume. +- Filament diameter line width. Adjust filament diameter to calculate line width in the GCODE parser. +- Holes in model surfaces. Fixed a bug where illogical travel moves leave holes in the model surface. +- Nozzle legacy file variant. Fixed crashes caused by loading legacy nozzle variant files. +- Brim wall order. Fixed a bug related to brim wall order. Contributed by smartavionics. +- GCODE reader gaps. Fixed a GCODE reader bug that can create a gap at the start of a spiralized layer. +- Korean translation. Fixed some typos in Korean translation. +- ARM/Mali systems. Graphics pipeline for ARM/Mali fixed. Contributed by jwalt. +- NGC Writer. Fixed missing author for NGC Writer plugin. +- Support blocker legacy GPU. Fixes depth picking on older GPUs that do not support the 4.1 shading model which caused the support blocker to put cubes in unexpected locations. Contributed by fieldOfView. + +*Third-party printers +- Felix Tec4 printer. Updated definitions for Felix Tec4. Contributed by kerog777. +- Deltacomb. Updated definitions for Deltacomb. Contributed by kaleidoscopeit. +- Rigid3D Mucit. Added definitions for Rigid3D Mucit. Contributed by Rigid3D. + [3.3.0] *Profile for the Ultimaker S5 @@ -66,7 +190,7 @@ Generate a cube mesh to prevent support material generation in specific areas of *Real bridging - smartavionics New experimental feature that detects bridges, adjusting the print speed, slow and fan speed to enhance print quality on bridging parts. -*Updated CuraEngine executable - thopiekar & Ultimaker B.V. ❤️ +*Updated CuraEngine executable - thopiekar & Ultimaker B.V. The CuraEngine executable contains a dedicated icon, author and license info on Windows now. The icon has been designed by Ultimaker B.V. *Use RapidJSON and ClipperLib from system libraries 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/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 0297a34385..467351dd4b 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -287,14 +287,9 @@ class StartSliceJob(Job): # \return A dictionary of replacement tokens to the values they should be # replaced with. def _buildReplacementTokens(self, stack) -> dict: - default_extruder_position = int(Application.getInstance().getMachineManager().defaultExtruderPosition) result = {} for key in stack.getAllKeys(): - setting_type = stack.definition.getProperty(key, "type") value = stack.getProperty(key, "value") - if setting_type == "extruder" and value == -1: - # replace with the default value - value = default_extruder_position result[key] = value Job.yieldThread() 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 66ee43209f..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, @@ -63,13 +62,26 @@ class FirmwareUpdateCheckerJob(Job): # notify the user when no new firmware version is available. if (checked_version != "") and (checked_version != current_version): Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE") - message = Message(i18n_catalog.i18nc("@info Don't translate {machine_name}, since it gets replaced by a printer name!", "New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format(machine_name = machine_name), - title = i18n_catalog.i18nc("@info:title The %s gets replaced with the printer name.", "New %s firmware available") % machine_name) - message.addAction("download", i18n_catalog.i18nc("@action:button", "How to update"), "[no_icon]", "[no_description]") + + message = Message(i18n_catalog.i18nc( + "@info Don't translate {machine_name}, since it gets replaced by a printer name!", + "New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format( + machine_name=machine_name), + title=i18n_catalog.i18nc( + "@info:title The %s gets replaced with the printer name.", + "New %s firmware available") % machine_name) + + message.addAction("download", + i18n_catalog.i18nc("@action:button", "How to update"), + "[no_icon]", + "[no_description]", + button_style=Message.ActionButtonStyle.LINK, + button_align=Message.ActionButtonStyle.BUTTON_ALIGN_LEFT) + # If we do this in a cool way, the download url should be available in the JSON file if self._set_download_url_callback: - self._set_download_url_callback("https://ultimaker.com/en/resources/23129-updating-the-firmware?utm_source=cura&utm_medium=software&utm_campaign=hw-update") + self._set_download_url_callback("https://ultimaker.com/en/resources/20500-upgrade-firmware") message.actionTriggered.connect(self._callback) message.show() 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/ModelChecker/ModelChecker.py b/plugins/ModelChecker/ModelChecker.py index 297844a0a3..d2c2eefac2 100644 --- a/plugins/ModelChecker/ModelChecker.py +++ b/plugins/ModelChecker/ModelChecker.py @@ -27,7 +27,7 @@ class ModelChecker(QObject, Extension): self._caution_message = Message("", #Message text gets set when the message gets shown, to display the models in question. lifetime = 0, - title = catalog.i18nc("@info:title", "Model Checker Warning")) + title = catalog.i18nc("@info:title", "3D Model Assistant")) Application.getInstance().initializationFinished.connect(self._pluginsInitialized) Application.getInstance().getController().getScene().sceneChanged.connect(self._onChanged) 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 82e07da464..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,22 +33,23 @@ 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, title = catalog.i18nc("@info:title", "Collecting Data")) - self.send_slice_info_message.addAction("Dismiss", name = catalog.i18nc("@action:button", "Allow"), icon = None, - description = catalog.i18nc("@action:tooltip", "Allow Cura to send anonymized usage statistics to help prioritize future improvements to Cura. Some of your preferences and settings are sent, the Cura version and a hash of the models you're slicing.")) self.send_slice_info_message.addAction("MoreInfo", name = catalog.i18nc("@action:button", "More info"), icon = None, description = catalog.i18nc("@action:tooltip", "See more information on what data Cura sends."), button_style = Message.ActionButtonStyle.LINK) + + self.send_slice_info_message.addAction("Dismiss", name = catalog.i18nc("@action:button", "Allow"), icon = None, + description = catalog.i18nc("@action:tooltip", "Allow Cura to send anonymized usage statistics to help prioritize future improvements to Cura. Some of your preferences and settings are sent, the Cura version and a hash of the models you're slicing.")) self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered) self.send_slice_info_message.show() @@ -62,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() @@ -88,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 @@ -107,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: @@ -122,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/Toolbox/resources/qml/ToolboxCompatibilityChart.qml b/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml index 229ab5afb3..b4219d53bf 100644 --- a/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml +++ b/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml @@ -31,8 +31,9 @@ Item frameVisible: false selectionMode: 0 model: packageData.supported_configs - headerDelegate: Item + headerDelegate: Rectangle { + color: UM.Theme.getColor("sidebar") height: UM.Theme.getSize("toolbox_chart_row").height Label { diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml index f82fb049d8..cd1e4cdbda 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml @@ -34,6 +34,7 @@ Column // Don't allow installing while another download is running enabled: installed || !(toolbox.isDownloading && toolbox.activePackage != model) opacity: enabled ? 1.0 : 0.5 + visible: !updateButton.visible // Don't show when the update button is visible } ToolboxProgressButton @@ -55,7 +56,7 @@ Column // Don't allow installing while another download is running enabled: !(toolbox.isDownloading && toolbox.activePackage != model) opacity: enabled ? 1.0 : 0.5 - visible: installed && canUpdate + visible: canUpdate } Connections { diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml index e1ffc6326c..05186b961d 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml @@ -12,6 +12,7 @@ Column height: childrenRect.height width: parent.width spacing: UM.Theme.getSize("default_margin").height + /* Hidden for 3.4 Label { id: heading @@ -20,6 +21,7 @@ Column color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("medium") } + */ GridLayout { id: grid diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml index 170fd10fc7..3f9173c69b 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml @@ -18,6 +18,7 @@ ScrollView spacing: UM.Theme.getSize("default_margin").height padding: UM.Theme.getSize("wide_margin").height height: childrenRect.height + 2 * padding + ToolboxDownloadsShowcase { id: showcase @@ -29,6 +30,7 @@ ScrollView width: parent.width height: UM.Theme.getSize("default_lining").height } + ToolboxDownloadsGrid { id: allPlugins diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml index d3ce443704..5a8128b51e 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml @@ -19,10 +19,11 @@ Column color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("medium") } - Row + Grid { height: childrenRect.height spacing: UM.Theme.getSize("wide_margin").width + columns: 3 anchors { horizontalCenter: parent.horizontalCenter diff --git a/plugins/Toolbox/resources/qml/ToolboxHeader.qml b/plugins/Toolbox/resources/qml/ToolboxHeader.qml index 88495e3f63..9c9f967d54 100644 --- a/plugins/Toolbox/resources/qml/ToolboxHeader.qml +++ b/plugins/Toolbox/resources/qml/ToolboxHeader.qml @@ -33,6 +33,7 @@ Item toolbox.viewPage = "overview" } } + ToolboxTabButton { text: catalog.i18nc("@title:tab", "Materials") diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml index 78c970659c..b16564fdd2 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml @@ -16,7 +16,7 @@ Item { color: UM.Theme.getColor("lining") width: parent.width - height: UM.Theme.getSize("default_lining").height + height: Math.floor(UM.Theme.getSize("default_lining").height) anchors.bottom: parent.bottom } Row @@ -40,14 +40,14 @@ Item Column { id: pluginInfo - topPadding: UM.Theme.getSize("default_margin").height / 2 + topPadding: Math.floor(UM.Theme.getSize("default_margin").height / 2) property var color: model.type === "plugin" && !isEnabled ? UM.Theme.getColor("lining") : UM.Theme.getColor("text") - width: tileRow.width - (authorInfo.width + pluginActions.width + 2 * tileRow.spacing + ((disableButton.visible) ? disableButton.width + tileRow.spacing : 0)) + width: Math.floor(tileRow.width - (authorInfo.width + pluginActions.width + 2 * tileRow.spacing + ((disableButton.visible) ? disableButton.width + tileRow.spacing : 0))) Label { text: model.name width: parent.width - height: UM.Theme.getSize("toolbox_property_label").height + height: Math.floor(UM.Theme.getSize("toolbox_property_label").height) wrapMode: Text.WordWrap font: UM.Theme.getFont("default_bold") color: pluginInfo.color @@ -81,7 +81,7 @@ Item } } width: parent.width - height: UM.Theme.getSize("toolbox_property_label").height + height: Math.floor(UM.Theme.getSize("toolbox_property_label").height) wrapMode: Text.WordWrap verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignLeft diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml index 0ae738b71d..b0aecfc9a2 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml @@ -13,6 +13,16 @@ Column width: UM.Theme.getSize("toolbox_action_button").width spacing: UM.Theme.getSize("narrow_margin").height + Label + { + visible: !model.is_installed + text: catalog.i18nc("@label", "Will install upon restarting") + color: UM.Theme.getColor("lining") + font: UM.Theme.getFont("default") + wrapMode: Text.WordWrap + width: parent.width + } + ToolboxProgressButton { id: updateButton @@ -39,7 +49,7 @@ Column { id: removeButton text: canDowngrade ? catalog.i18nc("@action:button", "Downgrade") : catalog.i18nc("@action:button", "Uninstall") - visible: !model.is_bundled + visible: !model.is_bundled && model.is_installed enabled: !toolbox.isDownloading style: ButtonStyle { diff --git a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml index b598bd96d0..2744e40ec9 100644 --- a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml +++ b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml @@ -150,7 +150,7 @@ Item { id: loader visible: active - source: "../images/loading.gif" + source: visible ? "../images/loading.gif" : "" width: UM.Theme.getSize("toolbox_loader").width height: UM.Theme.getSize("toolbox_loader").height anchors.right: button.left diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py index 5ec3eba1d8..2a3b169e87 100644 --- a/plugins/Toolbox/src/PackagesModel.py +++ b/plugins/Toolbox/src/PackagesModel.py @@ -29,8 +29,9 @@ class PackagesModel(ListModel): self.addRoleName(Qt.UserRole + 12, "last_updated") self.addRoleName(Qt.UserRole + 13, "is_bundled") self.addRoleName(Qt.UserRole + 14, "is_enabled") - self.addRoleName(Qt.UserRole + 15, "has_configs") - self.addRoleName(Qt.UserRole + 16, "supported_configs") + self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed + self.addRoleName(Qt.UserRole + 16, "has_configs") + self.addRoleName(Qt.UserRole + 17, "supported_configs") # List of filters for queries. The result is the union of the each list of results. self._filter = {} # type: Dict[str, str] @@ -73,6 +74,7 @@ class PackagesModel(ListModel): "last_updated": package["last_updated"] if "last_updated" in package else None, "is_bundled": package["is_bundled"] if "is_bundled" in package else False, "is_enabled": package["is_enabled"] if "is_enabled" in package else False, + "is_installed": package["is_installed"] if "is_installed" in package else False, "has_configs": has_configs, "supported_configs": configs_model }) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index eb5ea94811..c29c673c8a 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -28,7 +28,8 @@ i18n_catalog = i18nCatalog("cura") ## The Toolbox class is responsible of communicating with the server through the API class Toolbox(QObject, Extension): - DEFAULT_PACKAGES_API_ROOT = "https://api.ultimaker.com" + DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" + DEFAULT_CLOUD_API_VERSION = 1 def __init__(self, parent=None) -> None: super().__init__(parent) @@ -36,14 +37,11 @@ class Toolbox(QObject, Extension): self._application = Application.getInstance() self._package_manager = None self._plugin_registry = Application.getInstance().getPluginRegistry() - self._packages_api_root = self._getPackagesApiRoot() - self._packages_version = self._getPackagesVersion() - self._api_version = 1 - self._api_url = "{api_root}/cura-packages/v{api_version}/cura/v{package_version}".format( - api_root = self._packages_api_root, - api_version = self._api_version, - package_version = self._packages_version - ) + + self._sdk_version = None + self._cloud_api_version = None + self._cloud_api_root = None + self._api_url = None # Network: self._get_packages_request = None @@ -64,21 +62,19 @@ class Toolbox(QObject, Extension): ) ) ] - self._request_urls = { - "authors": QUrl("{base_url}/authors".format(base_url = self._api_url)), - "packages": QUrl("{base_url}/packages".format(base_url = self._api_url)), - "plugins_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)), - "materials_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)) - } + self._request_urls = {} self._to_update = [] # Package_ids that are waiting to be updated + self._old_plugin_ids = [] # Data: self._metadata = { "authors": [], "packages": [], "plugins_showcase": [], + "plugins_available": [], "plugins_installed": [], "materials_showcase": [], + "materials_available": [], "materials_installed": [] } @@ -161,22 +157,52 @@ class Toolbox(QObject, Extension): # this is initialized. Therefore, we wait until the application is ready. def _onAppInitialized(self) -> None: self._package_manager = Application.getInstance().getPackageManager() + self._sdk_version = self._getSDKVersion() + self._cloud_api_version = self._getCloudAPIVersion() + self._cloud_api_root = self._getCloudAPIRoot() + self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format( + cloud_api_root=self._cloud_api_root, + cloud_api_version=self._cloud_api_version, + sdk_version=self._sdk_version + ) + self._request_urls = { + "authors": QUrl("{base_url}/authors".format(base_url=self._api_url)), + "packages": QUrl("{base_url}/packages".format(base_url=self._api_url)), + "plugins_showcase": QUrl("{base_url}/showcase".format(base_url=self._api_url)), + "plugins_available": QUrl("{base_url}/packages?package_type=plugin".format(base_url=self._api_url)), + "materials_showcase": QUrl("{base_url}/showcase".format(base_url=self._api_url)), + "materials_available": QUrl("{base_url}/packages?package_type=material".format(base_url=self._api_url)) + } # Get the API root for the packages API depending on Cura version settings. - def _getPackagesApiRoot(self) -> str: + def _getCloudAPIRoot(self) -> str: if not hasattr(cura, "CuraVersion"): - return self.DEFAULT_PACKAGES_API_ROOT - if not hasattr(cura.CuraVersion, "CuraPackagesApiRoot"): - return self.DEFAULT_PACKAGES_API_ROOT - return cura.CuraVersion.CuraPackagesApiRoot + return self.DEFAULT_CLOUD_API_ROOT + if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): + return self.DEFAULT_CLOUD_API_ROOT + if not cura.CuraVersion.CuraCloudAPIRoot: + return self.DEFAULT_CLOUD_API_ROOT + return cura.CuraVersion.CuraCloudAPIRoot + + # Get the cloud API version from CuraVersion + def _getCloudAPIVersion(self) -> int: + if not hasattr(cura, "CuraVersion"): + return self.DEFAULT_CLOUD_API_VERSION + if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): + return self.DEFAULT_CLOUD_API_VERSION + if not cura.CuraVersion.CuraCloudAPIVersion: + return self.DEFAULT_CLOUD_API_VERSION + return cura.CuraVersion.CuraCloudAPIVersion # Get the packages version depending on Cura version settings. - def _getPackagesVersion(self) -> int: + def _getSDKVersion(self) -> int: if not hasattr(cura, "CuraVersion"): return self._plugin_registry.APIVersion - if not hasattr(cura.CuraVersion, "CuraPackagesVersion"): + if not hasattr(cura.CuraVersion, "CuraSDKVersion"): return self._plugin_registry.APIVersion - return cura.CuraVersion.CuraPackagesVersion + if not cura.CuraVersion.CuraSDKVersion: + return self._plugin_registry.APIVersion + return cura.CuraVersion.CuraSDKVersion @pyqtSlot() def browsePackages(self) -> None: @@ -212,15 +238,52 @@ class Toolbox(QObject, Extension): dialog = Application.getInstance().createQmlComponent(path, {"toolbox": self}) return dialog + + def _convertPluginMetadata(self, plugin: dict) -> dict: + formatted = { + "package_id": plugin["id"], + "package_type": "plugin", + "display_name": plugin["plugin"]["name"], + "package_version": plugin["plugin"]["version"], + "sdk_version": plugin["plugin"]["api"], + "author": { + "author_id": plugin["plugin"]["author"], + "display_name": plugin["plugin"]["author"] + }, + "is_installed": True, + "description": plugin["plugin"]["description"] + } + return formatted + @pyqtSlot() def _updateInstalledModels(self) -> None: + + # This is moved here to avoid code duplication and so that after installing plugins they get removed from the + # list of old plugins + old_plugin_ids = self._plugin_registry.getInstalledPlugins() + installed_package_ids = self._package_manager.getAllInstalledPackageIDs() + + self._old_plugin_ids = [] + self._old_plugin_metadata = [] + + for plugin_id in old_plugin_ids: + if plugin_id not in installed_package_ids: + Logger.log('i', 'Found a plugin that was installed with the old plugin browser: %s', plugin_id) + + old_metadata = self._plugin_registry.getMetaData(plugin_id) + new_metadata = self._convertPluginMetadata(old_metadata) + + self._old_plugin_ids.append(plugin_id) + self._old_plugin_metadata.append(new_metadata) + all_packages = self._package_manager.getAllInstalledPackagesInfo() if "plugin" in all_packages: - self._metadata["plugins_installed"] = all_packages["plugin"] + self._metadata["plugins_installed"] = all_packages["plugin"] + self._old_plugin_metadata self._models["plugins_installed"].setMetadata(self._metadata["plugins_installed"]) self.metadataChanged.emit() if "material" in all_packages: self._metadata["materials_installed"] = all_packages["material"] + # TODO: ADD MATERIALS HERE ONCE MATERIALS PORTION OF TOOLBOX IS LIVE self._models["materials_installed"].setMetadata(self._metadata["materials_installed"]) self.metadataChanged.emit() @@ -250,8 +313,6 @@ class Toolbox(QObject, Extension): if remote_package: download_url = remote_package["download_url"] Logger.log("d", "Updating package [%s]..." % plugin_id) - if self._package_manager.isUserInstalledPackage(plugin_id): - self.uninstall(plugin_id) self.startDownload(download_url) else: Logger.log("e", "Could not update package [%s] because there is no remote package info available.", plugin_id) @@ -306,6 +367,9 @@ class Toolbox(QObject, Extension): # -------------------------------------------------------------------------- @pyqtSlot(str, result = bool) def canUpdate(self, package_id: str) -> bool: + if self.isOldPlugin(package_id): + return True + local_package = self._package_manager.getInstalledPackageInfo(package_id) if local_package is None: return False @@ -318,19 +382,21 @@ class Toolbox(QObject, Extension): remote_version = Version(remote_package["package_version"]) return remote_version > local_version - @pyqtSlot(str, result=bool) + @pyqtSlot(str, result = bool) def canDowngrade(self, package_id: str) -> bool: + # If the currently installed version is higher than the bundled version (if present), the we can downgrade + # this package. local_package = self._package_manager.getInstalledPackageInfo(package_id) if local_package is None: return False - remote_package = self.getRemotePackage(package_id) - if remote_package is None: + bundled_package = self._package_manager.getBundledPackageInfo(package_id) + if bundled_package is None: return False local_version = Version(local_package["package_version"]) - remote_version = Version(remote_package["package_version"]) - return remote_version < local_version + bundled_version = Version(bundled_package["package_version"]) + return bundled_version < local_version @pyqtSlot(str, result = bool) def isInstalled(self, package_id: str) -> bool: @@ -342,6 +408,13 @@ class Toolbox(QObject, Extension): return True return False + # Check for plugins that were installed with the old plugin browser + @pyqtSlot(str, result = bool) + def isOldPlugin(self, plugin_id: str) -> bool: + if plugin_id in self._old_plugin_ids: + return True + return False + def loadingComplete(self) -> bool: populated = 0 for list in self._metadata.items(): @@ -383,7 +456,10 @@ class Toolbox(QObject, Extension): def resetDownload(self) -> None: if self._download_reply: - self._download_reply.downloadProgress.disconnect(self._onDownloadProgress) + try: + self._download_reply.downloadProgress.disconnect(self._onDownloadProgress) + except TypeError: #Raised when the method is not connected to the signal yet. + pass #Don't need to disconnect. self._download_reply.abort() self._download_reply = None self._download_request = None diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 282d507e09..c54ced6b13 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -148,6 +148,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): def selectPrinter(self, target_printer: str = "") -> None: self._sending_job.send(target_printer) + @pyqtSlot() + def cancelPrintSelection(self) -> None: + self._sending_gcode = False + ## Greenlet to send a job to the printer over the network. # # This greenlet gets called asynchronously in requestWrite. It is a @@ -388,7 +392,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._updatePrintJob(print_job, print_job_data) if print_job.state != "queued": # Print job should be assigned to a printer. - if print_job.state in ["failed", "finished", "aborted"]: + if print_job.state in ["failed", "finished", "aborted", "none"]: # Print job was already completed, so don't attach it to a printer. printer = None else: diff --git a/plugins/UM3NetworkPrinting/PrintWindow.qml b/plugins/UM3NetworkPrinting/PrintWindow.qml index 5b011d98c4..0553db0eb2 100644 --- a/plugins/UM3NetworkPrinting/PrintWindow.qml +++ b/plugins/UM3NetworkPrinting/PrintWindow.qml @@ -90,6 +90,7 @@ UM.Dialog onClicked: { base.visible = false; printerSelectionCombobox.currentIndex = 0 + OutputDevice.cancelPrintSelection() } } ] 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/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index b2ca5562e3..00eb2f0b25 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -379,13 +379,14 @@ class USBPrinterOutputDevice(PrinterOutputDevice): def resumePrint(self): self._paused = False + self._sendNextGcodeLine() #Send one line of g-code next so that we'll trigger an "ok" response loop even if we're not polling temperatures. def cancelPrint(self): self._gcode_position = 0 self._gcode.clear() self._printers[0].updateActivePrintJob(None) self._is_printing = False - self._is_paused = False + self._paused = False # Turn off temperatures, fan and steppers self._sendCommand("M140 S0") 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/VersionUpgrade/VersionUpgrade21to22/MachineInstance.py b/plugins/VersionUpgrade/VersionUpgrade21to22/MachineInstance.py index 4a75b23c2f..37b6989add 100644 --- a/plugins/VersionUpgrade/VersionUpgrade21to22/MachineInstance.py +++ b/plugins/VersionUpgrade/VersionUpgrade21to22/MachineInstance.py @@ -107,7 +107,12 @@ class MachineInstance: user_profile["values"] = {} version_upgrade_manager = UM.VersionUpgradeManager.VersionUpgradeManager.getInstance() - user_storage = os.path.join(Resources.getDataStoragePath(), next(iter(version_upgrade_manager.getStoragePaths("user")))) + user_version_to_paths_dict = version_upgrade_manager.getStoragePaths("user") + paths_set = set() + for paths in user_version_to_paths_dict.values(): + paths_set |= paths + + user_storage = os.path.join(Resources.getDataStoragePath(), next(iter(paths_set))) user_profile_file = os.path.join(user_storage, urllib.parse.quote_plus(self._name) + "_current_settings.inst.cfg") if not os.path.exists(user_storage): os.makedirs(user_storage) @@ -135,4 +140,4 @@ class MachineInstance: output = io.StringIO() config.write(output) - return [self._filename], [output.getvalue()] \ No newline at end of file + return [self._filename], [output.getvalue()] diff --git a/plugins/VersionUpgrade/VersionUpgrade33to34/VersionUpgrade33to34.py b/plugins/VersionUpgrade/VersionUpgrade33to34/VersionUpgrade33to34.py index 17abace547..e2241fd195 100644 --- a/plugins/VersionUpgrade/VersionUpgrade33to34/VersionUpgrade33to34.py +++ b/plugins/VersionUpgrade/VersionUpgrade33to34/VersionUpgrade33to34.py @@ -6,6 +6,9 @@ import io #To serialise the preference files afterwards. from UM.VersionUpgrade import VersionUpgrade #We're inheriting from this. +_renamed_settings = { + "infill_hollow": "infill_support_enabled" +} ## Upgrades configurations from the state they were in at version 3.3 to the # state they should be in at version 3.4. @@ -38,6 +41,17 @@ class VersionUpgrade33to34(VersionUpgrade): # Update version number. parser["general"]["version"] = "4" + if "values" in parser: + #If infill_hollow was enabled and the overhang angle was adjusted, copy that overhang angle to the new infill support angle. + if "infill_hollow" in parser["values"] and parser["values"]["infill_hollow"] and "support_angle" in parser["values"]: + parser["values"]["infill_support_angle"] = parser["values"]["support_angle"] + + #Renamed settings. + for original, replacement in _renamed_settings.items(): + if original in parser["values"]: + parser["values"][replacement] = parser["values"][original] + del parser["values"][original] + result = io.StringIO() parser.write(result) - return [filename], [result.getvalue()] + return [filename], [result.getvalue()] \ No newline at end of file diff --git a/plugins/VersionUpgrade/VersionUpgrade33to34/__init__.py b/plugins/VersionUpgrade/VersionUpgrade33to34/__init__.py index c36247353f..4faa1290b5 100644 --- a/plugins/VersionUpgrade/VersionUpgrade33to34/__init__.py +++ b/plugins/VersionUpgrade/VersionUpgrade33to34/__init__.py @@ -21,7 +21,7 @@ def getMetaData(): }, "quality_changes": { "get_version": upgrade.getCfgVersion, - "location": {"./quality"} + "location": {"./quality_changes"} }, "user": { "get_version": upgrade.getCfgVersion, 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) } diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 70d07c1fc5..f0d6915f04 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import copy @@ -12,10 +12,10 @@ import xml.etree.ElementTree as ET from UM.Resources import Resources from UM.Logger import Logger from cura.CuraApplication import CuraApplication - import UM.Dictionary from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.ContainerRegistry import ContainerRegistry +from UM.ConfigurationErrorMessage import ConfigurationErrorMessage from .XmlMaterialValidator import XmlMaterialValidator @@ -540,7 +540,9 @@ class XmlMaterialProfile(InstanceContainer): validation_message = XmlMaterialValidator.validateMaterialMetaData(meta_data) if validation_message is not None: - raise Exception("Not valid material profile: %s" % (validation_message)) + ConfigurationErrorMessage.getInstance().addFaultyContainers(self.getId()) + Logger.log("e", "Not a valid material profile: {message}".format(message = validation_message)) + return property_values = {} properties = data.iterfind("./um:properties/*", self.__namespaces) diff --git a/resources/bundled_packages.json b/resources/bundled_packages.json index a63d08ddab..7f3ba2a92e 100644 --- a/resources/bundled_packages.json +++ b/resources/bundled_packages.json @@ -6,7 +6,7 @@ "display_name": "3MF Reader", "description": "Provides support for reading 3MF files.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -23,7 +23,7 @@ "display_name": "3MF Writer", "description": "Provides support for writing 3MF files.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -40,7 +40,7 @@ "display_name": "Change Log", "description": "Shows changes since latest checked version.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -57,7 +57,7 @@ "display_name": "CuraEngine Backend", "description": "Provides the link to the CuraEngine slicing backend.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -74,7 +74,7 @@ "display_name": "Cura Profile Reader", "description": "Provides support for importing Cura profiles.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -91,7 +91,7 @@ "display_name": "Cura Profile Writer", "description": "Provides support for exporting Cura profiles.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -108,7 +108,7 @@ "display_name": "Firmware Update Checker", "description": "Checks for firmware updates.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -125,7 +125,7 @@ "display_name": "Compressed G-code Reader", "description": "Reads g-code from a compressed archive.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -142,7 +142,7 @@ "display_name": "Compressed G-code Writer", "description": "Writes g-code to a compressed archive.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -159,7 +159,7 @@ "display_name": "G-Code Profile Reader", "description": "Provides support for importing profiles from g-code files.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -176,7 +176,7 @@ "display_name": "G-Code Reader", "description": "Allows loading and displaying G-code files.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "VictorLarchenko", @@ -193,7 +193,7 @@ "display_name": "G-Code Writer", "description": "Writes g-code to a file.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -210,7 +210,7 @@ "display_name": "Image Reader", "description": "Enables ability to generate printable geometry from 2D image files.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -227,7 +227,7 @@ "display_name": "Legacy Cura Profile Reader", "description": "Provides support for importing profiles from legacy Cura versions.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -244,7 +244,7 @@ "display_name": "Machine Settings Action", "description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "fieldOfView", @@ -261,7 +261,7 @@ "display_name": "Model Checker", "description": "Checks models and print configuration for possible printing issues and give suggestions.", "package_version": "0.1.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -278,7 +278,7 @@ "display_name": "Monitor Stage", "description": "Provides a monitor stage in Cura.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -295,7 +295,7 @@ "display_name": "Per-Object Settings Tool", "description": "Provides the per-model settings.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -312,7 +312,7 @@ "display_name": "Post Processing", "description": "Extension that allows for user created scripts for post processing.", "package_version": "2.2.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -329,7 +329,7 @@ "display_name": "Prepare Stage", "description": "Provides a prepare stage in Cura.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -346,7 +346,7 @@ "display_name": "Removable Drive Output Device", "description": "Provides removable drive hotplugging and writing support.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -363,7 +363,7 @@ "display_name": "Simulation View", "description": "Provides the Simulation view.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -380,7 +380,7 @@ "display_name": "Slice Info", "description": "Submits anonymous slice info. Can be disabled through preferences.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -397,7 +397,7 @@ "display_name": "Solid View", "description": "Provides a normal solid mesh view.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -414,7 +414,7 @@ "display_name": "Support Eraser Tool", "description": "Creates an eraser mesh to block the printing of support in certain places.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -431,7 +431,7 @@ "display_name": "Toolbox", "description": "Find, manage and install new Cura packages.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -448,7 +448,7 @@ "display_name": "UFP Writer", "description": "Provides support for writing Ultimaker Format Packages.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -465,7 +465,7 @@ "display_name": "Ultimaker Machine Actions", "description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -482,7 +482,7 @@ "display_name": "UM3 Network Printing", "description": "Manages network connections to Ultimaker 3 printers.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -499,7 +499,7 @@ "display_name": "USB Printing", "description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -516,7 +516,7 @@ "display_name": "User Agreement", "description": "Ask the user once if he/she agrees with our license.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -533,7 +533,7 @@ "display_name": "Version Upgrade 2.1 to 2.2", "description": "Upgrades configurations from Cura 2.1 to Cura 2.2.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -550,7 +550,7 @@ "display_name": "Version Upgrade 2.2 to 2.4", "description": "Upgrades configurations from Cura 2.2 to Cura 2.4.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -567,7 +567,7 @@ "display_name": "Version Upgrade 2.5 to 2.6", "description": "Upgrades configurations from Cura 2.5 to Cura 2.6.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -584,7 +584,7 @@ "display_name": "Version Upgrade 2.6 to 2.7", "description": "Upgrades configurations from Cura 2.6 to Cura 2.7.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -601,7 +601,7 @@ "display_name": "Version Upgrade 2.7 to 3.0", "description": "Upgrades configurations from Cura 2.7 to Cura 3.0.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -618,7 +618,7 @@ "display_name": "Version Upgrade 3.0 to 3.1", "description": "Upgrades configurations from Cura 3.0 to Cura 3.1.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -635,7 +635,7 @@ "display_name": "Version Upgrade 3.2 to 3.3", "description": "Upgrades configurations from Cura 3.2 to Cura 3.3.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -652,7 +652,7 @@ "display_name": "Version Upgrade 3.3 to 3.4", "description": "Upgrades configurations from Cura 3.3 to Cura 3.4.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -669,7 +669,7 @@ "display_name": "X3D Reader", "description": "Provides support for reading X3D files.", "package_version": "0.5.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "SevaAlekseyev", @@ -686,7 +686,7 @@ "display_name": "XML Material Profiles", "description": "Provides capabilities to read and write XML-based material profiles.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -703,7 +703,7 @@ "display_name": "X-Ray View", "description": "Provides the X-Ray view.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com", "author": { "author_id": "Ultimaker", @@ -720,7 +720,7 @@ "display_name": "Dagoma Chromatik PLA", "description": "Filament testé et approuvé pour les imprimantes 3D Dagoma. Chromatik est l'idéal pour débuter et suivre les tutoriels premiers pas. Il vous offre qualité et résistance pour chacune de vos impressions.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://dagoma.fr/boutique/filaments.html", "author": { "author_id": "Dagoma", @@ -737,7 +737,7 @@ "display_name": "FABtotum ABS", "description": "This material is easy to be extruded but it is not the simplest to use. It is one of the most used in 3D printing to get very well finished objects. It is not sustainable and its smoke can be dangerous if inhaled. The reason to prefer this filament to PLA is mainly because of its precision and mechanical specs. ABS (for plastic) stands for Acrylonitrile Butadiene Styrene and it is a thermoplastic which is widely used in everyday objects. It can be printed with any FFF 3D printer which can get to high temperatures as it must be extruded in a range between 220° and 245°, so it’s compatible with all versions of the FABtotum Personal fabricator.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=40", "author": { "author_id": "FABtotum", @@ -754,7 +754,7 @@ "display_name": "FABtotum Nylon", "description": "When 3D printing started this material was not listed among the extrudable filaments. It is flexible as well as resistant to tractions. It is well known for its uses in textile but also in industries which require a strong and flexible material. There are different kinds of Nylon: 3D printing mostly uses Nylon 6 and Nylon 6.6, which are the most common. It requires higher temperatures to be printed, so a 3D printer must be able to reach them (around 240°C): the FABtotum, of course, can.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=53", "author": { "author_id": "FABtotum", @@ -771,7 +771,7 @@ "display_name": "FABtotum PLA", "description": "It is the most common filament used for 3D printing. It is studied to be bio-degradable as it comes from corn starch’s sugar mainly. It is completely made of renewable sources and has no footprint on polluting. PLA stands for PolyLactic Acid and it is a thermoplastic that today is still considered the easiest material to be 3D printed. It can be extruded at lower temperatures: the standard range of FABtotum’s one is between 185° and 195°.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=39", "author": { "author_id": "FABtotum", @@ -788,7 +788,7 @@ "display_name": "FABtotum TPU Shore 98A", "description": "", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=66", "author": { "author_id": "FABtotum", @@ -805,7 +805,7 @@ "display_name": "Fiberlogy HD PLA", "description": "With our HD PLA you have many more options. You can use this material in two ways. Choose the one you like best. You can use it as a normal PLA and get prints characterized by a very good adhesion between the layers and high precision. You can also make your prints acquire similar properties to that of ABS – better impact resistance and high temperature resistance. All you need is an oven. Yes, an oven! By annealing our HD PLA in an oven, in accordance with the manual, you will avoid all the inconveniences of printing with ABS, such as unpleasant odour or hazardous fumes.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "http://fiberlogy.com/en/fiberlogy-filaments/filament-hd-pla/", "author": { "author_id": "Fiberlogy", @@ -822,7 +822,7 @@ "display_name": "Filo3D PLA", "description": "Fast, safe and reliable printing. PLA is ideal for the fast and reliable printing of parts and prototypes with a great surface quality.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://dagoma.fr", "author": { "author_id": "Dagoma", @@ -839,7 +839,7 @@ "display_name": "IMADE3D JellyBOX PETG", "description": "", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "http://shop.imade3d.com/filament.html", "author": { "author_id": "IMADE3D", @@ -856,7 +856,7 @@ "display_name": "IMADE3D JellyBOX PLA", "description": "", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "http://shop.imade3d.com/filament.html", "author": { "author_id": "IMADE3D", @@ -873,7 +873,7 @@ "display_name": "Octofiber PLA", "description": "PLA material from Octofiber.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://nl.octofiber.com/3d-printing-filament/pla.html", "author": { "author_id": "Octofiber", @@ -890,7 +890,7 @@ "display_name": "PolyFlex™ PLA", "description": "PolyFlex™ is a highly flexible yet easy to print 3D printing material. Featuring good elasticity and a large strain-to- failure, PolyFlex™ opens up a completely new realm of applications.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "http://www.polymaker.com/shop/polyflex/", "author": { "author_id": "Polymaker", @@ -907,7 +907,7 @@ "display_name": "PolyMax™ PLA", "description": "PolyMax™ PLA is a 3D printing material with excellent mechanical properties and printing quality. PolyMax™ PLA has an impact resistance of up to nine times that of regular PLA, and better overall mechanical properties than ABS.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "http://www.polymaker.com/shop/polymax/", "author": { "author_id": "Polymaker", @@ -924,7 +924,7 @@ "display_name": "PolyPlus™ PLA True Colour", "description": "PolyPlus™ PLA is a premium PLA designed for all desktop FDM/FFF 3D printers. It is produced with our patented Jam-Free™ technology that ensures consistent extrusion and prevents jams.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "http://www.polymaker.com/shop/polyplus-true-colour/", "author": { "author_id": "Polymaker", @@ -941,7 +941,7 @@ "display_name": "PolyWood™ PLA", "description": "PolyWood™ is a wood mimic printing material that contains no actual wood ensuring a clean Jam-Free™ printing experience.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "http://www.polymaker.com/shop/polywood/", "author": { "author_id": "Polymaker", @@ -958,7 +958,7 @@ "display_name": "Ultimaker ABS", "description": "Example package for material and quality profiles for Ultimaker materials.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com/products/materials/abs", "author": { "author_id": "Ultimaker", @@ -977,7 +977,7 @@ "display_name": "Ultimaker CPE", "description": "Example package for material and quality profiles for Ultimaker materials.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com/products/materials/abs", "author": { "author_id": "Ultimaker", @@ -996,7 +996,7 @@ "display_name": "Ultimaker Nylon", "description": "Example package for material and quality profiles for Ultimaker materials.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com/products/materials/abs", "author": { "author_id": "Ultimaker", @@ -1015,7 +1015,7 @@ "display_name": "Ultimaker PC", "description": "Example package for material and quality profiles for Ultimaker materials.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com/products/materials/pc", "author": { "author_id": "Ultimaker", @@ -1034,7 +1034,7 @@ "display_name": "Ultimaker PLA", "description": "Example package for material and quality profiles for Ultimaker materials.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com/products/materials/abs", "author": { "author_id": "Ultimaker", @@ -1053,7 +1053,7 @@ "display_name": "Ultimaker PVA", "description": "Example package for material and quality profiles for Ultimaker materials.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://ultimaker.com/products/materials/abs", "author": { "author_id": "Ultimaker", @@ -1072,7 +1072,7 @@ "display_name": "Vertex Delta ABS", "description": "ABS material and quality files for the Delta Vertex K8800.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://vertex3dprinter.eu", "author": { "author_id": "Velleman", @@ -1089,7 +1089,7 @@ "display_name": "Vertex Delta PET", "description": "ABS material and quality files for the Delta Vertex K8800.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://vertex3dprinter.eu", "author": { "author_id": "Velleman", @@ -1106,7 +1106,7 @@ "display_name": "Vertex Delta PLA", "description": "ABS material and quality files for the Delta Vertex K8800.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://vertex3dprinter.eu", "author": { "author_id": "Velleman", @@ -1123,7 +1123,7 @@ "display_name": "Vertex Delta TPU", "description": "ABS material and quality files for the Delta Vertex K8800.", "package_version": "1.0.0", - "cura_version": 4, + "sdk_version": 4, "website": "https://vertex3dprinter.eu", "author": { "author_id": "Velleman", @@ -1132,5 +1132,277 @@ "website": "https://www.vellemanprojects.eu" } } + }, + "ConsoleLogger": { + "package_info": { + "package_id": "ConsoleLogger", + "package_type": "plugin", + "display_name": "Console Logger", + "description": "Outputs log information to the console.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "OBJReader": { + "package_info": { + "package_id": "OBJReader", + "package_type": "plugin", + "display_name": "Wavefront OBJ Reader", + "description": "Makes it possible to read Wavefront OBJ files.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "OBJWriter": { + "package_info": { + "package_id": "OBJWriter", + "package_type": "plugin", + "display_name": "Wavefront OBJ Writer", + "description": "Makes it possible to write Wavefront OBJ files.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "STLReader": { + "package_info": { + "package_id": "STLReader", + "package_type": "plugin", + "display_name": "STL Reader", + "description": "Provides support for reading STL files.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "STLWriter": { + "package_info": { + "package_id": "STLWriter", + "package_type": "plugin", + "display_name": "STL Writer", + "description": "Provides support for writing STL files.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "FileLogger": { + "package_info": { + "package_id": "FileLogger", + "package_type": "plugin", + "display_name": "File Logger", + "description": "Outputs log information to a file in your settings folder.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "LocalContainerProvider": { + "package_info": { + "package_id": "LocalContainerProvider", + "package_type": "plugin", + "display_name": "Local Container Provider", + "description": "Provides built-in setting containers that come with the installation of the application.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "LocalFileOutputDevice": { + "package_info": { + "package_id": "LocalFileOutputDevice", + "package_type": "plugin", + "display_name": "Local File Output Device", + "description": "Enables saving to local files.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "CameraTool": { + "package_info": { + "package_id": "CameraTool", + "package_type": "plugin", + "display_name": "Camera Tool", + "description": "Provides the tool to manipulate the camera.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "MirrorTool": { + "package_info": { + "package_id": "MirrorTool", + "package_type": "plugin", + "display_name": "Mirror Tool", + "description": "Provides the Mirror tool.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "RotateTool": { + "package_info": { + "package_id": "RotateTool", + "package_type": "plugin", + "display_name": "Rotate Tool", + "description": "Provides the Rotate tool.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "ScaleTool": { + "package_info": { + "package_id": "ScaleTool", + "package_type": "plugin", + "display_name": "Scale Tool", + "description": "Provides the Scale tool.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "SelectionTool": { + "package_info": { + "package_id": "SelectionTool", + "package_type": "plugin", + "display_name": "Selection Tool", + "description": "Provides the Selection tool.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "TranslateTool": { + "package_info": { + "package_id": "TranslateTool", + "package_type": "plugin", + "display_name": "Move Tool", + "description": "Provides the Move tool.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "UpdateChecker": { + "package_info": { + "package_id": "UpdateChecker", + "package_type": "plugin", + "display_name": "Update Checker", + "description": "Checks for updates of the software.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, + "SimpleView": { + "package_info": { + "package_id": "SimpleView", + "package_type": "plugin", + "display_name": "Simple View", + "description": "Provides a simple solid mesh view.", + "package_version": "1.0.0", + "sdk_version": 4, + "website": "https://ultimaker.com", + "author": { + "author_id": "Ultimaker", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } } } diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 0d6c1eaa7b..73d842a077 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -1277,6 +1277,29 @@ } } }, + "wall_min_flow": + { + "label": "Minimum Wall Flow", + "description": "Minimum allowed percentage flow for a wall line. The wall overlap compensation reduces a wall's flow when it lies close to an existing wall. Walls whose flow is less than this value will be replaced with a travel move. When using this setting, you must enable the wall overlap compensation and print the outer wall before inner walls.", + "unit": "%", + "minimum_value": "0", + "maximum_value": "100", + "default_value": 0, + "type": "float", + "enabled": "travel_compensate_overlapping_walls_0_enabled or travel_compensate_overlapping_walls_x_enabled", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "wall_min_flow_retract": + { + "label": "Prefer Retract", + "description": "If enabled, retraction is used rather than combing for travel moves that replace walls whose flow is below the minimum flow threshold.", + "type": "bool", + "default_value": false, + "enabled": "(travel_compensate_overlapping_walls_0_enabled or travel_compensate_overlapping_walls_x_enabled) and wall_min_flow > 0", + "settable_per_mesh": false, + "settable_per_extruder": true + }, "fill_perimeter_gaps": { "label": "Fill Gaps Between Walls", @@ -1807,6 +1830,30 @@ "limit_to_extruder": "infill_extruder_nr", "settable_per_mesh": true }, + "infill_support_enabled": + { + "label": "Infill Support", + "description": "Print infill structures only where tops of the model should be supported. Enabling this reduces print time and material usage, but leads to ununiform object strength.", + "type": "bool", + "default_value": false, + "enabled": "infill_sparse_density > 0", + "limit_to_extruder": "infill_extruder_nr", + "settable_per_mesh": true + }, + "infill_support_angle": + { + "label": "Infill Overhang Angle", + "description": "The minimum angle of internal overhangs for which infill is added. At a value of 0° objects are totally filled with infill, 90° will not provide any infill.", + "unit": "°", + "type": "float", + "minimum_value": "0", + "minimum_value_warning": "2", + "maximum_value": "90", + "default_value": 40, + "enabled": "infill_sparse_density > 0 and infill_support_enabled", + "limit_to_extruder": "infill_extruder_nr", + "settable_per_mesh": true + }, "skin_preshrink": { "label": "Skin Removal Width", @@ -3597,7 +3644,7 @@ "description": "The extruder train to use for printing the support. This is used in multi-extrusion.", "type": "extruder", "default_value": "0", - "value": "-1", + "value": "defaultExtruderPosition()", "enabled": "(support_enable or support_tree_enable) and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false, @@ -4338,7 +4385,7 @@ "description": "The extruder train to use for printing the skirt/brim/raft. This is used in multi-extrusion.", "type": "extruder", "default_value": "0", - "value": "-1", + "value": "defaultExtruderPosition()", "enabled": "extruders_enabled_count > 1 and resolveOrValue('adhesion_type') != 'none'", "settable_per_mesh": false, "settable_per_extruder": false @@ -5772,16 +5819,6 @@ "limit_to_extruder": "infill_extruder_nr", "settable_per_mesh": true }, - "cross_infill_apply_pockets_alternatingly": - { - "label": "Alternate Cross 3D Pockets", - "description": "Only apply pockets at half of the four-way crossings in the cross 3D pattern and alternate the location of the pockets between heights where the pattern is touching itself.", - "type": "bool", - "default_value": true, - "enabled": "infill_pattern == 'cross_3d'", - "limit_to_extruder": "infill_extruder_nr", - "settable_per_mesh": true - }, "cross_infill_density_image": { "label": "Cross Infill Density Image", @@ -5798,7 +5835,7 @@ "description": "The file location of an image of which the brightness values determine the minimal density at the corresponding location in the support.", "type": "str", "default_value": "", - "enabled": "infill_pattern == 'cross' or infill_pattern == 'cross_3d'", + "enabled": "support_pattern == 'cross' or support_pattern == 'cross_3d'", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -5928,14 +5965,6 @@ "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": true }, - "infill_hollow": - { - "label": "Hollow Out Objects", - "description": "Remove all infill and make the inside of the object eligible for support.", - "type": "bool", - "default_value": false, - "settable_per_mesh": true - }, "magic_fuzzy_skin_enabled": { "label": "Fuzzy Skin", @@ -6672,14 +6701,6 @@ "type": "float", "enabled": "bridge_settings_enabled and bridge_enable_more_layers", "settable_per_mesh": true - }, - "wall_try_line_thickness": - { - "label": "Try Multiple Line Thicknesses", - "description": "When creating inner walls, try various line thicknesses to fit the wall lines better in narrow spaces. This reduces or increases the inner wall line width by up to 0.01mm.", - "default_value": false, - "type": "bool", - "settable_per_mesh": true } } }, diff --git a/resources/definitions/malyan_m180.def.json b/resources/definitions/malyan_m180.def.json index 11b61328ed..c74317a633 100644 --- a/resources/definitions/malyan_m180.def.json +++ b/resources/definitions/malyan_m180.def.json @@ -7,6 +7,7 @@ "visible": true, "author": "Ruben Dulek", "manufacturer": "Malyan", + "machine_x3g_variant": "r1d", "file_formats": "application/x3g" }, diff --git a/resources/definitions/ultimaker3.def.json b/resources/definitions/ultimaker3.def.json index 64948b4fe9..08fe01a76b 100644 --- a/resources/definitions/ultimaker3.def.json +++ b/resources/definitions/ultimaker3.def.json @@ -122,7 +122,7 @@ "raft_jerk": { "value": "jerk_layer_0" }, "raft_margin": { "value": "10" }, "raft_surface_layers": { "value": "1" }, - "retraction_amount": { "value": "2" }, + "retraction_amount": { "value": "6.5" }, "retraction_count_max": { "value": "10" }, "retraction_extrusion_window": { "value": "1" }, "retraction_hop": { "value": "2" }, diff --git a/resources/definitions/ultimaker_s5.def.json b/resources/definitions/ultimaker_s5.def.json index 8af77e22ac..f6971d0da3 100644 --- a/resources/definitions/ultimaker_s5.def.json +++ b/resources/definitions/ultimaker_s5.def.json @@ -119,7 +119,7 @@ "raft_margin": { "value": "10" }, "raft_speed": { "value": "25" }, "raft_surface_layers": { "value": "1" }, - "retraction_amount": { "value": "2" }, + "retraction_amount": { "value": "6.5" }, "retraction_count_max": { "value": "10" }, "retraction_extrusion_window": { "value": "1" }, "retraction_hop": { "value": "2" }, diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index dd5c60ea53..e8f832a733 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -323,10 +323,11 @@ UM.MainWindow { if (drop.urls.length > 0) { - // As the drop area also supports plugins, first check if it's a plugin that was dropped. - if (drop.urls.length == 1) + + var nonPackages = []; + for (var i = 0; i < drop.urls.length; i++) { - var filename = drop.urls[0]; + var filename = drop.urls[i]; if (filename.endsWith(".curapackage")) { // Try to install plugin & close. @@ -334,11 +335,13 @@ UM.MainWindow packageInstallDialog.text = catalog.i18nc("@label", "This package will be installed after restarting."); packageInstallDialog.icon = StandardIcon.Information; packageInstallDialog.open(); - return; + } + else + { + nonPackages.push(filename); } } - - openDialog.handleOpenFileUrls(drop.urls); + openDialog.handleOpenFileUrls(nonPackages); } } } diff --git a/resources/qml/JobSpecs.qml b/resources/qml/JobSpecs.qml index e7f2d304b5..3b10cf59d5 100644 --- a/resources/qml/JobSpecs.qml +++ b/resources/qml/JobSpecs.qml @@ -80,13 +80,10 @@ Item { property int unremovableSpacing: 5 text: PrintInformation.jobName horizontalAlignment: TextInput.AlignRight - onTextChanged: { - PrintInformation.setJobName(text); - } onEditingFinished: { - if (printJobTextfield.text != ''){ - printJobTextfield.focus = false; - } + var new_name = text == "" ? "unnamed" : text; + PrintInformation.setJobName(new_name, true); + printJobTextfield.focus = false; } validator: RegExpValidator { regExp: /^[^\\ \/ \*\?\|\[\]]*$/ diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationItem.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationItem.qml index be8c8bcb45..6f0130d5ca 100644 --- a/resources/qml/Menus/ConfigurationMenu/ConfigurationItem.qml +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationItem.qml @@ -92,6 +92,7 @@ Rectangle anchors.verticalCenter: buildplateIcon.verticalCenter anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").height / 2) text: configuration.buildplateConfiguration + renderType: Text.NativeRendering color: textColor } } diff --git a/resources/qml/Menus/ConfigurationMenu/PrintCoreConfiguration.qml b/resources/qml/Menus/ConfigurationMenu/PrintCoreConfiguration.qml index ca1b666e69..97b5bee745 100644 --- a/resources/qml/Menus/ConfigurationMenu/PrintCoreConfiguration.qml +++ b/resources/qml/Menus/ConfigurationMenu/PrintCoreConfiguration.qml @@ -26,6 +26,7 @@ Column { id: extruderLabel text: catalog.i18nc("@label:extruder label", "Extruder") + renderType: Text.NativeRendering elide: Text.ElideRight anchors.left: parent.left font: UM.Theme.getFont("default") @@ -59,6 +60,7 @@ Column id: extruderNumberText anchors.centerIn: parent text: printCoreConfiguration.position + 1 + renderType: Text.NativeRendering font: UM.Theme.getFont("default") color: mainColor } @@ -69,6 +71,7 @@ Column { id: materialLabel text: printCoreConfiguration.material.name + renderType: Text.NativeRendering elide: Text.ElideRight width: parent.width font: UM.Theme.getFont("default_bold") @@ -79,6 +82,7 @@ Column { id: printCoreTypeLabel text: printCoreConfiguration.hotendID + renderType: Text.NativeRendering elide: Text.ElideRight width: parent.width font: UM.Theme.getFont("default") diff --git a/resources/qml/Menus/ConfigurationMenu/SyncButton.qml b/resources/qml/Menus/ConfigurationMenu/SyncButton.qml index 078acb65b2..3099d684c1 100644 --- a/resources/qml/Menus/ConfigurationMenu/SyncButton.qml +++ b/resources/qml/Menus/ConfigurationMenu/SyncButton.qml @@ -13,7 +13,7 @@ Button id: base property var outputDevice: null property var matched: updateOnSync() - text: matched == true ? "Yes" : "No" + text: matched == true ? catalog.i18nc("@label:extruder label", "Yes") : catalog.i18nc("@label:extruder label", "No") width: parent.width height: parent.height diff --git a/resources/qml/Menus/MaterialMenu.qml b/resources/qml/Menus/MaterialMenu.qml index d81e0c86c3..64b3130724 100644 --- a/resources/qml/Menus/MaterialMenu.qml +++ b/resources/qml/Menus/MaterialMenu.qml @@ -63,8 +63,7 @@ Menu exclusiveGroup: group onTriggered: { - var activeExtruderIndex = Cura.ExtruderManager.activeExtruderIndex; - Cura.MachineManager.setMaterial(activeExtruderIndex, model.container_node); + Cura.MachineManager.setMaterial(extruderIndex, model.container_node); } } onObjectAdded: brandMaterialsMenu.insertItem(index, object) diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml index ceb2ed12be..ad91f2ee9a 100644 --- a/resources/qml/Preferences/MaterialView.qml +++ b/resources/qml/Preferences/MaterialView.qml @@ -404,10 +404,17 @@ TabView id: spinBox anchors.left: label.right value: { + // In case the setting is not in the material... if (!isNaN(parseFloat(materialPropertyProvider.properties.value))) { return parseFloat(materialPropertyProvider.properties.value); } + // ... we search in the variant, and if it is not there... + if (!isNaN(parseFloat(variantPropertyProvider.properties.value))) + { + return parseFloat(variantPropertyProvider.properties.value); + } + // ... then look in the definition container. if (!isNaN(parseFloat(machinePropertyProvider.properties.value))) { return parseFloat(machinePropertyProvider.properties.value); @@ -431,6 +438,13 @@ TabView key: model.key } UM.ContainerPropertyProvider + { + id: variantPropertyProvider + containerId: Cura.MachineManager.activeVariantId + watchedProperties: [ "value" ] + key: model.key + } + UM.ContainerPropertyProvider { id: machinePropertyProvider containerId: Cura.MachineManager.activeDefinitionId diff --git a/resources/qml/Settings/SettingExtruder.qml b/resources/qml/Settings/SettingExtruder.qml index 38b1c2cab0..2239628e3f 100644 --- a/resources/qml/Settings/SettingExtruder.qml +++ b/resources/qml/Settings/SettingExtruder.qml @@ -93,14 +93,7 @@ SettingItem { target: control property: "currentIndex" - value: - { - if(propertyProvider.properties.value == -1) - { - return control.getIndexByPosition(Cura.MachineManager.defaultExtruderPosition); - } - return propertyProvider.properties.value - } + value: control.getIndexByPosition(propertyProvider.properties.value) // Sometimes when the value is already changed, the model is still being built. // The when clause ensures that the current index is not updated when this happens. when: control.model.items.length > 0 diff --git a/resources/qml/SidebarSimple.qml b/resources/qml/SidebarSimple.qml index cbf2ac227a..4b229d9807 100644 --- a/resources/qml/SidebarSimple.qml +++ b/resources/qml/SidebarSimple.qml @@ -57,7 +57,8 @@ Item interval: 50 running: false repeat: false - onTriggered: { + onTriggered: + { var item = Cura.QualityProfilesDropDownMenuModel.getItem(qualitySlider.value); Cura.MachineManager.activeQualityGroup = item.quality_group; } @@ -77,7 +78,8 @@ Item { // update needs to be called when the widgets are visible, otherwise the step width calculation // will fail because the width of an invisible item is 0. - if (visible) { + if (visible) + { qualityModel.update(); } } @@ -97,24 +99,30 @@ Item property var qualitySliderAvailableMax: 0 property var qualitySliderMarginRight: 0 - function update () { + function update () + { reset() var availableMin = -1 var availableMax = -1 - for (var i = 0; i < Cura.QualityProfilesDropDownMenuModel.rowCount(); i++) { + for (var i = 0; i < Cura.QualityProfilesDropDownMenuModel.rowCount(); i++) + { var qualityItem = Cura.QualityProfilesDropDownMenuModel.getItem(i) // Add each quality item to the UI quality model qualityModel.append(qualityItem) // Set selected value - if (Cura.MachineManager.activeQualityType == qualityItem.quality_type) { + if (Cura.MachineManager.activeQualityType == qualityItem.quality_type) + { // set to -1 when switching to user created profile so all ticks are clickable - if (Cura.SimpleModeSettingsManager.isProfileUserCreated) { + if (Cura.SimpleModeSettingsManager.isProfileUserCreated) + { qualityModel.qualitySliderActiveIndex = -1 - } else { + } + else + { qualityModel.qualitySliderActiveIndex = i } @@ -122,18 +130,21 @@ Item } // Set min available - if (qualityItem.available && availableMin == -1) { + if (qualityItem.available && availableMin == -1) + { availableMin = i } // Set max available - if (qualityItem.available) { + if (qualityItem.available) + { availableMax = i } } // Set total available ticks for active slider part - if (availableMin != -1) { + if (availableMin != -1) + { qualityModel.availableTotalTicks = availableMax - availableMin + 1 } @@ -145,16 +156,23 @@ Item qualityModel.qualitySliderAvailableMax = availableMax } - function calculateSliderStepWidth (totalTicks) { + function calculateSliderStepWidth (totalTicks) + { qualityModel.qualitySliderStepWidth = totalTicks != 0 ? Math.round((base.width * 0.55) / (totalTicks)) : 0 } - function calculateSliderMargins (availableMin, availableMax, totalTicks) { - if (availableMin == -1 || (availableMin == 0 && availableMax == 0)) { + function calculateSliderMargins (availableMin, availableMax, totalTicks) + { + if (availableMin == -1 || (availableMin == 0 && availableMax == 0)) + { qualityModel.qualitySliderMarginRight = Math.round(base.width * 0.55) - } else if (availableMin == availableMax) { + } + else if (availableMin == availableMax) + { qualityModel.qualitySliderMarginRight = Math.round((totalTicks - availableMin) * qualitySliderStepWidth) - } else { + } + else + { qualityModel.qualitySliderMarginRight = Math.round((totalTicks - availableMax) * qualitySliderStepWidth) } } @@ -215,16 +233,24 @@ Item return result } - x: { + x: + { // Make sure the text aligns correctly with each tick - if (qualityModel.totalTicks == 0) { + if (qualityModel.totalTicks == 0) + { // If there is only one tick, align it centrally return Math.round(((base.width * 0.55) - width) / 2) - } else if (index == 0) { + } + else if (index == 0) + { return Math.round(base.width * 0.55 / qualityModel.totalTicks) * index - } else if (index == qualityModel.totalTicks) { + } + else if (index == qualityModel.totalTicks) + { return Math.round(base.width * 0.55 / qualityModel.totalTicks) * index - width - } else { + } + else + { return Math.round((base.width * 0.55 / qualityModel.totalTicks) * index - (width / 2)) } } @@ -291,7 +317,8 @@ Item Rectangle { id: rightArea - width: { + width: + { if(qualityModel.availableTotalTicks == 0) return 0 @@ -299,8 +326,10 @@ Item } height: parent.height color: "transparent" - x: { - if (qualityModel.availableTotalTicks == 0) { + x: + { + if (qualityModel.availableTotalTicks == 0) + { return 0 } @@ -310,7 +339,8 @@ Item return totalGap } - MouseArea { + MouseArea + { anchors.fill: parent hoverEnabled: true enabled: Cura.SimpleModeSettingsManager.isProfileUserCreated == false @@ -373,13 +403,16 @@ Item style: SliderStyle { //Draw Available line - groove: Rectangle { + groove: Rectangle + { implicitHeight: 2 * screenScaleFactor color: UM.Theme.getColor("quality_slider_available") radius: Math.round(height / 2) } - handle: Item { - Rectangle { + handle: Item + { + Rectangle + { id: qualityhandleButton anchors.centerIn: parent color: UM.Theme.getColor("quality_slider_available") @@ -391,11 +424,14 @@ Item } } - onValueChanged: { + onValueChanged: + { // only change if an active machine is set and the slider is visible at all. - if (Cura.MachineManager.activeMachine != null && visible) { + if (Cura.MachineManager.activeMachine != null && visible) + { // prevent updating during view initializing. Trigger only if the value changed by user - if (qualitySlider.value != qualityModel.qualitySliderActiveIndex && qualityModel.qualitySliderActiveIndex != -1) { + if (qualitySlider.value != qualityModel.qualitySliderActiveIndex && qualityModel.qualitySliderActiveIndex != -1) + { // start updating with short delay qualitySliderChangeTimer.start() } @@ -587,8 +623,10 @@ Item // same operation var active_mode = UM.Preferences.getValue("cura/active_mode") - if (active_mode == 0 || active_mode == "simple") { + if (active_mode == 0 || active_mode == "simple") + { Cura.MachineManager.setSettingForAllExtruders("infill_sparse_density", "value", roundedSliderValue) + Cura.MachineManager.resetSettingForAllExtruders("infill_line_distance") } } diff --git a/resources/qml/WorkspaceSummaryDialog.qml b/resources/qml/WorkspaceSummaryDialog.qml index 0869d7e698..079d840ae7 100644 --- a/resources/qml/WorkspaceSummaryDialog.qml +++ b/resources/qml/WorkspaceSummaryDialog.qml @@ -148,9 +148,22 @@ UM.Dialog { height: childrenRect.height width: parent.width - Label + Label { - text: catalog.i18nc("@action:label", "Extruder %1").arg(modelData) + text: { + var extruder = Number(modelData) + var extruder_id = "" + if(!isNaN(extruder)) + { + extruder_id = extruder + 1 // The extruder counter start from One and not Zero + } + else + { + extruder_id = modelData + } + + return catalog.i18nc("@action:label", "Extruder %1").arg(extruder_id) + } font.bold: true } Row diff --git a/resources/setting_visibility/basic.cfg b/resources/setting_visibility/basic.cfg index 4196a3a9e7..82045db93b 100644 --- a/resources/setting_visibility/basic.cfg +++ b/resources/setting_visibility/basic.cfg @@ -34,6 +34,7 @@ retraction_hop_enabled [cooling] cool_fan_enabled +cool_fan_speed [support] support_enable diff --git a/resources/setting_visibility/expert.cfg b/resources/setting_visibility/expert.cfg index 6d6b84883c..be0950ec6f 100644 --- a/resources/setting_visibility/expert.cfg +++ b/resources/setting_visibility/expert.cfg @@ -87,6 +87,7 @@ gradual_infill_steps gradual_infill_step_height infill_before_walls min_infill_area +infill_support_enabled skin_preshrink top_skin_preshrink bottom_skin_preshrink @@ -369,7 +370,6 @@ spaghetti_infill_extra_volume support_conical_enabled support_conical_angle support_conical_min_width -infill_hollow magic_fuzzy_skin_enabled magic_fuzzy_skin_thickness magic_fuzzy_skin_point_density diff --git a/resources/themes/cura-light/styles.qml b/resources/themes/cura-light/styles.qml index 14e7d196a7..b71ddd2d86 100755 --- a/resources/themes/cura-light/styles.qml +++ b/resources/themes/cura-light/styles.qml @@ -532,7 +532,7 @@ QtObject { SequentialAnimation on x { id: xAnim - property int animEndPoint: Theme.getSize("message").width - (Theme.getSize("default_margin").width * 2) - Theme.getSize("progressbar_control").width + property int animEndPoint: Theme.getSize("message").width - Math.round((Theme.getSize("default_margin").width * 2.5)) - Theme.getSize("progressbar_control").width running: control.indeterminate && control.visible loops: Animation.Infinite NumberAnimation { from: 0; to: xAnim.animEndPoint; duration: 2000;}