From 384618b5c4db9192e1f0d8e3fae1f220d63c15c3 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 7 Jun 2018 15:49:00 +0200 Subject: [PATCH 01/14] Fix branded materials model binding CURA-5336 When the global stack gets changed, the model should update itself so it refers to the correct extruder stack. --- cura/Machines/Models/BrandMaterialsModel.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/cura/Machines/Models/BrandMaterialsModel.py b/cura/Machines/Models/BrandMaterialsModel.py index f6c9a14632..236f105d12 100644 --- a/cura/Machines/Models/BrandMaterialsModel.py +++ b/cura/Machines/Models/BrandMaterialsModel.py @@ -47,22 +47,38 @@ class BrandMaterialsModel(ListModel): self.addRoleName(self.MaterialsRole, "materials") self._extruder_position = 0 + self._extruder_stack = None from cura.CuraApplication import CuraApplication self._machine_manager = CuraApplication.getInstance().getMachineManager() self._extruder_manager = CuraApplication.getInstance().getExtruderManager() self._material_manager = CuraApplication.getInstance().getMaterialManager() + self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack) self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines. self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes. self._update() + def _updateExtruderStack(self): + global_stack = self._machine_manager.activeMachine + if global_stack is None: + return + + if self._extruder_stack is not None: + self._extruder_stack.pyqtContainersChanged.disconnect(self._update) + 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() - @pyqtProperty(int, fset = setExtruderPosition, notify = extruderPositionChanged) + @pyqtProperty(int, fset=setExtruderPosition, notify=extruderPositionChanged) def extruderPosition(self) -> int: return self._extruder_position From b55d2eda4f684f5707b6693d189d282956d77340 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 11 Jun 2018 09:14:48 +0200 Subject: [PATCH 02/14] Remove Polish translation option The translator said he won't be maintaining this language any more. Someone else can pick it up now if he wants to, but we'll disable the language until it is complete again. Contributes to issue CURA-5451. --- resources/qml/Preferences/GeneralPage.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index 7841c7d506..5de74ac3a8 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -170,7 +170,7 @@ UM.PreferencesPage append({ text: "日本語", code: "ja_JP" }) append({ text: "한국어", code: "ko_KR" }) append({ text: "Nederlands", code: "nl_NL" }) - append({ text: "Polski", code: "pl_PL" }) + //Polish is disabled for being incomplete: append({ text: "Polski", code: "pl_PL" }) append({ text: "Português do Brasil", code: "pt_BR" }) append({ text: "Português", code: "pt_PT" }) append({ text: "Русский", code: "ru_RU" }) From ef4218830b92d13c77c16fb20a602f19d99deca4 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 11 Jun 2018 09:37:45 +0200 Subject: [PATCH 03/14] Fix package manager initialization --- cura/CuraApplication.py | 2 ++ cura/CuraPackageManager.py | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index e34d0a07ab..af65d63e70 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -225,6 +225,8 @@ class CuraApplication(QtApplication): from cura.Settings.CuraContainerRegistry import CuraContainerRegistry self._container_registry_class = CuraContainerRegistry + from cura.CuraPackageManager import CuraPackageManager + self._package_manager_class = CuraPackageManager # Adds command line options to the command line parser. This should be called after the application is created and # before the pre-start. diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 49f095941a..f65633ed66 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -7,8 +7,11 @@ from UM.Resources import Resources #To find storage paths for some resource type class CuraPackageManager(PackageManager): - def __init__(self, parent = None): - super().__init__(parent) + def __init__(self, application, parent = None): + super().__init__(application, parent) + def initialize(self): self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer) self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer) + + super().initialize() From 8ea3c879f4e72964deea5e0f714daf2e922f872c Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 11 Jun 2018 10:48:20 +0200 Subject: [PATCH 04/14] Fix quality profile update CURA-5453 Should take into account that the custom profile being updated may not have a quality_changes container for each stack, and Cura should create one if this is the case, so the user changes results can be stored. --- cura/Settings/ContainerManager.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index ea2821ce25..b1800facb5 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -42,6 +42,7 @@ class ContainerManager(QObject): self._container_registry = self._application.getContainerRegistry() self._machine_manager = self._application.getMachineManager() self._material_manager = self._application.getMaterialManager() + self._quality_manager = self._application.getQualityManager() self._container_name_filters = {} @pyqtSlot(str, str, result=str) @@ -313,10 +314,18 @@ class ContainerManager(QObject): self._machine_manager.blurSettings.emit() global_stack = self._machine_manager.activeMachine + current_quality_changes_name = global_stack.qualityChanges.getName() + current_quality_type = global_stack.quality.getMetaDataEntry("quality_type") 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 quality_changes.getId() == "empty_quality_changes": + quality_changes = self._quality_manager._createQualityChanges(current_quality_type, current_quality_changes_name, + global_stack, stack) + stack.qualityChanges = quality_changes + if not quality_changes or self._container_registry.isReadOnly(quality_changes.getId()): Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId()) continue From c779795618e72b2373faf5bc70d2921aba67a49a Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 11 Jun 2018 10:55:07 +0200 Subject: [PATCH 05/14] Remove unnecessary updateMaterialForDiameter() CURA-5327 --- cura/Settings/ExtruderManager.py | 9 --------- plugins/MachineSettingsAction/MachineSettingsAction.py | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 8c436ed24f..25690fcbde 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -552,15 +552,6 @@ class ExtruderManager(QObject): def getInstanceExtruderValues(self, key): return ExtruderManager.getExtruderValues(key) - ## Updates the material container to a material that matches the material diameter set for the printer - def updateMaterialForDiameter(self, extruder_position: int, global_stack = None): - if not global_stack: - global_stack = Application.getInstance().getGlobalContainerStack() - if not global_stack: - return - - Application.getInstance().getMachineManager()._updateMaterialWithVariant(extruder_position) - ## Get the value for a setting from a specific extruder. # # This is exposed to SettingFunction to use in value functions. diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index 7d5b317475..974ecf4b02 100755 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -158,4 +158,4 @@ class MachineSettingsAction(MachineAction): @pyqtSlot(int) def updateMaterialForDiameter(self, extruder_position: int): # Updates the material container to a material that matches the material diameter set for the printer - self._application.getExtruderManager().updateMaterialForDiameter(extruder_position) + self._application.getMachineManager()._updateMaterialWithVariant(extruder_position) From 54a03723ab4dbb5d29ce8b3f9ea98758ea404352 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 11 Jun 2018 11:08:47 +0200 Subject: [PATCH 06/14] Set encoding correctly when opening files everywhere Otherwise the encoding is interpreted differently on Windows and Mac. --- plugins/ChangeLogPlugin/ChangeLog.py | 2 +- plugins/GCodeProfileReader/GCodeProfileReader.py | 2 +- plugins/LegacyProfileReader/LegacyProfileReader.py | 2 +- plugins/USBPrinting/avr_isp/intelHex.py | 2 +- .../VersionUpgrade/VersionUpgrade22to24/VersionUpgrade.py | 6 +++--- .../VersionUpgrade25to26/VersionUpgrade25to26.py | 6 +++--- plugins/XmlMaterialProfile/XmlMaterialProfile.py | 2 +- tests/TestProfileRequirements.py | 4 ++-- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/ChangeLogPlugin/ChangeLog.py b/plugins/ChangeLogPlugin/ChangeLog.py index e93d5c4395..723c83a021 100644 --- a/plugins/ChangeLogPlugin/ChangeLog.py +++ b/plugins/ChangeLogPlugin/ChangeLog.py @@ -55,7 +55,7 @@ class ChangeLog(Extension, QObject,): def loadChangeLogs(self): self._change_logs = collections.OrderedDict() - with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r",-1, "utf-8") as f: + with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r", encoding = "utf-8") as f: open_version = None open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog for line in f: diff --git a/plugins/GCodeProfileReader/GCodeProfileReader.py b/plugins/GCodeProfileReader/GCodeProfileReader.py index 4b50a600ba..9fbae7b473 100644 --- a/plugins/GCodeProfileReader/GCodeProfileReader.py +++ b/plugins/GCodeProfileReader/GCodeProfileReader.py @@ -57,7 +57,7 @@ class GCodeProfileReader(ProfileReader): # TODO: Consider moving settings to the start? serialized = "" # Will be filled with the serialized profile. try: - with open(file_name, "r") as f: + with open(file_name, "r", encoding = "utf-8") as f: for line in f: if line.startswith(prefix): # Remove the prefix and the newline from the line and add it to the rest. diff --git a/plugins/LegacyProfileReader/LegacyProfileReader.py b/plugins/LegacyProfileReader/LegacyProfileReader.py index 3c2b9bfa76..40a843e6c4 100644 --- a/plugins/LegacyProfileReader/LegacyProfileReader.py +++ b/plugins/LegacyProfileReader/LegacyProfileReader.py @@ -100,7 +100,7 @@ class LegacyProfileReader(ProfileReader): return None try: - with open(os.path.join(PluginRegistry.getInstance().getPluginPath("LegacyProfileReader"), "DictionaryOfDoom.json"), "r", -1, "utf-8") as f: + with open(os.path.join(PluginRegistry.getInstance().getPluginPath("LegacyProfileReader"), "DictionaryOfDoom.json"), "r", encoding = "utf-8") as f: dict_of_doom = json.load(f) # Parse the Dictionary of Doom. except IOError as e: Logger.log("e", "Could not open DictionaryOfDoom.json for reading: %s", str(e)) diff --git a/plugins/USBPrinting/avr_isp/intelHex.py b/plugins/USBPrinting/avr_isp/intelHex.py index a51c798d8e..671f1788f7 100644 --- a/plugins/USBPrinting/avr_isp/intelHex.py +++ b/plugins/USBPrinting/avr_isp/intelHex.py @@ -13,7 +13,7 @@ def readHex(filename): """ data = [] extra_addr = 0 - f = io.open(filename, "r") + f = io.open(filename, "r", encoding = "utf-8") for line in f: line = line.strip() if len(line) < 1: diff --git a/plugins/VersionUpgrade/VersionUpgrade22to24/VersionUpgrade.py b/plugins/VersionUpgrade/VersionUpgrade22to24/VersionUpgrade.py index 7505911049..730a62e591 100644 --- a/plugins/VersionUpgrade/VersionUpgrade22to24/VersionUpgrade.py +++ b/plugins/VersionUpgrade/VersionUpgrade22to24/VersionUpgrade.py @@ -94,7 +94,7 @@ class VersionUpgrade22to24(VersionUpgrade): if variant_path.endswith("_variant.inst.cfg"): variant_path = variant_path[:-len("_variant.inst.cfg")] + "_settings.inst.cfg" - with open(os.path.join(machine_instances_dir, os.path.basename(variant_path)), "w") as fp: + with open(os.path.join(machine_instances_dir, os.path.basename(variant_path)), "w", encoding = "utf-8") as fp: variant_config.write(fp) return config_name @@ -105,9 +105,9 @@ class VersionUpgrade22to24(VersionUpgrade): result = [] for entry in os.scandir(variants_dir): - if entry.name.endswith('.inst.cfg') and entry.is_file(): + if entry.name.endswith(".inst.cfg") and entry.is_file(): config = configparser.ConfigParser(interpolation = None) - with open(entry.path, "r") as fhandle: + with open(entry.path, "r", encoding = "utf-8") as fhandle: config.read_file(fhandle) if config.has_section("general") and config.has_option("general", "name"): result.append( { "path": entry.path, "name": config.get("general", "name") } ) diff --git a/plugins/VersionUpgrade/VersionUpgrade25to26/VersionUpgrade25to26.py b/plugins/VersionUpgrade/VersionUpgrade25to26/VersionUpgrade25to26.py index e1c14be2e1..54b561c847 100644 --- a/plugins/VersionUpgrade/VersionUpgrade25to26/VersionUpgrade25to26.py +++ b/plugins/VersionUpgrade/VersionUpgrade25to26/VersionUpgrade25to26.py @@ -249,11 +249,11 @@ class VersionUpgrade25to26(VersionUpgrade): definition_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.DefinitionChangesContainer) user_settings_dir = Resources.getPath(CuraApplication.ResourceTypes.UserInstanceContainer) - with open(os.path.join(definition_changes_dir, definition_changes_filename), "w") as f: + with open(os.path.join(definition_changes_dir, definition_changes_filename), "w", encoding = "utf-8") as f: f.write(definition_changes_output.getvalue()) - with open(os.path.join(user_settings_dir, user_settings_filename), "w") as f: + with open(os.path.join(user_settings_dir, user_settings_filename), "w", encoding = "utf-8") as f: f.write(user_settings_output.getvalue()) - with open(os.path.join(extruder_stack_dir, extruder_filename), "w") as f: + with open(os.path.join(extruder_stack_dir, extruder_filename), "w", encoding = "utf-8") as f: f.write(extruder_output.getvalue()) ## Creates a definition changes container which doesn't contain anything for the Custom FDM Printers. diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 4985c2fd87..9d456c833d 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -1018,7 +1018,7 @@ class XmlMaterialProfile(InstanceContainer): @classmethod def getProductIdMap(cls) -> Dict[str, List[str]]: product_to_id_file = os.path.join(os.path.dirname(sys.modules[cls.__module__].__file__), "product_to_id.json") - with open(product_to_id_file) as f: + with open(product_to_id_file, encoding = "utf-8") as f: product_to_id_map = json.load(f) product_to_id_map = {key: [value] for key, value in product_to_id_map.items()} return product_to_id_map diff --git a/tests/TestProfileRequirements.py b/tests/TestProfileRequirements.py index edeec909f2..f75ca9da8d 100644 --- a/tests/TestProfileRequirements.py +++ b/tests/TestProfileRequirements.py @@ -19,7 +19,7 @@ import pytest def test_ultimaker3extended_variants(um3_file, um3e_file): directory = os.path.join(os.path.dirname(__file__), "..", "resources", "variants") #TODO: Hardcoded path relative to this test file. um3 = configparser.ConfigParser() - um3.read_file(open(os.path.join(directory, um3_file))) + um3.read_file(open(os.path.join(directory, um3_file), encoding = "utf-8")) um3e = configparser.ConfigParser() - um3e.read_file(open(os.path.join(directory, um3e_file))) + um3e.read_file(open(os.path.join(directory, um3e_file), encoding = "utf-8")) assert um3["values"] == um3e["values"] From dc1e36d726a4ea5a3f865ffc93170ff453687fa1 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 8 Jun 2018 11:21:17 +0200 Subject: [PATCH 07/14] CURA-5444 Always create the instance of the material even if it is not compatible, otherwise it will never show as incompatible if the material profile doesn't define hotends in the machine. --- .../XmlMaterialProfile/XmlMaterialProfile.py | 49 ++++++++++--------- plugins/XmlMaterialProfile/product_to_id.json | 1 + 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 148471fb6d..241ec2a711 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -632,35 +632,36 @@ class XmlMaterialProfile(InstanceContainer): machine_manufacturer = identifier.get("manufacturer", definition.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition. - if machine_compatibility: - new_material_id = self.getId() + "_" + machine_id + # Always create the instance of the material even if it is not compatible, otherwise it will never + # show as incompatible if the material profile doesn't define hotends in the machine - CURA-5444 + new_material_id = self.getId() + "_" + machine_id - # The child or derived material container may already exist. This can happen when a material in a - # project file and the a material in Cura have the same ID. - # In the case if a derived material already exists, override that material container because if - # the data in the parent material has been changed, the derived ones should be updated too. - if ContainerRegistry.getInstance().isLoaded(new_material_id): - new_material = ContainerRegistry.getInstance().findContainers(id = new_material_id)[0] - is_new_material = False - else: - new_material = XmlMaterialProfile(new_material_id) - is_new_material = True + # The child or derived material container may already exist. This can happen when a material in a + # project file and the a material in Cura have the same ID. + # In the case if a derived material already exists, override that material container because if + # the data in the parent material has been changed, the derived ones should be updated too. + if ContainerRegistry.getInstance().isLoaded(new_material_id): + new_material = ContainerRegistry.getInstance().findContainers(id = new_material_id)[0] + is_new_material = False + else: + new_material = XmlMaterialProfile(new_material_id) + is_new_material = True - new_material.setMetaData(copy.deepcopy(self.getMetaData())) - new_material.getMetaData()["id"] = new_material_id - new_material.getMetaData()["name"] = self.getName() - new_material.setDefinition(machine_id) - # Don't use setMetadata, as that overrides it for all materials with same base file - new_material.getMetaData()["compatible"] = machine_compatibility - new_material.getMetaData()["machine_manufacturer"] = machine_manufacturer - new_material.getMetaData()["definition"] = machine_id + new_material.setMetaData(copy.deepcopy(self.getMetaData())) + new_material.getMetaData()["id"] = new_material_id + new_material.getMetaData()["name"] = self.getName() + new_material.setDefinition(machine_id) + # Don't use setMetadata, as that overrides it for all materials with same base file + new_material.getMetaData()["compatible"] = machine_compatibility + new_material.getMetaData()["machine_manufacturer"] = machine_manufacturer + new_material.getMetaData()["definition"] = machine_id - new_material.setCachedValues(cached_machine_setting_properties) + new_material.setCachedValues(cached_machine_setting_properties) - new_material._dirty = False + new_material._dirty = False - if is_new_material: - containers_to_add.append(new_material) + if is_new_material: + containers_to_add.append(new_material) # Find the buildplates compatibility buildplates = machine.iterfind("./um:buildplate", self.__namespaces) diff --git a/plugins/XmlMaterialProfile/product_to_id.json b/plugins/XmlMaterialProfile/product_to_id.json index d6b8f3bade..3e7ce9311f 100644 --- a/plugins/XmlMaterialProfile/product_to_id.json +++ b/plugins/XmlMaterialProfile/product_to_id.json @@ -8,5 +8,6 @@ "Ultimaker 3 Extended": "ultimaker3_extended", "Ultimaker Original": "ultimaker_original", "Ultimaker Original+": "ultimaker_original_plus", + "Ultimaker Original Dual Extrusion": "ultimaker_original_dual", "IMADE3D JellyBOX": "imade3d_jellybox" } \ No newline at end of file From 8c9939e797886e214dc17db793e111ab66ecb17c Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 8 Jun 2018 12:03:46 +0200 Subject: [PATCH 08/14] CURA-5444 Also get the metadata even if the material was marked as not compatible. --- .../XmlMaterialProfile/XmlMaterialProfile.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 241ec2a711..4985c2fd87 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -872,22 +872,23 @@ class XmlMaterialProfile(InstanceContainer): machine_manufacturer = identifier.get("manufacturer", definition_metadata.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition. - if machine_compatibility: - new_material_id = container_id + "_" + machine_id + # Always create the instance of the material even if it is not compatible, otherwise it will never + # show as incompatible if the material profile doesn't define hotends in the machine - CURA-5444 + new_material_id = container_id + "_" + machine_id - # Do not look for existing container/container metadata with the same ID although they may exist. - # In project loading and perhaps some other places, we only want to get information (metadata) - # from a file without changing the current state of the system. If we overwrite the existing - # metadata here, deserializeMetadata() will not be safe for retrieving information. - new_material_metadata = {} + # Do not look for existing container/container metadata with the same ID although they may exist. + # In project loading and perhaps some other places, we only want to get information (metadata) + # from a file without changing the current state of the system. If we overwrite the existing + # metadata here, deserializeMetadata() will not be safe for retrieving information. + new_material_metadata = {} - new_material_metadata.update(base_metadata) - new_material_metadata["id"] = new_material_id - new_material_metadata["compatible"] = machine_compatibility - new_material_metadata["machine_manufacturer"] = machine_manufacturer - new_material_metadata["definition"] = machine_id + new_material_metadata.update(base_metadata) + new_material_metadata["id"] = new_material_id + new_material_metadata["compatible"] = machine_compatibility + new_material_metadata["machine_manufacturer"] = machine_manufacturer + new_material_metadata["definition"] = machine_id - result_metadata.append(new_material_metadata) + result_metadata.append(new_material_metadata) buildplates = machine.iterfind("./um:buildplate", cls.__namespaces) buildplate_map = {} From 310a99fba70b67253af482fa09ad7fe1e59679d3 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 11 Jun 2018 11:31:28 +0200 Subject: [PATCH 09/14] Fix deepcopy in SettingOverrideDecorator Obvious mistake... --- cura/Settings/SettingOverrideDecorator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Settings/SettingOverrideDecorator.py b/cura/Settings/SettingOverrideDecorator.py index a662027d8f..27ae1d69f0 100644 --- a/cura/Settings/SettingOverrideDecorator.py +++ b/cura/Settings/SettingOverrideDecorator.py @@ -63,7 +63,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): instance_container = copy.deepcopy(self._stack.getContainer(0), memo) # A unique name must be added, or replaceContainer will not replace it - instance_container.setMetaDataEntry("id", self._generateUniqueName) + instance_container.setMetaDataEntry("id", self._generateUniqueName()) ## Set the copied instance as the first (and only) instance container of the stack. deep_copy._stack.replaceContainer(0, instance_container) From 7a6e7112765c386c7d1ac1ac52fce132b67240ba Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 11 Jun 2018 13:29:34 +0200 Subject: [PATCH 10/14] Update SettingOverrideDecorator upon extruder enabled/disabled CURA-5456 When an extruder gets disabled, the SettingOverrideDecorator should update its associated extruder to an enabled one so the whole setup can be sliced. --- cura/Settings/SettingOverrideDecorator.py | 52 +++++++++++++++-------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/cura/Settings/SettingOverrideDecorator.py b/cura/Settings/SettingOverrideDecorator.py index 27ae1d69f0..f14977866e 100644 --- a/cura/Settings/SettingOverrideDecorator.py +++ b/cura/Settings/SettingOverrideDecorator.py @@ -9,6 +9,7 @@ from UM.Signal import Signal, signalemitter from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Logger import Logger +from UM.Util import parseBool from UM.Application import Application @@ -39,7 +40,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): user_container = InstanceContainer(container_id = self._generateUniqueName()) user_container.addMetaDataEntry("type", "user") self._stack.userChanges = user_container - self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId() + self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0) self._is_non_printing_mesh = False self._is_non_thumbnail_visible_mesh = False @@ -48,13 +49,25 @@ class SettingOverrideDecorator(SceneNodeDecorator): Application.getInstance().getContainerRegistry().addContainer(self._stack) - Application.getInstance().globalContainerStackChanged.connect(self._updateNextStack) + Application.getInstance().globalContainerStackChanged.connect(self._onNumberOfExtrudersEnabledChanged) + Application.getInstance().getMachineManager().numberExtrudersEnabledChanged.connect(self._onNumberOfExtrudersEnabledChanged) + self.activeExtruderChanged.connect(self._updateNextStack) self._updateNextStack() def _generateUniqueName(self): return "SettingOverrideInstanceContainer-%s" % uuid.uuid1() + def _onNumberOfExtrudersEnabledChanged(self, *args, **kwargs): + if not parseBool(self._extruder_stack.getMetaDataEntry("enabled", "True")): + # switch to the first extruder that's available + global_stack = Application.getInstance().getMachineManager().activeMachine + for _, extruder in sorted(list(global_stack.extruders.items())): + if parseBool(extruder.getMetaDataEntry("enabled", "True")): + self._extruder_stack = extruder + self._updateNextStack() + break + def __deepcopy__(self, memo): ## Create a fresh decorator object deep_copy = SettingOverrideDecorator() @@ -69,7 +82,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): deep_copy._stack.replaceContainer(0, instance_container) # Properly set the right extruder on the copy - deep_copy.setActiveExtruder(self._extruder_stack) + deep_copy.setActiveExtruder(self._extruder_stack.getId()) # use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh" # has not been updated yet. @@ -82,7 +95,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): # # \return An extruder's container stack. def getActiveExtruder(self): - return self._extruder_stack + return self._extruder_stack.getId() ## Gets the signal that emits if the active extruder changed. # @@ -124,20 +137,16 @@ class SettingOverrideDecorator(SceneNodeDecorator): # kept up to date. def _updateNextStack(self): if self._extruder_stack: - extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_stack) - if extruder_stack: - if self._stack.getNextStack(): - old_extruder_stack_id = self._stack.getNextStack().getId() - else: - old_extruder_stack_id = "" - - self._stack.setNextStack(extruder_stack[0]) - # Trigger slice/need slicing if the extruder changed. - if self._stack.getNextStack().getId() != old_extruder_stack_id: - Application.getInstance().getBackend().needsSlicing() - Application.getInstance().getBackend().tickle() + if self._stack.getNextStack(): + old_extruder_stack_id = self._stack.getNextStack().getId() else: - Logger.log("e", "Extruder stack %s below per-object settings does not exist.", self._extruder_stack) + old_extruder_stack_id = "" + + self._stack.setNextStack(self._extruder_stack) + # Trigger slice/need slicing if the extruder changed. + if self._stack.getNextStack().getId() != old_extruder_stack_id: + Application.getInstance().getBackend().needsSlicing() + Application.getInstance().getBackend().tickle() else: self._stack.setNextStack(Application.getInstance().getGlobalContainerStack()) @@ -145,7 +154,14 @@ class SettingOverrideDecorator(SceneNodeDecorator): # # \param extruder_stack_id The new extruder stack to print with. def setActiveExtruder(self, extruder_stack_id): - self._extruder_stack = extruder_stack_id + if self._extruder_stack.getId() == extruder_stack_id: + return + + global_stack = Application.getInstance().getMachineManager().activeMachine + for extruder in global_stack.extruders.values(): + if extruder.getId() == extruder_stack_id: + self._extruder_stack = extruder + break self._updateNextStack() ExtruderManager.getInstance().resetSelectedObjectExtruders() self.activeExtruderChanged.emit() From 74e3785a03b6575e0331d786f0c0ea66f7f93446 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 11 Jun 2018 13:42:36 +0200 Subject: [PATCH 11/14] Fix material update upon loading a machine CURA-5327 --- cura/Settings/MachineManager.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 39bda6a4a4..9abdd2cb96 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -306,6 +306,11 @@ class MachineManager(QObject): for position, extruder in global_stack.extruders.items(): material_dict[position] = extruder.material.getMetaDataEntry("base_file") self._current_root_material_id = material_dict + + # Update materials to make sure that the diameters match with the machine's + for position in global_stack.extruders: + self._updateMaterialWithVariant(position) + global_quality = global_stack.quality quality_type = global_quality.getMetaDataEntry("quality_type") global_quality_changes = global_stack.qualityChanges From 48bac1a0f70ed9e20a52131083205e2574d0f0d2 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 11 Jun 2018 14:45:43 +0200 Subject: [PATCH 12/14] Do not move locked nodes CURA-5347 --- cura/PlatformPhysics.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 6b539a4574..8ddcdbfb2f 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -8,6 +8,7 @@ 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.Scene.SceneNodeSettings import SceneNodeSettings from cura.Scene.ConvexHullDecorator import ConvexHullDecorator @@ -80,6 +81,10 @@ class PlatformPhysics: # only push away objects if this node is a printing mesh if not node.callDecoration("isNonPrintingMesh") and Application.getInstance().getPreferences().getValue("physics/automatic_push_free"): + # Do not move locked nodes + if node.getSetting(SceneNodeSettings.LockPosition): + continue + # Check for collisions between convex hulls for other_node in BreadthFirstIterator(root): # Ignore root, ourselves and anything that is not a normal SceneNode. From 2eedc96f36576fb93dedd982f4995becc10f4322 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 11 Jun 2018 15:00:35 +0200 Subject: [PATCH 13/14] Convert to Doxygen documentation Like the rest of Cura and Uranium. --- cura/API/Backups.py | 34 +++++++++++++---------------- cura/API/__init__.py | 13 ++++++------ cura/Backups/Backup.py | 39 +++++++++++++--------------------- cura/Backups/BackupsManager.py | 29 ++++++++++++------------- 4 files changed, 50 insertions(+), 65 deletions(-) diff --git a/cura/API/Backups.py b/cura/API/Backups.py index ba416bd870..a2423bd798 100644 --- a/cura/API/Backups.py +++ b/cura/API/Backups.py @@ -3,30 +3,26 @@ from cura.Backups.BackupsManager import BackupsManager +## The back-ups API provides a version-proof bridge between Cura's +# BackupManager and plug-ins that hook into it. +# +# Usage: +# ``from cura.API import CuraAPI +# api = CuraAPI() +# api.backups.createBackup() +# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})`` class Backups: - """ - The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it. - - Usage: - from cura.API import CuraAPI - api = CuraAPI() - api.backups.createBackup() - api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) - """ - manager = BackupsManager() # Re-used instance of the backups manager. + ## Create a new back-up using the BackupsManager. + # \return Tuple containing a ZIP file with the back-up data and a dict + # with metadata about the back-up. def createBackup(self) -> (bytes, dict): - """ - Create a new backup using the BackupsManager. - :return: Tuple containing a ZIP file with the backup data and a dict with meta data about the backup. - """ return self.manager.createBackup() + ## Restore a back-up using the BackupsManager. + # \param zip_file A ZIP file containing the actual back-up data. + # \param meta_data Some metadata needed for restoring a back-up, like the + # Cura version number. def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: - """ - Restore a backup using the BackupManager. - :param zip_file: A ZIP file containing the actual backup data. - :param meta_data: Some meta data needed for restoring a backup, like the Cura version number. - """ return self.manager.restoreBackup(zip_file, meta_data) diff --git a/cura/API/__init__.py b/cura/API/__init__.py index 7dd5d8f79e..13f6722336 100644 --- a/cura/API/__init__.py +++ b/cura/API/__init__.py @@ -3,14 +3,13 @@ from UM.PluginRegistry import PluginRegistry from cura.API.Backups import Backups - +## The official Cura API that plug-ins can use to interact with Cura. +# +# Python does not technically prevent talking to other classes as well, but +# this API provides a version-safe interface with proper deprecation warnings +# etc. Usage of any other methods than the ones provided in this API can cause +# plug-ins to be unstable. class CuraAPI: - """ - The official Cura API that plugins can use to interact with Cura. - Python does not technically prevent talking to other classes as well, - but this API provides a version-safe interface with proper deprecation warnings etc. - Usage of any other methods than the ones provided in this API can cause plugins to be unstable. - """ # For now we use the same API version to be consistent. VERSION = PluginRegistry.APIVersion diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index c4fe720b2b..70807a96d7 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -17,12 +17,11 @@ from UM.Resources import Resources from cura.CuraApplication import CuraApplication +## The back-up class holds all data about a back-up. +# +# It is also responsible for reading and writing the zip file to the user data +# folder. class Backup: - """ - The backup class holds all data about a backup. - It is also responsible for reading and writing the zip file to the user data folder. - """ - # These files should be ignored when making a backup. IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"] @@ -33,10 +32,8 @@ class Backup: self.zip_file = zip_file # type: Optional[bytes] self.meta_data = meta_data # type: Optional[dict] + ## Create a back-up from the current user config folder. def makeFromCurrent(self) -> (bool, Optional[str]): - """ - Create a backup from the current user config folder. - """ cura_release = CuraApplication.getInstance().getVersion() version_data_dir = Resources.getDataStoragePath() @@ -75,12 +72,10 @@ class Backup: "plugin_count": str(plugin_count) } + ## Make a full archive from the given root path with the given name. + # \param root_path The root directory to archive recursively. + # \return The archive as bytes. def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]: - """ - Make a full archive from the given root path with the given name. - :param root_path: The root directory to archive recursively. - :return: The archive as bytes. - """ ignore_string = re.compile("|".join(self.IGNORED_FILES)) try: archive = ZipFile(buffer, "w", ZIP_DEFLATED) @@ -99,15 +94,13 @@ class Backup: "Could not create archive from user data directory: {}".format(error))) return None + ## Show a UI message. def _showMessage(self, message: str) -> None: - """Show a UI message""" Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show() + ## Restore this back-up. + # \return Whether we had success or not. def restore(self) -> bool: - """ - Restore this backups - :return: A boolean whether we had success or not. - """ if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None): # We can restore without the minimum required information. Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.") @@ -140,14 +133,12 @@ class Backup: return extracted + ## Extract the whole archive to the given target path. + # \param archive The archive as ZipFile. + # \param target_path The target path. + # \return Whether we had success or not. @staticmethod def _extractArchive(archive: "ZipFile", target_path: str) -> bool: - """ - Extract the whole archive to the given target path. - :param archive: The archive as ZipFile. - :param target_path: The target path. - :return: A boolean whether we had success or not. - """ Logger.log("d", "Removing current data in location: %s", target_path) Resources.factoryReset() Logger.log("d", "Extracting backup to location: %s", target_path) diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index fa75ddb587..850b0a2edc 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -7,19 +7,18 @@ from cura.Backups.Backup import Backup from cura.CuraApplication import CuraApplication +## The BackupsManager is responsible for managing the creating and restoring of +# back-ups. +# +# Back-ups themselves are represented in a different class. class BackupsManager: - """ - The BackupsManager is responsible for managing the creating and restoring of backups. - Backups themselves are represented in a different class. - """ def __init__(self): self._application = CuraApplication.getInstance() + ## Get a back-up of the current configuration. + # \return A tuple containing a ZipFile (the actual back-up) and a dict + # containing some metadata (like version). def createBackup(self) -> (Optional[bytes], Optional[dict]): - """ - Get a backup of the current configuration. - :return: A Tuple containing a ZipFile (the actual backup) and a dict containing some meta data (like version). - """ self._disableAutoSave() backup = Backup() backup.makeFromCurrent() @@ -27,12 +26,11 @@ class BackupsManager: # We don't return a Backup here because we want plugins only to interact with our API and not full objects. return backup.zip_file, backup.meta_data + ## Restore a back-up from a given ZipFile. + # \param zip_file A bytes object containing the actual back-up. + # \param meta_data A dict containing some metadata that is needed to + # restore the back-up correctly. def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: - """ - Restore a backup from a given ZipFile. - :param zip_file: A bytes object containing the actual backup. - :param meta_data: A dict containing some meta data that is needed to restore the backup correctly. - """ if not meta_data.get("cura_release", None): # If there is no "cura_release" specified in the meta data, we don't execute a backup restore. Logger.log("w", "Tried to restore a backup without specifying a Cura version number.") @@ -47,10 +45,11 @@ class BackupsManager: # We don't want to store the data at this point as that would override the just-restored backup. self._application.windowClosed(save_data=False) + ## Here we try to disable the auto-save plug-in as it might interfere with + # restoring a back-up. def _disableAutoSave(self): - """Here we try to disable the auto-save plugin as it might interfere with restoring a backup.""" self._application.setSaveDataEnabled(False) + ## Re-enable auto-save after we're done. def _enableAutoSave(self): - """Re-enable auto-save after we're done.""" self._application.setSaveDataEnabled(True) From b3f6e5c9e0534e836325ffb7bc2fb4757cb59fc9 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 11 Jun 2018 15:52:28 +0200 Subject: [PATCH 14/14] CURA-5327 renamed MachineManager._updateMaterialWithVariant to updateMaterialWithVariant --- cura/Settings/MachineManager.py | 12 ++++++------ .../MachineSettingsAction/MachineSettingsAction.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 9abdd2cb96..1039085cf3 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -309,7 +309,7 @@ class MachineManager(QObject): # Update materials to make sure that the diameters match with the machine's for position in global_stack.extruders: - self._updateMaterialWithVariant(position) + self.updateMaterialWithVariant(position) global_quality = global_stack.quality quality_type = global_quality.getMetaDataEntry("quality_type") @@ -1205,7 +1205,7 @@ class MachineManager(QObject): current_quality_type, quality_type) self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True) - def _updateMaterialWithVariant(self, position: Optional[str]): + def updateMaterialWithVariant(self, position: Optional[str]): if self._global_container_stack is None: return if position is None: @@ -1291,7 +1291,7 @@ class MachineManager(QObject): self._setMaterial(position, material_container_node) else: self._global_container_stack.extruders[position].material = self._empty_material_container - self._updateMaterialWithVariant(position) + self.updateMaterialWithVariant(position) if configuration.buildplateConfiguration is not None: global_variant_container_node = self._variant_manager.getBuildplateVariantNode(self._global_container_stack.definition.getId(), configuration.buildplateConfiguration) @@ -1337,7 +1337,7 @@ class MachineManager(QObject): self.blurSettings.emit() with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): self._setGlobalVariant(container_node) - self._updateMaterialWithVariant(None) # Update all materials + self.updateMaterialWithVariant(None) # Update all materials self._updateQualityWithMaterial() @pyqtSlot(str, str) @@ -1374,7 +1374,7 @@ class MachineManager(QObject): self.blurSettings.emit() with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): self._setVariantNode(position, container_node) - self._updateMaterialWithVariant(position) + self.updateMaterialWithVariant(position) self._updateQualityWithMaterial() # See if we need to show the Discard or Keep changes screen @@ -1438,5 +1438,5 @@ class MachineManager(QObject): if self._global_container_stack is None: return with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): - self._updateMaterialWithVariant(None) + self.updateMaterialWithVariant(None) self._updateQualityWithMaterial() diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index 974ecf4b02..0f8e0fa1ca 100755 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -158,4 +158,4 @@ class MachineSettingsAction(MachineAction): @pyqtSlot(int) def updateMaterialForDiameter(self, extruder_position: int): # Updates the material container to a material that matches the material diameter set for the printer - self._application.getMachineManager()._updateMaterialWithVariant(extruder_position) + self._application.getMachineManager().updateMaterialWithVariant(extruder_position)