diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index b5d4001fe2..0c6740f740 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -189,7 +189,7 @@ class CrashHandler: json_metadata_file = os.path.join(directory, "plugin.json") try: - with open(json_metadata_file, "r") as f: + with open(json_metadata_file, "r", encoding = "utf-8") as f: try: metadata = json.loads(f.read()) module_version = metadata["version"] @@ -217,9 +217,9 @@ class CrashHandler: text_area = QTextEdit() tmp_file_fd, tmp_file_path = tempfile.mkstemp(prefix = "cura-crash", text = True) os.close(tmp_file_fd) - with open(tmp_file_path, "w") as f: + with open(tmp_file_path, "w", encoding = "utf-8") as f: faulthandler.dump_traceback(f, all_threads=True) - with open(tmp_file_path, "r") as f: + with open(tmp_file_path, "r", encoding = "utf-8") as f: logdata = f.read() text_area.setText(logdata) diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 21fc3f43c0..7ad185cf66 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -3,7 +3,7 @@ import copy import os.path -import urllib +import urllib.parse import uuid from typing import Any, Dict, List, Union @@ -486,7 +486,7 @@ class ContainerManager(QObject): container = container_type(container_id) try: - with open(file_url, "rt") as f: + with open(file_url, "rt", encoding = "utf-8") as f: container.deserialize(f.read()) except PermissionError: return { "status": "error", "message": "Permission denied when trying to read the file"} diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 41b2c492f0..0b328cebda 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -208,7 +208,7 @@ class CuraContainerRegistry(ContainerRegistry): except Exception as e: # Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None. Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name,profile_reader.getPluginId(), str(e)) - return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags or !", "Failed to import profile from {0}: {1}", file_name, str(e))} + return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags or !", "Failed to import profile from {0}: {1}", file_name, "\n" + str(e))} if profile_or_list: # Ensure it is always a list of profiles @@ -246,6 +246,41 @@ class CuraContainerRegistry(ContainerRegistry): if type(profile_or_list) is not list: profile_or_list = [profile_or_list] + # Make sure that there are also extruder stacks' quality_changes, not just one for the global stack + if len(profile_or_list) == 1: + global_profile = profile_or_list[0] + extruder_profiles = [] + for idx, extruder in enumerate(global_container_stack.extruders.values()): + profile_id = ContainerRegistry.getInstance().uniqueName(global_container_stack.getId() + "_extruder_" + str(idx + 1)) + profile = InstanceContainer(profile_id) + profile.setName(global_profile.getName()) + profile.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) + profile.addMetaDataEntry("type", "quality_changes") + profile.addMetaDataEntry("definition", global_profile.getMetaDataEntry("definition")) + profile.addMetaDataEntry("quality_type", global_profile.getMetaDataEntry("quality_type")) + profile.addMetaDataEntry("extruder", extruder.getId()) + profile.setDirty(True) + if idx == 0: + # move all per-extruder settings to the first extruder's quality_changes + for qc_setting_key in global_profile.getAllKeys(): + settable_per_extruder = global_container_stack.getProperty(qc_setting_key, + "settable_per_extruder") + if settable_per_extruder: + setting_value = global_profile.getProperty(qc_setting_key, "value") + + setting_definition = global_container_stack.getSettingDefinition(qc_setting_key) + new_instance = SettingInstance(setting_definition, profile) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + profile.addInstance(new_instance) + profile.setDirty(True) + + global_profile.removeInstance(qc_setting_key, postpone_emit=True) + extruder_profiles.append(profile) + + for profile in extruder_profiles: + profile_or_list.append(profile) + # Import all profiles for profile_index, profile in enumerate(profile_or_list): if profile_index == 0: @@ -296,7 +331,7 @@ class CuraContainerRegistry(ContainerRegistry): profile.setDirty(True) # Ensure the profiles are correctly saved new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile")) - profile._id = new_id + profile.setMetaDataEntry("id", new_id) profile.setName(new_name) # Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile @@ -507,9 +542,10 @@ class CuraContainerRegistry(ContainerRegistry): user_container = InstanceContainer(user_container_id) user_container.setName(user_container_name) user_container.addMetaDataEntry("type", "user") - user_container.addMetaDataEntry("machine", extruder_stack.getId()) + user_container.addMetaDataEntry("machine", machine.getId()) user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) user_container.setDefinition(machine.definition.getId()) + user_container.setMetaDataEntry("extruder", extruder_stack.getId()) if machine.userChanges: # for the newly created extruder stack, we need to move all "per-extruder" settings to the user changes @@ -693,7 +729,7 @@ class CuraContainerRegistry(ContainerRegistry): continue instance_container = InstanceContainer(container_id) - with open(file_path, "r") as f: + with open(file_path, "r", encoding = "utf-8") as f: serialized = f.read() instance_container.deserialize(serialized, file_path) self.addContainer(instance_container) diff --git a/cura_app.py b/cura_app.py index 6d1ff6ab6b..b9afb9bbcc 100755 --- a/cura_app.py +++ b/cura_app.py @@ -30,8 +30,8 @@ if not known_args["debug"]: if hasattr(sys, "frozen"): dirpath = get_cura_dir_path() os.makedirs(dirpath, exist_ok = True) - sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w") - sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w") + 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 diff --git a/plugins/CuraProfileReader/CuraProfileReader.py b/plugins/CuraProfileReader/CuraProfileReader.py index 5631d138aa..69b2187541 100644 --- a/plugins/CuraProfileReader/CuraProfileReader.py +++ b/plugins/CuraProfileReader/CuraProfileReader.py @@ -39,7 +39,7 @@ class CuraProfileReader(ProfileReader): except zipfile.BadZipFile: # It must be an older profile from Cura 2.1. - with open(file_name, encoding="utf-8") as fhandle: + with open(file_name, encoding = "utf-8") as fhandle: serialized = fhandle.read() return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized, file_name)] @@ -52,10 +52,10 @@ class CuraProfileReader(ProfileReader): parser = configparser.ConfigParser(interpolation=None) parser.read_string(serialized) - if not "general" in parser: + if "general" not in parser: Logger.log("w", "Missing required section 'general'.") return [] - if not "version" in parser["general"]: + if "version" not in parser["general"]: Logger.log("w", "Missing required 'version' property") return [] diff --git a/plugins/GCodeReader/GCodeReader.py b/plugins/GCodeReader/GCodeReader.py index cb2f5d99e0..050f243f9b 100755 --- a/plugins/GCodeReader/GCodeReader.py +++ b/plugins/GCodeReader/GCodeReader.py @@ -26,7 +26,7 @@ class GCodeReader(MeshReader): # PreRead is used to get the correct flavor. If not, Marlin is set by default def preRead(self, file_name, *args, **kwargs): - with open(file_name, "r") as file: + with open(file_name, "r", encoding = "utf-8") as file: for line in file: if line[:len(self._flavor_keyword)] == self._flavor_keyword: try: diff --git a/plugins/LegacyProfileReader/LegacyProfileReader.py b/plugins/LegacyProfileReader/LegacyProfileReader.py index 07cd8b0aad..824fc959a1 100644 --- a/plugins/LegacyProfileReader/LegacyProfileReader.py +++ b/plugins/LegacyProfileReader/LegacyProfileReader.py @@ -125,7 +125,10 @@ class LegacyProfileReader(ProfileReader): Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?") return None current_printer_definition = global_container_stack.definition - profile.setDefinition(current_printer_definition.getId()) + quality_definition = current_printer_definition.getMetaDataEntry("quality_definition") + if not quality_definition: + quality_definition = current_printer_definition.getId() + profile.setDefinition(quality_definition) for new_setting in dict_of_doom["translation"]: # Evaluate all new settings that would get a value from the translations. old_setting_expression = dict_of_doom["translation"][new_setting] compiled = compile(old_setting_expression, new_setting, "eval") @@ -162,20 +165,21 @@ class LegacyProfileReader(ProfileReader): data = stream.getvalue() profile.deserialize(data) + # The definition can get reset to fdmprinter during the deserialization's upgrade. Here we set the definition + # again. + profile.setDefinition(quality_definition) + #We need to return one extruder stack and one global stack. global_container_id = container_registry.uniqueName("Global Imported Legacy Profile") global_profile = profile.duplicate(new_id = global_container_id, new_name = profile_id) #Needs to have the same name as the extruder profile. global_profile.setDirty(True) - #Only the extruder stack has an extruder metadata entry. - profile.addMetaDataEntry("extruder", ExtruderManager.getInstance().getActiveExtruderStack().definition.getId()) + profile_definition = "fdmprinter" + from UM.Util import parseBool + if parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", "False")): + profile_definition = global_container_stack.getMetaDataEntry("quality_definition") + if not profile_definition: + profile_definition = global_container_stack.definition.getId() + global_profile.setDefinition(profile_definition) - #Split all settings into per-extruder and global settings. - for setting_key in profile.getAllKeys(): - settable_per_extruder = global_container_stack.getProperty(setting_key, "settable_per_extruder") - if settable_per_extruder: - global_profile.removeInstance(setting_key) - else: - profile.removeInstance(setting_key) - - return [global_profile, profile] + return [global_profile] diff --git a/plugins/UltimakerMachineActions/UM2UpgradeSelection.py b/plugins/UltimakerMachineActions/UM2UpgradeSelection.py index 1bf0b98217..0fd31ec445 100644 --- a/plugins/UltimakerMachineActions/UM2UpgradeSelection.py +++ b/plugins/UltimakerMachineActions/UM2UpgradeSelection.py @@ -2,7 +2,6 @@ # Uranium is released under the terms of the LGPLv3 or higher. from UM.Settings.ContainerRegistry import ContainerRegistry -from UM.Settings.InstanceContainer import InstanceContainer from cura.MachineAction import MachineAction from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty @@ -11,8 +10,6 @@ from UM.Application import Application from UM.Util import parseBool catalog = i18nCatalog("cura") -import UM.Settings.InstanceContainer - ## The Ultimaker 2 can have a few revisions & upgrades. class UM2UpgradeSelection(MachineAction): @@ -22,18 +19,29 @@ class UM2UpgradeSelection(MachineAction): self._container_registry = ContainerRegistry.getInstance() + self._current_global_stack = None + + from cura.CuraApplication import CuraApplication + CuraApplication.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) + self._reset() + def _reset(self): self.hasVariantsChanged.emit() + def _onGlobalStackChanged(self): + if self._current_global_stack: + self._current_global_stack.metaDataChanged.disconnect(self._onGlobalStackMetaDataChanged) + + self._current_global_stack = Application.getInstance().getGlobalContainerStack() + if self._current_global_stack: + self._current_global_stack.metaDataChanged.connect(self._onGlobalStackMetaDataChanged) + self._reset() + + def _onGlobalStackMetaDataChanged(self): + self._reset() + hasVariantsChanged = pyqtSignal() - @pyqtProperty(bool, notify = hasVariantsChanged) - def hasVariants(self): - global_container_stack = Application.getInstance().getGlobalContainerStack() - if global_container_stack: - return parseBool(global_container_stack.getMetaDataEntry("has_variants", "false")) - - @pyqtSlot(bool) def setHasVariants(self, has_variants = True): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: @@ -62,3 +70,9 @@ class UM2UpgradeSelection(MachineAction): global_container_stack.extruders["0"].variant = ContainerRegistry.getInstance().getEmptyInstanceContainer() Application.getInstance().globalContainerStackChanged.emit() + self._reset() + + @pyqtProperty(bool, fset = setHasVariants, notify = hasVariantsChanged) + def hasVariants(self): + if self._current_global_stack: + return parseBool(self._current_global_stack.getMetaDataEntry("has_variants", "false")) diff --git a/plugins/UltimakerMachineActions/UM2UpgradeSelectionMachineAction.qml b/plugins/UltimakerMachineActions/UM2UpgradeSelectionMachineAction.qml index 988c9d6128..793f3f00a8 100644 --- a/plugins/UltimakerMachineActions/UM2UpgradeSelectionMachineAction.qml +++ b/plugins/UltimakerMachineActions/UM2UpgradeSelectionMachineAction.qml @@ -13,6 +13,7 @@ import Cura 1.0 as Cura Cura.MachineAction { anchors.fill: parent; + Item { id: upgradeSelectionMachineAction @@ -39,12 +40,19 @@ Cura.MachineAction CheckBox { + id: olssonBlockCheckBox anchors.top: pageDescription.bottom anchors.topMargin: UM.Theme.getSize("default_margin").height text: catalog.i18nc("@label", "Olsson Block") checked: manager.hasVariants - onClicked: manager.setHasVariants(checked) + onClicked: manager.hasVariants = checked + + Connections + { + target: manager + onHasVariantsChanged: olssonBlockCheckBox.checked = manager.hasVariants + } } UM.I18nCatalog { id: catalog; name: "cura"; } diff --git a/plugins/VersionUpgrade/VersionUpgrade22to24/VersionUpgrade.py b/plugins/VersionUpgrade/VersionUpgrade22to24/VersionUpgrade.py index 19f0563f10..7505911049 100644 --- a/plugins/VersionUpgrade/VersionUpgrade22to24/VersionUpgrade.py +++ b/plugins/VersionUpgrade/VersionUpgrade22to24/VersionUpgrade.py @@ -74,7 +74,7 @@ class VersionUpgrade22to24(VersionUpgrade): def __convertVariant(self, variant_path): # Copy the variant to the machine_instances/*_settings.inst.cfg variant_config = configparser.ConfigParser(interpolation=None) - with open(variant_path, "r") as fhandle: + with open(variant_path, "r", encoding = "utf-8") as fhandle: variant_config.read_file(fhandle) config_name = "Unknown Variant" diff --git a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py index 8c5a160ff4..d7b2c1a001 100644 --- a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py +++ b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py @@ -59,6 +59,12 @@ _EMPTY_CONTAINER_DICT = { } +# Renamed definition files +_RENAMED_DEFINITION_DICT = { + "jellybox": "imade3d_jellybox", +} + + class VersionUpgrade30to31(VersionUpgrade): ## Gets the version number from a CFG file in Uranium's 3.0 format. # @@ -111,16 +117,9 @@ class VersionUpgrade30to31(VersionUpgrade): if not parser.has_section(each_section): parser.add_section(each_section) - # Copy global quality changes to extruder quality changes for single extrusion machines - if parser["metadata"]["type"] == "quality_changes": - all_quality_changes = self._getSingleExtrusionMachineQualityChanges(parser) - # Note that DO NOT!!! use the quality_changes returned from _getSingleExtrusionMachineQualityChanges(). - # Those are loaded from the hard drive which are original files that haven't been upgraded yet. - # NOTE 2: The number can be 0 or 1 depends on whether you are loading it from the qualities folder or - # from a project file. When you load from a project file, the custom profile may not be in cura - # yet, so you will get 0. - if len(all_quality_changes) <= 1 and not parser.has_option("metadata", "extruder"): - self._createExtruderQualityChangesForSingleExtrusionMachine(filename, parser) + # Check renamed definitions + if "definition" in parser["general"] and parser["general"]["definition"] in _RENAMED_DEFINITION_DICT: + parser["general"]["definition"] = _RENAMED_DEFINITION_DICT[parser["general"]["definition"]] # Update version numbers parser["general"]["version"] = "2" @@ -156,6 +155,10 @@ class VersionUpgrade30to31(VersionUpgrade): if parser.has_option("containers", key) and parser["containers"][key] == "empty": parser["containers"][key] = specific_empty_container + # check renamed definition + if parser.has_option("containers", "6") and parser["containers"]["6"] in _RENAMED_DEFINITION_DICT: + parser["containers"]["6"] = _RENAMED_DEFINITION_DICT[parser["containers"]["6"]] + # Update version numbers if "general" not in parser: parser["general"] = {} @@ -219,6 +222,10 @@ class VersionUpgrade30to31(VersionUpgrade): extruder_quality_changes_parser["general"]["name"] = global_quality_changes["general"]["name"] extruder_quality_changes_parser["general"]["definition"] = global_quality_changes["general"]["definition"] + # check renamed definition + if extruder_quality_changes_parser["general"]["definition"] in _RENAMED_DEFINITION_DICT: + extruder_quality_changes_parser["general"]["definition"] = _RENAMED_DEFINITION_DICT[extruder_quality_changes_parser["general"]["definition"]] + extruder_quality_changes_parser.add_section("metadata") extruder_quality_changes_parser["metadata"]["quality_type"] = global_quality_changes["metadata"]["quality_type"] extruder_quality_changes_parser["metadata"]["type"] = global_quality_changes["metadata"]["type"] @@ -231,5 +238,5 @@ class VersionUpgrade30to31(VersionUpgrade): quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer) - with open(os.path.join(quality_changes_dir, extruder_quality_changes_filename), "w") as f: + with open(os.path.join(quality_changes_dir, extruder_quality_changes_filename), "w", encoding = "utf-8") as f: f.write(extruder_quality_changes_output.getvalue()) diff --git a/resources/qml/Menus/ProfileMenu.qml b/resources/qml/Menus/ProfileMenu.qml index edce2641af..f543bb36eb 100644 --- a/resources/qml/Menus/ProfileMenu.qml +++ b/resources/qml/Menus/ProfileMenu.qml @@ -29,7 +29,11 @@ Menu onObjectRemoved: menu.removeItem(object); } - MenuSeparator { id: customSeparator } + MenuSeparator + { + id: customSeparator + visible: Cura.UserProfilesModel.rowCount > 0 + } Instantiator {