From ab0a52063d0d0f327b7b7c4ccbc28d35dd61feb1 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 2 Feb 2024 16:05:36 +0100 Subject: [PATCH] Now loading user settings CURA-11561 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 222 ++++++++++++-------- plugins/3MFReader/WorkspaceDialog.py | 29 ++- plugins/3MFReader/WorkspaceDialog.qml | 15 +- plugins/3MFWriter/SettingsExportModel.py | 2 +- 4 files changed, 178 insertions(+), 90 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 76cd1f386b..3398b2e7d5 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -5,6 +5,7 @@ from configparser import ConfigParser import zipfile import os import json +import re from typing import cast, Dict, List, Optional, Tuple, Any, Set import xml.etree.ElementTree as ET @@ -141,10 +142,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._old_new_materials: Dict[str, str] = {} self._machine_info = None + self._load_profile = False + def _clearState(self): self._id_mapping = {} self._old_new_materials = {} self._machine_info = None + self._load_profile = False def getNewId(self, old_id: str): """Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results. @@ -228,7 +232,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._resolve_strategies = {k: None for k in resolve_strategy_keys} containers_found_dict = {k: False for k in resolve_strategy_keys} - # Check whether the file is a PCB + # Check whether the file is a PCB, which changes some import options is_pcb = file_name.endswith('.pcb') # @@ -621,6 +625,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity) self._dialog.setMissingPackagesMetadata(missing_package_metadata) self._dialog.setHasVisibleSelectSameProfileChanged(is_pcb) + self._dialog.setAllowCreatemachine(not is_pcb) self._dialog.show() # Choosing the initially selected printer in MachineSelector @@ -652,6 +657,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setIsNetworkedMachine(is_networked_machine) self._dialog.setIsAbstractMachine(is_abstract_machine) self._dialog.setMachineName(machine_name) + self._dialog.updateCompatibleMachine() + self._dialog.setSelectSameProfileChecked(self._dialog.isCompatibleMachine) # Block until the dialog is closed. self._dialog.waitForClose() @@ -659,6 +666,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled + self._load_profile = not is_pcb or self._dialog.selectSameProfileChecked + self._resolve_strategies = self._dialog.getResult() # # There can be 3 resolve strategies coming from the dialog: @@ -694,16 +703,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader): except EnvironmentError as e: message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags or !", "Project file {0} is suddenly inaccessible: {1}.", file_name, str(e)), - title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"), - message_type = Message.MessageType.ERROR) + title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"), + message_type = Message.MessageType.ERROR) message.show() self.setWorkspaceName("") return [], {} except zipfile.BadZipFile as e: message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags or !", "Project file {0} is corrupt: {1}.", file_name, str(e)), - title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"), - message_type = Message.MessageType.ERROR) + title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"), + message_type = Message.MessageType.ERROR) message.show() self.setWorkspaceName("") return [], {} @@ -765,9 +774,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Find the machine which will be overridden global_stacks = self._container_registry.findContainerStacks(id = self._dialog.getMachineToOverride(), type = "machine") if not global_stacks: - message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag !", + message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag !", "Project file {0} is made using profiles that are unknown to this version of UltiMaker Cura.", file_name), - message_type = Message.MessageType.ERROR) + message_type = Message.MessageType.ERROR) message.show() self.setWorkspaceName("") return [], {} @@ -781,84 +790,107 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for stack in extruder_stacks: stack.setNextStack(global_stack, connect_signals = False) - Logger.log("d", "Workspace loading is checking definitions...") - # Get all the definition files & check if they exist. If not, add them. - definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] - for definition_container_file in definition_container_files: - container_id = self._stripFileToId(definition_container_file) + user_settings = {} - definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id) - if not definitions: - definition_container = DefinitionContainer(container_id) - try: - definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"), - file_name = definition_container_file) - except ContainerFormatError: - # We cannot just skip the definition file because everything else later will just break if the - # machine definition cannot be found. - Logger.logException("e", "Failed to deserialize definition file %s in project file %s", - definition_container_file, file_name) - definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults. - self._container_registry.addContainer(definition_container) - Job.yieldThread() - QCoreApplication.processEvents() # Ensure that the GUI does not freeze. + if self._load_profile: + Logger.log("d", "Workspace loading is checking definitions...") + # Get all the definition files & check if they exist. If not, add them. + definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] + for definition_container_file in definition_container_files: + container_id = self._stripFileToId(definition_container_file) - Logger.log("d", "Workspace loading is checking materials...") - # Get all the material files and check if they exist. If not, add them. - xml_material_profile = self._getXmlProfileClass() - if self._material_container_suffix is None: - self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0] - if xml_material_profile: - material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)] - for material_container_file in material_container_files: - to_deserialize_material = False - container_id = self._stripFileToId(material_container_file) - need_new_name = False - materials = self._container_registry.findInstanceContainers(id = container_id) - - if not materials: - # No material found, deserialize this material later and add it - to_deserialize_material = True - else: - material_container = materials[0] - old_material_root_id = material_container.getMetaDataEntry("base_file") - if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only. - to_deserialize_material = True - - if self._resolve_strategies["material"] == "override": - # Remove the old materials and then deserialize the one from the project - root_material_id = material_container.getMetaDataEntry("base_file") - application.getContainerRegistry().removeContainer(root_material_id) - elif self._resolve_strategies["material"] == "new": - # Note that we *must* deserialize it with a new ID, as multiple containers will be - # auto created & added. - container_id = self.getNewId(container_id) - self._old_new_materials[old_material_root_id] = container_id - need_new_name = True - - if to_deserialize_material: - material_container = xml_material_profile(container_id) + definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id) + if not definitions: + definition_container = DefinitionContainer(container_id) try: - material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"), - file_name = container_id + "." + self._material_container_suffix) + definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"), + file_name = definition_container_file) except ContainerFormatError: - Logger.logException("e", "Failed to deserialize material file %s in project file %s", - material_container_file, file_name) - continue - if need_new_name: - new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName()) - material_container.setName(new_name) - material_container.setDirty(True) - self._container_registry.addContainer(material_container) + # We cannot just skip the definition file because everything else later will just break if the + # machine definition cannot be found. + Logger.logException("e", "Failed to deserialize definition file %s in project file %s", + definition_container_file, file_name) + definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults. + self._container_registry.addContainer(definition_container) Job.yieldThread() QCoreApplication.processEvents() # Ensure that the GUI does not freeze. - if global_stack: - # Handle quality changes if any - self._processQualityChanges(global_stack) + Logger.log("d", "Workspace loading is checking materials...") + # Get all the material files and check if they exist. If not, add them. + xml_material_profile = self._getXmlProfileClass() + if self._material_container_suffix is None: + self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0] + if xml_material_profile: + material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)] + for material_container_file in material_container_files: + to_deserialize_material = False + container_id = self._stripFileToId(material_container_file) + need_new_name = False + materials = self._container_registry.findInstanceContainers(id = container_id) - # Prepare the machine - self._applyChangesToMachine(global_stack, extruder_stack_dict) + if not materials: + # No material found, deserialize this material later and add it + to_deserialize_material = True + else: + material_container = materials[0] + old_material_root_id = material_container.getMetaDataEntry("base_file") + if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only. + to_deserialize_material = True + + if self._resolve_strategies["material"] == "override": + # Remove the old materials and then deserialize the one from the project + root_material_id = material_container.getMetaDataEntry("base_file") + application.getContainerRegistry().removeContainer(root_material_id) + elif self._resolve_strategies["material"] == "new": + # Note that we *must* deserialize it with a new ID, as multiple containers will be + # auto created & added. + container_id = self.getNewId(container_id) + self._old_new_materials[old_material_root_id] = container_id + need_new_name = True + + if to_deserialize_material: + material_container = xml_material_profile(container_id) + try: + material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"), + file_name = container_id + "." + self._material_container_suffix) + except ContainerFormatError: + Logger.logException("e", "Failed to deserialize material file %s in project file %s", + material_container_file, file_name) + continue + if need_new_name: + new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName()) + material_container.setName(new_name) + material_container.setDirty(True) + self._container_registry.addContainer(material_container) + Job.yieldThread() + QCoreApplication.processEvents() # Ensure that the GUI does not freeze. + else: + Logger.log("d", "Workspace loading user settings...") + try: + user_settings = json.loads(archive.open("Cura/user-settings.json").read().decode("utf-8")) + except KeyError as e: + # If there is no user settings file, it's not a PCB, so notify user of failure. + Logger.log("w", "File %s is not a valid PCB.", file_name) + message = Message( + i18n_catalog.i18nc("@info:error Don't translate the XML tags or !", + "Project file {0} is corrupt: {1}.", + file_name, str(e)), + title=i18n_catalog.i18nc("@info:title", "Can't Open Project File"), + message_type=Message.MessageType.ERROR) + message.show() + self.setWorkspaceName("") + return [], {} + + + if global_stack: + if self._load_profile: + # Handle quality changes if any + self._processQualityChanges(global_stack) + + # Prepare the machine + self._applyChangesToMachine(global_stack, extruder_stack_dict) + else: + self._applyUserSettings(global_stack, extruder_stack_dict, user_settings) Logger.log("d", "Workspace loading is notifying rest of the code of changes...") # Actually change the active machine. @@ -1181,21 +1213,47 @@ class ThreeMFWorkspaceReader(WorkspaceReader): material_node = machine_node.variants[extruder_stack.variant.getName()].materials[root_material_id] extruder_stack.material = material_node.container - def _applyChangesToMachine(self, global_stack, extruder_stack_dict): - # Clear all first + def _clearMachineSettings(self, global_stack, extruder_stack_dict): self._clearStack(global_stack) for extruder_stack in extruder_stack_dict.values(): self._clearStack(extruder_stack) + self._quality_changes_to_apply = None + self._quality_type_to_apply = None + self._intent_category_to_apply = None + self._user_settings_to_apply = None + + def _applyUserSettings(self, global_stack, extruder_stack_dict, user_settings): + # Clear all first + self._clearMachineSettings(global_stack, extruder_stack_dict) + + for stack_name, settings in user_settings.items(): + if stack_name == 'global': + ThreeMFWorkspaceReader._applyUserSettingsOnStack(global_stack, settings) + else: + extruder_match = re.fullmatch('extruder_([0-9]+)', stack_name) + if extruder_match is not None: + extruder_nr = extruder_match.group(1) + if extruder_nr in extruder_stack_dict: + ThreeMFWorkspaceReader._applyUserSettingsOnStack(extruder_stack_dict[extruder_nr], settings) + + @staticmethod + def _applyUserSettingsOnStack(stack, user_settings): + user_settings_container = stack.userChanges + + for setting_to_import, setting_value in user_settings.items(): + user_settings_container.setProperty(setting_to_import, 'value', setting_value) + + def _applyChangesToMachine(self, global_stack, extruder_stack_dict): + # Clear all first + self._clearMachineSettings(global_stack, extruder_stack_dict) + self._applyDefinitionChanges(global_stack, extruder_stack_dict) self._applyUserChanges(global_stack, extruder_stack_dict) self._applyVariants(global_stack, extruder_stack_dict) self._applyMaterials(global_stack, extruder_stack_dict) # prepare the quality to select - self._quality_changes_to_apply = None - self._quality_type_to_apply = None - self._intent_category_to_apply = None if self._machine_info.quality_changes_info is not None: self._quality_changes_to_apply = self._machine_info.quality_changes_info.name else: diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index c5b624d35d..c0006e21a8 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -73,6 +73,8 @@ class WorkspaceDialog(QObject): self._is_networked_machine = False self._is_compatible_machine = False self._has_visible_select_same_profile = False + self._select_same_profile_checked = True + self._allow_create_machine = True machineConflictChanged = pyqtSignal() qualityChangesConflictChanged = pyqtSignal() @@ -98,6 +100,7 @@ class WorkspaceDialog(QObject): missingPackagesChanged = pyqtSignal() isCompatibleMachineChanged = pyqtSignal() hasVisibleSelectSameProfileChanged = pyqtSignal() + selectSameProfileCheckedChanged = pyqtSignal() @pyqtProperty(bool, notify = isPrinterGroupChanged) def isPrinterGroup(self) -> bool: @@ -295,17 +298,19 @@ class WorkspaceDialog(QObject): @pyqtSlot(str) def setMachineToOverride(self, machine_name: str) -> None: + self._override_machine = machine_name + self.updateCompatibleMachine() + + def updateCompatibleMachine(self): registry = ContainerRegistry.getInstance() - containers_expected = registry.findDefinitionContainers(name = self._machine_type) - containers_selected = registry.findContainerStacks(id = machine_name) + containers_expected = registry.findDefinitionContainers(name=self._machine_type) + containers_selected = registry.findContainerStacks(id=self._override_machine) if len(containers_expected) == 1 and len(containers_selected) == 1: new_compatible_machine = (containers_expected[0] == containers_selected[0].definition) if new_compatible_machine != self._is_compatible_machine: self._is_compatible_machine = new_compatible_machine self.isCompatibleMachineChanged.emit() - self._override_machine = machine_name - @pyqtProperty(bool, notify = isCompatibleMachineChanged) def isCompatibleMachine(self) -> bool: return self._is_compatible_machine @@ -319,6 +324,22 @@ class WorkspaceDialog(QObject): def hasVisibleSelectSameProfile(self): return self._has_visible_select_same_profile + def setSelectSameProfileChecked(self, select_same_profile_checked): + if select_same_profile_checked != self._select_same_profile_checked: + self._select_same_profile_checked = select_same_profile_checked + self.selectSameProfileCheckedChanged.emit() + + @pyqtProperty(bool, notify = selectSameProfileCheckedChanged, fset = setSelectSameProfileChecked) + def selectSameProfileChecked(self): + return self._select_same_profile_checked + + def setAllowCreatemachine(self, allow_create_machine): + self._allow_create_machine = allow_create_machine + + @pyqtProperty(bool, constant = True) + def allowCreateMachine(self): + return self._allow_create_machine + @pyqtSlot() def closeBackend(self) -> None: """Close the backend: otherwise one could end up with "Slicing...""" diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 45fe7b6989..c7074ce220 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -120,13 +120,17 @@ UM.Dialog minDropDownWidth: machineSelector.width - buttons: [ + Component + { + id: componentNewPrinter + Cura.SecondaryButton { id: createNewPrinter text: catalog.i18nc("@button", "Create new") fixedWidthMode: true width: parent.width - leftPadding * 1.5 + visible: manager.allowCreateMachine onClicked: { toggleContent() @@ -136,7 +140,9 @@ UM.Dialog manager.setIsNetworkedMachine(false) } } - ] + } + + buttons: manager.allowCreateMachine ? [componentNewPrinter.createObject()] : [] onSelectPrinter: function(machine) { @@ -191,9 +197,12 @@ UM.Dialog { text: catalog.i18nc("@action:checkbox", "Select the same profile") enabled: manager.isCompatibleMachine - onEnabledChanged: checked = enabled + onEnabledChanged: manager.selectSameProfileChecked = enabled tooltip: enabled ? "" : catalog.i18nc("@tooltip", "You can use the same profile only if you have the same printer as the project was published with") visible: manager.hasVisibleSelectSameProfile + + checked: manager.selectSameProfileChecked + onCheckedChanged: manager.selectSameProfileChecked = checked } } diff --git a/plugins/3MFWriter/SettingsExportModel.py b/plugins/3MFWriter/SettingsExportModel.py index a4acaf02f7..0c34278067 100644 --- a/plugins/3MFWriter/SettingsExportModel.py +++ b/plugins/3MFWriter/SettingsExportModel.py @@ -105,7 +105,7 @@ class SettingsExportModel(QObject): @staticmethod def _exportSettings(settings_stack): - user_settings_container = settings_stack.getTop() + user_settings_container = settings_stack.userChanges user_keys = user_settings_container.getAllKeys() settings_export = []