diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index b825e456c2..9261f0df1a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -279,6 +279,11 @@ class CuraApplication(QtApplication): # We need them to simplify the switching between materials. empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer() + 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) + empty_variant_container = copy.deepcopy(empty_container) empty_variant_container.setMetaDataEntry("id", "empty_variant") empty_variant_container.addMetaDataEntry("type", "variant") diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index b945ec0609..4567226060 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -245,6 +245,9 @@ class CuraContainerRegistry(ContainerRegistry): return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())} + # This message is throw when the profile reader doesn't find any profile in the file + return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)} + # If it hasn't returned by now, none of the plugins loaded the profile successfully. return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)} diff --git a/cura/Settings/CuraStackBuilder.py b/cura/Settings/CuraStackBuilder.py index 8d2d66ea87..608a13c600 100644 --- a/cura/Settings/CuraStackBuilder.py +++ b/cura/Settings/CuraStackBuilder.py @@ -63,6 +63,7 @@ class CuraStackBuilder: next_stack = new_global_stack ) new_global_stack.addExtruder(new_extruder) + registry.addContainer(new_extruder) else: # create extruder stack for each found extruder definition for extruder_definition in registry.findDefinitionContainers(machine = machine_definition.id): @@ -81,6 +82,11 @@ class CuraStackBuilder: next_stack = new_global_stack ) new_global_stack.addExtruder(new_extruder) + registry.addContainer(new_extruder) + + # Register the global stack after the extruder stacks are created. This prevents the registry from adding another + # extruder stack because the global stack didn't have one yet (which is enforced since Cura 3.1). + registry.addContainer(new_global_stack) return new_global_stack @@ -135,9 +141,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. - registry = ContainerRegistry.getInstance() - registry.addContainer(stack) - registry.addContainer(user_container) + ContainerRegistry.getInstance().addContainer(user_container) return stack @@ -181,9 +185,7 @@ class CuraStackBuilder: if "quality_changes" in kwargs: stack.setQualityChangesById(kwargs["quality_changes"]) - registry = ContainerRegistry.getInstance() - registry.addContainer(stack) - registry.addContainer(user_container) + ContainerRegistry.getInstance().addContainer(user_container) return stack diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index b5f9a35914..6d52ce87fd 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -407,6 +407,12 @@ class ExtruderManager(QObject): extruder_train.setNextStack(global_stack) extruders_changed = True + # FIX: We have to remove those settings here because we know that those values have been copied to all + # the extruders at this point. + for key in ("material_diameter", "machine_nozzle_size"): + if global_stack.definitionChanges.hasProperty(key, "value"): + global_stack.definitionChanges.removeInstance(key, postpone_emit = True) + if extruders_changed: self.extrudersChanged.emit(global_stack_id) self.extrudersAdded.emit() diff --git a/cura/Settings/ExtruderStack.py b/cura/Settings/ExtruderStack.py index 6dffeda6c2..dd03ce7b3b 100644 --- a/cura/Settings/ExtruderStack.py +++ b/cura/Settings/ExtruderStack.py @@ -18,10 +18,6 @@ if TYPE_CHECKING: from cura.Settings.GlobalStack import GlobalStack -_EXTRUDER_SPECIFIC_DEFINITION_CHANGES_SETTINGS = ["machine_nozzle_size", - "material_diameter"] - - ## Represents an Extruder and its related containers. # # @@ -53,20 +49,38 @@ class ExtruderStack(CuraContainerStack): # when we are upgrading a definition_changes container file, there is NO guarantee that other files such as # machine an extruder stack files are upgraded before this, so we cannot read those files assuming they are in # the latest format. + # + # MORE: + # For single-extrusion machines, nozzle size is saved in the global stack, so the nozzle size value should be + # carried to the first extruder. + # For material diameter, it was supposed to be applied to all extruders, so its value should be copied to all + # extruders. + # + keys_to_copy = ["material_diameter"] # material diameter will be copied to all extruders if self.getMetaDataEntry("position") == "0": - for key in _EXTRUDER_SPECIFIC_DEFINITION_CHANGES_SETTINGS: - setting_value = stack.definitionChanges.getProperty(key, "value") - if setting_value is None: - continue + keys_to_copy.append("machine_nozzle_size") - setting_definition = stack.getSettingDefinition(key) - new_instance = SettingInstance(setting_definition, self.definitionChanges) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - self.definitionChanges.addInstance(new_instance) - self.definitionChanges.setDirty(True) + for key in keys_to_copy: + # Only copy the value when this extruder doesn't have the value. + if self.definitionChanges.hasProperty(key, "value"): + continue + setting_value = stack.definitionChanges.getProperty(key, "value") + if setting_value is None: + continue - stack.definitionChanges.removeInstance(key, postpone_emit = True) + setting_definition = stack.getSettingDefinition(key) + new_instance = SettingInstance(setting_definition, self.definitionChanges) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + self.definitionChanges.addInstance(new_instance) + self.definitionChanges.setDirty(True) + + # NOTE: We cannot remove the setting from the global stack's definition changes container because for + # material diameter, it needs to be applied to all extruders, but here we don't know how many extruders + # a machine actually has and how many extruders has already been loaded for that machine, so we have to + # keep this setting for any remaining extruders that haven't been loaded yet. + # + # Those settings will be removed in ExtruderManager which knows all those info. @override(ContainerStack) def getNextStack(self) -> Optional["GlobalStack"]: diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 309f27b6d7..b98fcccbb2 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -3,6 +3,8 @@ #Type hinting. from typing import Union, List, Dict + +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Signal import Signal from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer @@ -75,6 +77,7 @@ class MachineManager(QObject): self._stacks_have_errors = None + self._empty_definition_changes_container = ContainerRegistry.getInstance().findContainers(id = "empty_definition_changes")[0] self._empty_variant_container = ContainerRegistry.getInstance().findContainers(id = "empty_variant")[0] self._empty_material_container = ContainerRegistry.getInstance().findContainers(id = "empty_material")[0] self._empty_quality_container = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0] @@ -1348,6 +1351,68 @@ class MachineManager(QObject): if containers: return containers[0].definition.getId() + ## 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): + extruder_manager = Application.getInstance().getExtruderManager() + + definition_changes_container = self._global_container_stack.definitionChanges + if not self._global_container_stack or definition_changes_container == self._empty_definition_changes_container: + return + + previous_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") + if extruder_count == previous_extruder_count: + return + + # reset all extruder number settings whose value is no longer valid + for setting_instance in self._global_container_stack.userChanges.findInstances(): + setting_key = setting_instance.definition.key + if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"): + continue + + old_value = int(self._global_container_stack.userChanges.getProperty(setting_key, "value")) + if old_value >= extruder_count: + self._global_container_stack.userChanges.removeInstance(setting_key) + Logger.log("d", "Reset [%s] because its old value [%s] is no longer valid ", setting_key, old_value) + + # 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() + for node in DepthFirstIterator(root_node): + if node.getMeshData(): + extruder_nr = node.callDecoration("getActiveExtruderPosition") + + if extruder_nr is not None and int(extruder_nr) > extruder_count - 1: + node.callDecoration("setActiveExtruder", extruder_manager.getExtruderStack(extruder_count - 1).getId()) + + definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count) + + # Make sure one of the extruder stacks is active + extruder_manager.setActiveExtruderIndex(0) + + # Move settable_per_extruder values out of the global container + # After CURA-4482 this should not be the case anymore, but we still want to support older project files. + global_user_container = self._global_container_stack.getTop() + + # Make sure extruder_stacks exists + extruder_stacks = [] + + if previous_extruder_count == 1: + extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks() + global_user_container = self._global_container_stack.getTop() + + for setting_instance in global_user_container.findInstances(): + setting_key = setting_instance.definition.key + settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder") + + if settable_per_extruder: + limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder")) + extruder_stack = extruder_stacks[max(0, limit_to_extruder)] + extruder_stack.getTop().setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value")) + global_user_container.removeInstance(setting_key) + + # Signal that the global stack has changed + Application.getInstance().globalContainerStackChanged.emit() + @staticmethod def createMachineManager(): return MachineManager() diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 826b488e02..5418dcef6d 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -390,6 +390,13 @@ UM.Dialog } } + function accept() { + manager.closeBackend(); + manager.onOkButtonClicked(); + base.visible = false; + base.accept(); + } + function reject() { manager.onCancelButtonClicked(); base.visible = false; diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 06d38eccd0..7c46b8edaa 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -431,7 +431,6 @@ class CuraEngineBackend(QObject, Backend): # cached layer data is removed so the previous data is not rendered - CURA-4821 if source.callDecoration("isBlockSlicing") and source.callDecoration("getLayerData"): if self._stored_optimized_layer_data: - print(self._stored_optimized_layer_data) del self._stored_optimized_layer_data[source.callDecoration("getBuildPlateNumber")] build_plate_changed = set() diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py index 458aca5787..8e4c70517f 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py @@ -49,7 +49,6 @@ class FirmwareUpdateChecker(Extension): def _onContainerAdded(self, container): # Only take care when a new GlobalStack was added if isinstance(container, GlobalStack): - Logger.log("i", "You have a '%s' in printer list. Let's check the firmware!", container.getId()) self.checkFirmwareVersion(container, True) ## Connect with software.ultimaker.com, load latest.version and check version info. diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py index 6dd7338cfd..fd6c4680e8 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py @@ -44,6 +44,8 @@ class FirmwareUpdateCheckerJob(Job): # Now we just do that if the active printer is Ultimaker 3 or Ultimaker 3 Extended or any # other Ultimaker 3 that will come in the future if len(machine_name_parts) >= 2 and machine_name_parts[:2] == ["ultimaker", "3"]: + Logger.log("i", "You have a UM3 in printer list. Let's check the firmware!") + # Nothing to parse, just get the string # TODO: In the future may be done by parsing a JSON file with diferent version for each printer model current_version = reader(current_version_file).readline().rstrip() diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index 95c48c4d9e..2dfaf5aef7 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -107,7 +107,7 @@ class GCodeWriter(MeshWriter): prefix_length = len(prefix) container_with_profile = stack.qualityChanges - if not container_with_profile: + if container_with_profile.getId() == "empty_quality_changes": Logger.log("e", "No valid quality profile found, not writing settings to GCode!") return "" @@ -123,9 +123,9 @@ class GCodeWriter(MeshWriter): serialized = flat_global_container.serialize() data = {"global_quality": serialized} - for extruder in sorted(ExtruderManager.getInstance().getMachineExtruders(stack.getId()), key = lambda k: k.getMetaDataEntry("position")): + for extruder in sorted(stack.extruders.values(), key = lambda k: k.getMetaDataEntry("position")): extruder_quality = extruder.qualityChanges - if not extruder_quality: + if extruder_quality.getId() == "empty_quality_changes": Logger.log("w", "No extruder quality profile found, not writing quality for extruder %s to file!", extruder.getId()) continue flat_extruder_quality = self._createFlattenedContainerInstance(extruder.getTop(), extruder_quality) diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index 101ba54ed0..ae1c1663dd 100755 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -105,60 +105,9 @@ class MachineSettingsAction(MachineAction): @pyqtSlot(int) def setMachineExtruderCount(self, extruder_count): - extruder_manager = Application.getInstance().getExtruderManager() - - definition_changes_container = self._global_container_stack.definitionChanges - if not self._global_container_stack or definition_changes_container == self._empty_container: - return - - previous_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") - if extruder_count == previous_extruder_count: - return - - # reset all extruder number settings whose value is no longer valid - for setting_instance in self._global_container_stack.userChanges.findInstances(): - setting_key = setting_instance.definition.key - if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"): - continue - - old_value = int(self._global_container_stack.userChanges.getProperty(setting_key, "value")) - if old_value >= extruder_count: - self._global_container_stack.userChanges.removeInstance(setting_key) - Logger.log("d", "Reset [%s] because its old value [%s] is no longer valid ", setting_key, old_value) - - # 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() - for node in DepthFirstIterator(root_node): - if node.getMeshData(): - extruder_nr = node.callDecoration("getActiveExtruderPosition") - - if extruder_nr is not None and int(extruder_nr) > extruder_count - 1: - node.callDecoration("setActiveExtruder", extruder_manager.getExtruderStack(extruder_count - 1).getId()) - - definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count) - - # Make sure one of the extruder stacks is active - extruder_manager.setActiveExtruderIndex(0) - - # Move settable_per_extruder values out of the global container - # After CURA-4482 this should not be the case anymore, but we still want to support older project files. - global_user_container = self._global_container_stack.getTop() - - if previous_extruder_count == 1: - extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks() - global_user_container = self._global_container_stack.getTop() - - for setting_instance in global_user_container.findInstances(): - setting_key = setting_instance.definition.key - settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder") - - if settable_per_extruder: - limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder")) - extruder_stack = extruder_stacks[max(0, limit_to_extruder)] - extruder_stack.getTop().setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value")) - global_user_container.removeInstance(setting_key) - - self.forceUpdate() + # Note: this method was in this class before, but since it's quite generic and other plugins also need it + # it was moved to the machine manager instead. Now this method just calls the machine manager. + Application.getInstance().getMachineManager().setActiveMachineExtruderCount(extruder_count) @pyqtSlot() def forceUpdate(self): diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.qml b/plugins/MachineSettingsAction/MachineSettingsAction.qml index b815d71acc..536c6c30d6 100644 --- a/plugins/MachineSettingsAction/MachineSettingsAction.qml +++ b/plugins/MachineSettingsAction/MachineSettingsAction.qml @@ -390,7 +390,7 @@ Cura.MachineAction visible: Cura.MachineManager.hasMaterials sourceComponent: numericTextFieldWithUnit property string settingKey: "material_diameter" - property string label: catalog.i18nc("@label", "Material diameter") + property string label: catalog.i18nc("@label", "Compatible material diameter") property string unit: catalog.i18nc("@label", "mm") property string tooltip: catalog.i18nc("@tooltip", "The nominal diameter of filament supported by the printer. The exact diameter will be overridden by the material and/or the profile.") function afterOnEditingFinished() diff --git a/resources/qml/AskOpenAsProjectOrModelsDialog.qml b/resources/qml/AskOpenAsProjectOrModelsDialog.qml index a53c711f9d..bd37d1acdb 100644 --- a/resources/qml/AskOpenAsProjectOrModelsDialog.qml +++ b/resources/qml/AskOpenAsProjectOrModelsDialog.qml @@ -26,33 +26,54 @@ UM.Dialog minimumHeight: maximumHeight minimumWidth: maximumWidth - modality: UM.Application.platform == "linux" ? Qt.NonModal : Qt.WindowModal; + modality: UM.Application.platform == "linux" ? Qt.NonModal : Qt.WindowModal property var fileUrl - function loadProjectFile(projectFile) - { - UM.WorkspaceFileHandler.readLocalFile(projectFile); + // load the entire project + function loadProjectFile() { - var meshName = backgroundItem.getMeshName(projectFile.toString()); - backgroundItem.hasMesh(decodeURIComponent(meshName)); - } - - function loadModelFiles(fileUrls) - { - for (var i in fileUrls) - { - CuraApplication.readLocalFile(fileUrls[i]); + // update preference + if (rememberChoiceCheckBox.checked) { + UM.Preferences.setValue("cura/choice_on_open_project", "open_as_project") } - var meshName = backgroundItem.getMeshName(fileUrls[0].toString()); - backgroundItem.hasMesh(decodeURIComponent(meshName)); + UM.WorkspaceFileHandler.readLocalFile(base.fileUrl) + var meshName = backgroundItem.getMeshName(base.fileUrl.toString()) + backgroundItem.hasMesh(decodeURIComponent(meshName)) + + base.hide() } - onVisibleChanged: - { - if (visible) - { + // load the project file as separated models + function loadModelFiles() { + + // update preference + if (rememberChoiceCheckBox.checked) { + UM.Preferences.setValue("cura/choice_on_open_project", "open_as_model") + } + + CuraApplication.readLocalFile(base.fileUrl) + var meshName = backgroundItem.getMeshName(base.fileUrl.toString()) + backgroundItem.hasMesh(decodeURIComponent(meshName)) + + base.hide() + } + + // override UM.Dialog accept + function accept () { + var openAsPreference = UM.Preferences.getValue("cura/choice_on_open_project") + + // when hitting 'enter', we always open as project unless open_as_model was explicitly stored as preference + if (openAsPreference == "open_as_model") { + loadModelFiles() + } else { + loadProjectFile() + } + } + + onVisibleChanged: { + if (visible) { var rememberMyChoice = UM.Preferences.getValue("cura/choice_on_open_project") != "always_ask"; rememberChoiceCheckBox.checked = rememberMyChoice; } @@ -90,47 +111,26 @@ UM.Dialog } // Buttons - Item - { + Item { id: buttonBar anchors.right: parent.right anchors.left: parent.left height: childrenRect.height - Button - { + Button { id: openAsProjectButton - text: catalog.i18nc("@action:button", "Open as project"); + text: catalog.i18nc("@action:button", "Open as project") anchors.right: importModelsButton.left anchors.rightMargin: UM.Theme.getSize("default_margin").width isDefault: true - onClicked: - { - // update preference - if (rememberChoiceCheckBox.checked) - UM.Preferences.setValue("cura/choice_on_open_project", "open_as_project"); - - // load this file as project - base.hide(); - loadProjectFile(base.fileUrl); - } + onClicked: loadProjectFile() } - Button - { + Button { id: importModelsButton - text: catalog.i18nc("@action:button", "Import models"); + text: catalog.i18nc("@action:button", "Import models") anchors.right: parent.right - onClicked: - { - // update preference - if (rememberChoiceCheckBox.checked) - UM.Preferences.setValue("cura/choice_on_open_project", "open_as_model"); - - // load models from this project file - base.hide(); - loadModelFiles([base.fileUrl]); - } + onClicked: loadModelFiles() } } }