From 7f09859be28be7dd5a51d3448dd3d9cbe8d28c14 Mon Sep 17 00:00:00 2001 From: Vlad Gribinchuk Date: Fri, 28 Sep 2018 13:30:48 +0300 Subject: [PATCH 01/20] Settings for the "Minimum Support Area" and "Minimum Support Interface Area" features --- resources/definitions/fdmprinter.def.json | 58 +++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 823635a62a..cd489126bf 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -4083,6 +4083,20 @@ "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false }, + "minimum_support_area": + { + "label": "Minimum Support Area", + "description": "Minimum area size for support polygons. Polygons which area is smaller than this value will not be generated.", + "unit": "mm²", + "type": "float", + "default_value": 0.0, + "minimum_value": "0", + "enabled": "support_enable", + "limit_to_extruder": "support_infill_extruder_nr", + "settable_per_mesh": true, + "fabricate_enabled": true, + "intermediate_enabled": true + }, "support_interface_enable": { "label": "Enable Support Interface", @@ -4322,6 +4336,50 @@ } } }, + "minimum_interface_area": + { + "label": "Minimum Support Interface Area", + "description": "Minimum area size for support interface polygons. Polygons which area are smaller than this value will not be generated.", + "unit": "mm²", + "type": "float", + "default_value": 1.0, + "minimum_value": "0", + "minimum_value_warning": "minimum_support_area", + "limit_to_extruder": "support_interface_extruder_nr", + "enabled": "support_interface_enable and (support_enable or support_tree_enable)", + "settable_per_mesh": true, + "children": + { + "minimum_roof_area": + { + "label": "Minimum Support Roof Area", + "description": "Minimum area size for the roofs of the support. Polygons which area are smaller than this value will not be generated.", + "unit": "mm²", + "type": "float", + "default_value": 1.0, + "value": "extruderValue(support_roof_extruder_nr, 'minimum_interface_area')", + "minimum_value": "0", + "minimum_value_warning": "minimum_support_area", + "limit_to_extruder": "support_roof_extruder_nr", + "enabled": "support_roof_enable and (support_enable or support_tree_enable)", + "settable_per_mesh": true + }, + "minimum_bottom_area": + { + "label": "Minimum Support Floor Area", + "description": "Minimum area size for the floors of the support. Polygons which area are smaller than this value will not be generated.", + "unit": "mm²", + "type": "float", + "default_value": 1.0, + "value": "extruderValue(support_bottom_extruder_nr, 'minimum_interface_area')", + "minimum_value": "0", + "minimum_value_warning": "minimum_support_area", + "limit_to_extruder": "support_bottom_extruder_nr", + "enabled": "support_bottom_enable and (support_enable or support_tree_enable)", + "settable_per_mesh": true + } + } + }, "support_fan_enable": { "label": "Fan Speed Override", From 9c555bf67f1964fcc0055c7826f1097199548dc8 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 12 Nov 2018 11:02:43 +0100 Subject: [PATCH 02/20] Add typing and always add error message if loading failed There were some places where it would return None. Then in the QML it would give a QML error that the null object has no dictionary items. Contributes to issue CURA-5929. --- cura/Settings/ContainerManager.py | 8 ++++---- cura/Settings/CuraContainerRegistry.py | 15 +++++++-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 3cfca1a944..3c1e09f086 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -419,13 +419,13 @@ class ContainerManager(QObject): self._container_name_filters[name_filter] = entry ## Import single profile, file_url does not have to end with curaprofile - @pyqtSlot(QUrl, result="QVariantMap") - def importProfile(self, file_url: QUrl): + @pyqtSlot(QUrl, result = "QVariantMap") + def importProfile(self, file_url: QUrl) -> Dict[str, str]: if not file_url.isValid(): - return + return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + file_url} path = file_url.toLocalFile() if not path: - return + return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + file_url} return self._container_registry.importProfile(path) @pyqtSlot(QObject, QUrl, str) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 11640adc0f..63277ebce5 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -5,8 +5,7 @@ import os import re import configparser -from typing import cast, Optional - +from typing import cast, Dict, Optional from PyQt5.QtWidgets import QMessageBox from UM.Decorators import override @@ -161,20 +160,20 @@ class CuraContainerRegistry(ContainerRegistry): ## Imports a profile from a file # - # \param file_name \type{str} the full path and filename of the profile to import - # \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key - # containing a message for the user - def importProfile(self, file_name): + # \param file_name The full path and filename of the profile to import. + # \return Dict with a 'status' key containing the string 'ok' or 'error', + # and a 'message' key containing a message for the user. + def importProfile(self, file_name: str) -> Dict[str, str]: Logger.log("d", "Attempting to import profile %s", file_name) if not file_name: - return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags or !", "Failed to import profile from {0}: {1}", file_name, "Invalid path")} + return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Failed to import profile from {0}: {1}", file_name, "Invalid path")} plugin_registry = PluginRegistry.getInstance() extension = file_name.split(".")[-1] global_stack = Application.getInstance().getGlobalContainerStack() if not global_stack: - return + return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Can't import profile from {0} before a printer is added.", file_name)} machine_extruders = [] for position in sorted(global_stack.extruders): From 9d94b0d63eaf1db2b7e08a7ce7f4ae848df98f9e Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 12 Nov 2018 11:16:33 +0100 Subject: [PATCH 03/20] Rename input vs. output parsers Technically you could re-use the variable name but that is confusing. Contributes to issue CURA-5929. --- .../LegacyProfileReader.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/LegacyProfileReader/LegacyProfileReader.py b/plugins/LegacyProfileReader/LegacyProfileReader.py index cd577218d5..b5b2379b22 100644 --- a/plugins/LegacyProfileReader/LegacyProfileReader.py +++ b/plugins/LegacyProfileReader/LegacyProfileReader.py @@ -82,9 +82,9 @@ class LegacyProfileReader(ProfileReader): profile_id = container_registry.uniqueName("Imported Legacy Profile") profile = InstanceContainer(profile_id) # Create an empty profile. - parser = configparser.ConfigParser(interpolation = None) + input_parser = configparser.ConfigParser(interpolation = None) try: - parser.read([file_name]) # Parse the INI file. + input_parser.read([file_name]) # Parse the INI file. except Exception as e: Logger.log("e", "Unable to open legacy profile %s: %s", file_name, str(e)) return None @@ -92,7 +92,7 @@ class LegacyProfileReader(ProfileReader): # Legacy Cura saved the profile under the section "profile_N" where N is the ID of a machine, except when you export in which case it saves it in the section "profile". # Since importing multiple machine profiles is out of scope, just import the first section we find. section = "" - for found_section in parser.sections(): + for found_section in input_parser.sections(): if found_section.startswith("profile"): section = found_section break @@ -110,7 +110,7 @@ class LegacyProfileReader(ProfileReader): return None defaults = self.prepareDefaults(dict_of_doom) - legacy_settings = self.prepareLocals(parser, section, defaults) #Gets the settings from the legacy profile. + legacy_settings = self.prepareLocals(input_parser, section, defaults) #Gets the settings from the legacy profile. #Check the target version in the Dictionary of Doom with this application version. if "target_version" not in dict_of_doom: @@ -152,15 +152,15 @@ class LegacyProfileReader(ProfileReader): profile.setDirty(True) #Serialise and deserialise in order to perform the version upgrade. - parser = configparser.ConfigParser(interpolation = None) + output_parser = configparser.ConfigParser(interpolation = None) data = profile.serialize() - parser.read_string(data) - parser["general"]["version"] = "1" - if parser.has_section("values"): - parser["settings"] = parser["values"] - del parser["values"] + output_parser.read_string(data) + output_parser["general"]["version"] = "1" + if output_parser.has_section("values"): + output_parser["settings"] = output_parser["values"] + del output_parser["values"] stream = io.StringIO() - parser.write(stream) + output_parser.write(stream) data = stream.getvalue() profile.deserialize(data) From 6ad682b00db9e614ff40eb530133247744e02658 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 12 Nov 2018 12:02:49 +0100 Subject: [PATCH 04/20] Don't serialise legacy profile via Profile instance That profile instance was being explicitly set to version 1 (but then serialised as version 4) and then deserialised with upgrade, so the upgrade was thinking it was upgrading from version 1 to 4, but it was actually upgrading a file which was already at version 4. We shouldn't use the Profile() instance at all but just perform the upgrade on simple string data generated by the configparser. This also updates the format to the newest version (since that was easiest for me to reimplement) but we don't need to ever update this again because it gets passed through the version upgrade system, which upgrades it from version 4000005 to the latest version in the future. Contributes to issue CURA-5929. --- .../LegacyProfileReader.py | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/plugins/LegacyProfileReader/LegacyProfileReader.py b/plugins/LegacyProfileReader/LegacyProfileReader.py index b5b2379b22..b915242d55 100644 --- a/plugins/LegacyProfileReader/LegacyProfileReader.py +++ b/plugins/LegacyProfileReader/LegacyProfileReader.py @@ -80,7 +80,6 @@ class LegacyProfileReader(ProfileReader): Logger.log("i", "Importing legacy profile from file " + file_name + ".") container_registry = ContainerRegistry.getInstance() profile_id = container_registry.uniqueName("Imported Legacy Profile") - profile = InstanceContainer(profile_id) # Create an empty profile. input_parser = configparser.ConfigParser(interpolation = None) try: @@ -112,13 +111,11 @@ class LegacyProfileReader(ProfileReader): defaults = self.prepareDefaults(dict_of_doom) legacy_settings = self.prepareLocals(input_parser, section, defaults) #Gets the settings from the legacy profile. - #Check the target version in the Dictionary of Doom with this application version. - if "target_version" not in dict_of_doom: - Logger.log("e", "Dictionary of Doom has no target version. Is it the correct JSON file?") - return None - if InstanceContainer.Version != dict_of_doom["target_version"]: - Logger.log("e", "Dictionary of Doom of legacy profile reader (version %s) is not in sync with the current instance container version (version %s)!", dict_of_doom["target_version"], str(InstanceContainer.Version)) - return None + # Serialised format into version 4.5. Do NOT upgrade this, let the version upgrader handle it. + output_parser = configparser.ConfigParser(interpolation = None) + output_parser.add_section("general") + output_parser.add_section("metadata") + output_parser.add_section("values") if "translation" not in dict_of_doom: Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?") @@ -127,7 +124,7 @@ class LegacyProfileReader(ProfileReader): quality_definition = current_printer_definition.getMetaDataEntry("quality_definition") if not quality_definition: quality_definition = current_printer_definition.getId() - profile.setDefinition(quality_definition) + output_parser["general"]["definition"] = 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") @@ -140,37 +137,34 @@ class LegacyProfileReader(ProfileReader): definitions = current_printer_definition.findDefinitions(key = new_setting) if definitions: if new_value != value_using_defaults and definitions[0].default_value != new_value: # Not equal to the default in the new Cura OR the default in the legacy Cura. - profile.setProperty(new_setting, "value", new_value) # Store the setting in the profile! + output_parser["values"][new_setting] = new_value # Store the setting in the profile! - if len(profile.getAllKeys()) == 0: + if len(output_parser["values"]) == 0: Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.") - profile.setMetaDataEntry("type", "profile") - # don't know what quality_type it is based on, so use "normal" by default - profile.setMetaDataEntry("quality_type", "normal") - profile.setName(profile_id) - profile.setDirty(True) + output_parser["general"]["version"] = "4" + output_parser["general"]["name"] = profile_id + output_parser["metadata"]["type"] = "quality_changes" + output_parser["metadata"]["quality_type"] = "normal" # Don't know what quality_type it is based on, so use "normal" by default. + output_parser["metadata"]["position"] = "0" # We only support single extrusion. + output_parser["metadata"]["setting_version"] = "5" # What the dictionary of doom is made for. - #Serialise and deserialise in order to perform the version upgrade. - output_parser = configparser.ConfigParser(interpolation = None) - data = profile.serialize() - output_parser.read_string(data) - output_parser["general"]["version"] = "1" - if output_parser.has_section("values"): - output_parser["settings"] = output_parser["values"] - del output_parser["values"] + # Serialise in order to perform the version upgrade. stream = io.StringIO() output_parser.write(stream) 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) + profile = InstanceContainer(profile_id) + profile.deserialize(data) # Also performs the version upgrade. + profile.setDirty(True) #We need to return one extruder stack and one global stack. global_container_id = container_registry.uniqueName("Global Imported Legacy Profile") + # We duplicate the extruder profile into the global stack. + # This may introduce some settings that are global in the extruder stack and some settings that are per-extruder in the global stack. + # We don't care about that. The engine will ignore them anyway. global_profile = profile.duplicate(new_id = global_container_id, new_name = profile_id) #Needs to have the same name as the extruder profile. + del global_profile.getMetaData()["position"] # Has no position because it's global. global_profile.setDirty(True) profile_definition = "fdmprinter" From 6022ed0f2347001efb013cdbad7ba952fb532ab4 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 12 Nov 2018 12:06:00 +0100 Subject: [PATCH 05/20] Update target version in DoD There are no changes to these settings, luckily. This target version is now only here for documentation (like source_version was). It is no longer actually used by the code. Contributes to issue CURA-5929. --- plugins/LegacyProfileReader/DictionaryOfDoom.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/LegacyProfileReader/DictionaryOfDoom.json b/plugins/LegacyProfileReader/DictionaryOfDoom.json index 0be413dd2c..f65cc271d1 100644 --- a/plugins/LegacyProfileReader/DictionaryOfDoom.json +++ b/plugins/LegacyProfileReader/DictionaryOfDoom.json @@ -1,6 +1,6 @@ { "source_version": "15.04", - "target_version": 3, + "target_version": "4.5", "translation": { "machine_nozzle_size": "nozzle_size", From 27aff4e5da270f446e03545f69247f7e38abc3d3 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 12 Nov 2018 12:48:49 +0100 Subject: [PATCH 06/20] Fix typing issues Because this function now has typing, it's raising a load of issues with it. Contributes to issue CURA-5929. --- cura/Machines/QualityManager.py | 4 ++-- cura/Settings/CuraContainerRegistry.py | 24 ++++++++++--------- .../LegacyProfileReader.py | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/cura/Machines/QualityManager.py b/cura/Machines/QualityManager.py index fc8262de52..a784d17f0b 100644 --- a/cura/Machines/QualityManager.py +++ b/cura/Machines/QualityManager.py @@ -16,7 +16,7 @@ from .QualityGroup import QualityGroup from .QualityNode import QualityNode if TYPE_CHECKING: - from UM.Settings.DefinitionContainer import DefinitionContainer + from UM.Settings.Interfaces import DefinitionContainerInterface from cura.Settings.GlobalStack import GlobalStack from .QualityChangesGroup import QualityChangesGroup from cura.CuraApplication import CuraApplication @@ -538,7 +538,7 @@ class QualityManager(QObject): # Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended # shares the same set of qualities profiles as Ultimaker 3. # -def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainer", +def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainerInterface", default_definition_id: str = "fdmprinter") -> str: machine_definition_id = default_definition_id if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)): diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 63277ebce5..9f44d075e0 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -10,6 +10,7 @@ from PyQt5.QtWidgets import QMessageBox from UM.Decorators import override from UM.Settings.ContainerFormatError import ContainerFormatError +from UM.Settings.Interfaces import ContainerInterface from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerStack import ContainerStack from UM.Settings.InstanceContainer import InstanceContainer @@ -27,7 +28,7 @@ from . import GlobalStack import cura.CuraApplication from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch -from cura.ReaderWriters.ProfileReader import NoProfileException +from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") @@ -182,7 +183,7 @@ class CuraContainerRegistry(ContainerRegistry): for plugin_id, meta_data in self._getIOPlugins("profile_reader"): if meta_data["profile_reader"][0]["extension"] != extension: continue - profile_reader = plugin_registry.getPluginObject(plugin_id) + profile_reader = cast(ProfileReader, plugin_registry.getPluginObject(plugin_id)) try: profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader. except NoProfileException: @@ -220,13 +221,13 @@ class CuraContainerRegistry(ContainerRegistry): # Make sure we have a profile_definition in the file: if profile_definition is None: break - machine_definition = self.findDefinitionContainers(id = profile_definition) - if not machine_definition: + machine_definitions = self.findDefinitionContainers(id = profile_definition) + if not machine_definitions: Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition) return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "This profile {0} contains incorrect data, could not import it.", file_name) } - machine_definition = machine_definition[0] + machine_definition = machine_definitions[0] # Get the expected machine definition. # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... @@ -273,11 +274,12 @@ class CuraContainerRegistry(ContainerRegistry): setting_value = global_profile.getProperty(qc_setting_key, "value") setting_definition = global_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) + if setting_definition is not None: + 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) @@ -289,7 +291,7 @@ class CuraContainerRegistry(ContainerRegistry): for profile_index, profile in enumerate(profile_or_list): if profile_index == 0: # This is assumed to be the global profile - profile_id = (global_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_") + profile_id = (cast(ContainerInterface, global_stack.getBottom()).getId() + "_" + name_seed).lower().replace(" ", "_") elif profile_index < len(machine_extruders) + 1: # This is assumed to be an extruder profile diff --git a/plugins/LegacyProfileReader/LegacyProfileReader.py b/plugins/LegacyProfileReader/LegacyProfileReader.py index b915242d55..9a74c15082 100644 --- a/plugins/LegacyProfileReader/LegacyProfileReader.py +++ b/plugins/LegacyProfileReader/LegacyProfileReader.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import configparser # For reading the legacy profile INI files. From 6d1b64465a8142c7d9e52b4d3a48ac4e5438cf96 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 12 Nov 2018 12:50:18 +0100 Subject: [PATCH 07/20] Fix URL in error message Contributes to issue CURA-5929. --- cura/Settings/ContainerManager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 3c1e09f086..133e04e8fc 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -422,10 +422,10 @@ class ContainerManager(QObject): @pyqtSlot(QUrl, result = "QVariantMap") def importProfile(self, file_url: QUrl) -> Dict[str, str]: if not file_url.isValid(): - return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + file_url} + return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)} path = file_url.toLocalFile() if not path: - return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + file_url} + return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)} return self._container_registry.importProfile(path) @pyqtSlot(QObject, QUrl, str) From bbbb08c793e45b49e0421daf900afdc2bda31a81 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 12 Nov 2018 13:02:28 +0100 Subject: [PATCH 08/20] Add test for prepareDefaults Contributes to issue CURA-5929. --- .../tests/TestLegacyProfileReader.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py diff --git a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py new file mode 100644 index 0000000000..9f6ffe4f24 --- /dev/null +++ b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py @@ -0,0 +1,31 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import pytest #To register tests with. + +from LegacyProfileReader import LegacyProfileReader #The module we're testing. + +@pytest.fixture +def legacy_profile_reader(): + return LegacyProfileReader() + +test_prepareDefaultsData = [ + { + "defaults": { + "foo": "bar" + }, + "cheese": "delicious" + }, + { + "cat": "fluffy", + "dog": "floofy" + } +] + +@pytest.mark.parametrize("input", test_prepareDefaultsData) +def test_prepareDefaults(legacy_profile_reader, input): + output = legacy_profile_reader.prepareDefaults(input) + if "defaults" in input: + assert input["defaults"] == output + else: + assert output == {} \ No newline at end of file From 0bf7bf4cbe5f242c6f520d947064631bb5dd7317 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 12 Nov 2018 13:33:44 +0100 Subject: [PATCH 09/20] Fix handling dictionaries without 'defaults' section According to the test, this should return an empty dict then. Contributes to issue CURA-5929. --- plugins/LegacyProfileReader/LegacyProfileReader.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/LegacyProfileReader/LegacyProfileReader.py b/plugins/LegacyProfileReader/LegacyProfileReader.py index 9a74c15082..7974bbe829 100644 --- a/plugins/LegacyProfileReader/LegacyProfileReader.py +++ b/plugins/LegacyProfileReader/LegacyProfileReader.py @@ -6,6 +6,7 @@ import io import json # For reading the Dictionary of Doom. import math # For mathematical operations included in the Dictionary of Doom. import os.path # For concatenating the path to the plugin and the relative path to the Dictionary of Doom. +from typing import Dict from UM.Application import Application # To get the machine manager to create the new profile in. from UM.Logger import Logger # Logging errors. @@ -33,10 +34,11 @@ class LegacyProfileReader(ProfileReader): # \param json The JSON file to load the default setting values from. This # should not be a URL but a pre-loaded JSON handle. # \return A dictionary of the default values of the legacy Cura version. - def prepareDefaults(self, json): + def prepareDefaults(self, json: Dict[str, Dict[str, str]]) -> Dict[str, str]: defaults = {} - for key in json["defaults"]: # We have to copy over all defaults from the JSON handle to a normal dict. - defaults[key] = json["defaults"][key] + if "defaults" in json: + for key in json["defaults"]: # We have to copy over all defaults from the JSON handle to a normal dict. + defaults[key] = json["defaults"][key] return defaults ## Prepares the local variables that can be used in evaluation of computing From 91e8c177fe2c7b999d7724112c4bcb71d9e8912b Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 12 Nov 2018 13:35:18 +0100 Subject: [PATCH 10/20] Add test for prepareLocal Contributes to issue CURA-5929. --- .../tests/TestLegacyProfileReader.py | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py index 9f6ffe4f24..f54939e884 100644 --- a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py +++ b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py @@ -1,9 +1,10 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import pytest #To register tests with. +import configparser # An input for some functions we're testing. +import pytest # To register tests with. -from LegacyProfileReader import LegacyProfileReader #The module we're testing. +from LegacyProfileReader import LegacyProfileReader # The module we're testing. @pytest.fixture def legacy_profile_reader(): @@ -11,7 +12,8 @@ def legacy_profile_reader(): test_prepareDefaultsData = [ { - "defaults": { + "defaults": + { "foo": "bar" }, "cheese": "delicious" @@ -28,4 +30,37 @@ def test_prepareDefaults(legacy_profile_reader, input): if "defaults" in input: assert input["defaults"] == output else: - assert output == {} \ No newline at end of file + assert output == {} + +test_prepareLocalsData = [ + ( + { # Parser data. + "profile": + { + "layer_height": "0.2", + "infill_density": "30" + } + }, + "profile", # Config section. + { # Defaults. + "layer_height": "0.1", + "infill_density": "20", + "line_width": "0.4" + } + ) +] + +@pytest.mark.parametrize("parser_data, config_section, defaults", test_prepareLocalsData) +def test_prepareLocals(legacy_profile_reader, parser_data, config_section, defaults): + parser = configparser.ConfigParser() + parser.read_dict(parser_data) + + output = legacy_profile_reader.prepareLocals(parser, config_section, defaults) + + assert set(defaults.keys()) <= set(output.keys()) # All defaults must be in there. + assert set(parser_data[config_section]) <= set(output.keys()) # All overwritten values must be in there. + for key in output: + if key in parser_data[config_section]: + assert output[key] == parser_data[config_section][key] # If overwritten, must be the overwritten value. + else: + assert output[key] == defaults[key] # Otherwise must be equal to the default. \ No newline at end of file From 53c9cdc3fe7d0a6c4efa0bd2223edd15e9be71a1 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 12 Nov 2018 13:42:14 +0100 Subject: [PATCH 11/20] Add alternative scenarios for prepareLocals Contributes to issue CURA-5929. --- .../tests/TestLegacyProfileReader.py | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py index f54939e884..6d730ed786 100644 --- a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py +++ b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py @@ -33,7 +33,7 @@ def test_prepareDefaults(legacy_profile_reader, input): assert output == {} test_prepareLocalsData = [ - ( + ( # Ordinary case. { # Parser data. "profile": { @@ -47,6 +47,56 @@ test_prepareLocalsData = [ "infill_density": "20", "line_width": "0.4" } + ), + ( # Empty data. + { # Parser data. + "profile": + { + } + }, + "profile", # Config section. + { # Defaults. + } + ), + ( # All defaults. + { # Parser data. + "profile": + { + } + }, + "profile", # Config section. + { # Defaults. + "foo": "bar", + "boo": "far" + } + ), + ( # Multiple config sections. + { # Parser data. + "some_other_name": + { + "foo": "bar" + }, + "profile": + { + "foo": "baz" #Not the same as in some_other_name + } + }, + "profile", # Config section. + { # Defaults. + "foo": "bla" + } + ), + ( # Section does not exist. + { # Parser data. + "some_other_name": + { + "foo": "bar" + }, + }, + "profile", # Config section. + { # Defaults. + "foo": "baz" + } ) ] From e18ea4bca4d37d2feabcd47e0bf42cf36fedac68 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 12 Nov 2018 13:47:24 +0100 Subject: [PATCH 12/20] Expect a NoSectionError if testing with a section that is missing We want to get that error in order to debug. Contributes to issue CURA-5929. --- .../tests/TestLegacyProfileReader.py | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py index 6d730ed786..f20658ae5d 100644 --- a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py +++ b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py @@ -85,18 +85,6 @@ test_prepareLocalsData = [ { # Defaults. "foo": "bla" } - ), - ( # Section does not exist. - { # Parser data. - "some_other_name": - { - "foo": "bar" - }, - }, - "profile", # Config section. - { # Defaults. - "foo": "baz" - } ) ] @@ -113,4 +101,28 @@ def test_prepareLocals(legacy_profile_reader, parser_data, config_section, defau if key in parser_data[config_section]: assert output[key] == parser_data[config_section][key] # If overwritten, must be the overwritten value. else: - assert output[key] == defaults[key] # Otherwise must be equal to the default. \ No newline at end of file + assert output[key] == defaults[key] # Otherwise must be equal to the default. + +test_prepareLocalsNoSectionErrorData = [ + ( # Section does not exist. + { # Parser data. + "some_other_name": + { + "foo": "bar" + }, + }, + "profile", # Config section. + { # Defaults. + "foo": "baz" + } + ) +] + +## Test cases where a key error is expected. +@pytest.mark.parametrize("parser_data, config_section, defaults", test_prepareLocalsNoSectionErrorData) +def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, config_section, defaults): + parser = configparser.ConfigParser() + parser.read_dict(parser_data) + + with pytest.raises(configparser.NoSectionError): + legacy_profile_reader.prepareLocals(parser, config_section, defaults) \ No newline at end of file From 7e87a303cbf54d36c9404f2a1060417ae6dcc8e4 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 12 Nov 2018 13:49:13 +0100 Subject: [PATCH 13/20] Remove config_section parameter Just always take 'profile'. We don't need to test with anything else. Just adjust the data to it. Contributes to issue CURA-5929. --- .../tests/TestLegacyProfileReader.py | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py index f20658ae5d..828c14bbf5 100644 --- a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py +++ b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py @@ -41,7 +41,6 @@ test_prepareLocalsData = [ "infill_density": "30" } }, - "profile", # Config section. { # Defaults. "layer_height": "0.1", "infill_density": "20", @@ -54,7 +53,6 @@ test_prepareLocalsData = [ { } }, - "profile", # Config section. { # Defaults. } ), @@ -64,7 +62,6 @@ test_prepareLocalsData = [ { } }, - "profile", # Config section. { # Defaults. "foo": "bar", "boo": "far" @@ -81,25 +78,24 @@ test_prepareLocalsData = [ "foo": "baz" #Not the same as in some_other_name } }, - "profile", # Config section. { # Defaults. "foo": "bla" } ) ] -@pytest.mark.parametrize("parser_data, config_section, defaults", test_prepareLocalsData) -def test_prepareLocals(legacy_profile_reader, parser_data, config_section, defaults): +@pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsData) +def test_prepareLocals(legacy_profile_reader, parser_data, defaults): parser = configparser.ConfigParser() parser.read_dict(parser_data) - output = legacy_profile_reader.prepareLocals(parser, config_section, defaults) + output = legacy_profile_reader.prepareLocals(parser, "profile", defaults) assert set(defaults.keys()) <= set(output.keys()) # All defaults must be in there. - assert set(parser_data[config_section]) <= set(output.keys()) # All overwritten values must be in there. + assert set(parser_data["profile"]) <= set(output.keys()) # All overwritten values must be in there. for key in output: - if key in parser_data[config_section]: - assert output[key] == parser_data[config_section][key] # If overwritten, must be the overwritten value. + if key in parser_data["profile"]: + assert output[key] == parser_data["profile"][key] # If overwritten, must be the overwritten value. else: assert output[key] == defaults[key] # Otherwise must be equal to the default. @@ -111,7 +107,6 @@ test_prepareLocalsNoSectionErrorData = [ "foo": "bar" }, }, - "profile", # Config section. { # Defaults. "foo": "baz" } @@ -119,10 +114,10 @@ test_prepareLocalsNoSectionErrorData = [ ] ## Test cases where a key error is expected. -@pytest.mark.parametrize("parser_data, config_section, defaults", test_prepareLocalsNoSectionErrorData) -def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, config_section, defaults): +@pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsNoSectionErrorData) +def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, defaults): parser = configparser.ConfigParser() parser.read_dict(parser_data) with pytest.raises(configparser.NoSectionError): - legacy_profile_reader.prepareLocals(parser, config_section, defaults) \ No newline at end of file + legacy_profile_reader.prepareLocals(parser, "profile", defaults) \ No newline at end of file From 7bd55f5064ae86ceadc0653b6b9f05df1186f2e1 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 12 Nov 2018 16:28:29 +0100 Subject: [PATCH 14/20] Add test for read() function This is complex. I don't want to get into the actual translations from the DoD here. Contributes to issue CURA-5929. --- .../tests/TestLegacyProfileReader.py | 69 ++++++++++++++++++- .../LegacyProfileReader/tests/normal_case.ini | 7 ++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 plugins/LegacyProfileReader/tests/normal_case.ini diff --git a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py index 828c14bbf5..480a61f301 100644 --- a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py +++ b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py @@ -2,8 +2,16 @@ # Cura is released under the terms of the LGPLv3 or higher. import configparser # An input for some functions we're testing. +import os.path # To find the integration test .ini files. import pytest # To register tests with. +import unittest.mock # To mock the application, plug-in and container registry out. +import UM.Application # To mock the application out. +import UM.PluginRegistry # To mock the plug-in registry out. +import UM.Settings.ContainerRegistry # To mock the container registry out. +import UM.Settings.InstanceContainer # To intercept the serialised data from the read() function. + +import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module. from LegacyProfileReader import LegacyProfileReader # The module we're testing. @pytest.fixture @@ -120,4 +128,63 @@ def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, default parser.read_dict(parser_data) with pytest.raises(configparser.NoSectionError): - legacy_profile_reader.prepareLocals(parser, "profile", defaults) \ No newline at end of file + legacy_profile_reader.prepareLocals(parser, "profile", defaults) + +intercepted_data = "" + +@pytest.mark.parametrize("file_name", ["normal_case.ini"]) +def test_read(legacy_profile_reader, file_name): + # Mock out all dependencies. Quite a lot! + global_stack = unittest.mock.MagicMock() + global_stack.getProperty = unittest.mock.MagicMock(return_value = 1) # For machine_extruder_count setting. + def getMetaDataEntry(key, default_value = ""): + if key == "quality_definition": + return "mocked_quality_definition" + if key == "has_machine_quality": + return "True" + global_stack.definition.getMetaDataEntry = getMetaDataEntry + global_stack.definition.getId = unittest.mock.MagicMock(return_value = "mocked_global_definition") + application = unittest.mock.MagicMock() + application.getGlobalContainerStack = unittest.mock.MagicMock(return_value = global_stack) + application_getInstance = unittest.mock.MagicMock(return_value = application) + container_registry = unittest.mock.MagicMock() + container_registry_getInstance = unittest.mock.MagicMock(return_value = container_registry) + container_registry.uniqueName = unittest.mock.MagicMock(return_value = "Imported Legacy Profile") + container_registry.findDefinitionContainers = unittest.mock.MagicMock(return_value = [global_stack.definition]) + UM.Settings.InstanceContainer.setContainerRegistry(container_registry) + plugin_registry = unittest.mock.MagicMock() + plugin_registry_getInstance = unittest.mock.MagicMock(return_value = plugin_registry) + plugin_registry.getPluginPath = unittest.mock.MagicMock(return_value = os.path.dirname(LegacyProfileReaderModule.__file__)) + + # Mock out the resulting InstanceContainer so that we can intercept the data before it's passed through the version upgrader. + def deserialize(self, data): # Intercepts the serialised data that we'd perform the version upgrade from when deserializing. + global intercepted_data + intercepted_data = data + + parser = configparser.ConfigParser() + parser.read_string(data) + self._metadata["position"] = parser["metadata"]["position"] + def duplicate(self, new_id, new_name): + self._metadata["id"] = new_id + self._metadata["name"] = new_name + return self + + with unittest.mock.patch.object(UM.Application.Application, "getInstance", application_getInstance): + with unittest.mock.patch.object(UM.Settings.ContainerRegistry.ContainerRegistry, "getInstance", container_registry_getInstance): + with unittest.mock.patch.object(UM.PluginRegistry.PluginRegistry, "getInstance", plugin_registry_getInstance): + with unittest.mock.patch.object(UM.Settings.InstanceContainer.InstanceContainer, "deserialize", deserialize): + with unittest.mock.patch.object(UM.Settings.InstanceContainer.InstanceContainer, "duplicate", duplicate): + result = legacy_profile_reader.read(os.path.join(os.path.dirname(__file__), file_name)) + + assert len(result) == 1 + + # Let's see what's inside the actual output file that we generated. + parser = configparser.ConfigParser() + parser.read_string(intercepted_data) + assert parser["general"]["definition"] == "mocked_quality_definition" + assert parser["general"]["version"] == "4" # Yes, before we upgraded. + assert parser["general"]["name"] == "Imported Legacy Profile" # Because we overwrote uniqueName. + assert parser["metadata"]["type"] == "quality_changes" + assert parser["metadata"]["quality_type"] == "normal" + assert parser["metadata"]["position"] == "0" + assert parser["metadata"]["setting_version"] == "5" # Yes, before we upgraded. \ No newline at end of file diff --git a/plugins/LegacyProfileReader/tests/normal_case.ini b/plugins/LegacyProfileReader/tests/normal_case.ini new file mode 100644 index 0000000000..213444d2d3 --- /dev/null +++ b/plugins/LegacyProfileReader/tests/normal_case.ini @@ -0,0 +1,7 @@ +[profile] +foo = bar +boo = far +fill_overlap = 3 + +[alterations] +some = values From 87cbc3907c1a4912e30ca60f30d00d5b84fbef51 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 13 Nov 2018 12:05:14 +0100 Subject: [PATCH 15/20] Convert to string before storing in configparser Because configparser can only handle strings. Contributes to issue CURA-5929. --- plugins/LegacyProfileReader/LegacyProfileReader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/LegacyProfileReader/LegacyProfileReader.py b/plugins/LegacyProfileReader/LegacyProfileReader.py index 7974bbe829..013bab6f11 100644 --- a/plugins/LegacyProfileReader/LegacyProfileReader.py +++ b/plugins/LegacyProfileReader/LegacyProfileReader.py @@ -139,7 +139,7 @@ class LegacyProfileReader(ProfileReader): definitions = current_printer_definition.findDefinitions(key = new_setting) if definitions: if new_value != value_using_defaults and definitions[0].default_value != new_value: # Not equal to the default in the new Cura OR the default in the legacy Cura. - output_parser["values"][new_setting] = new_value # Store the setting in the profile! + output_parser["values"][new_setting] = str(new_value) # Store the setting in the profile! if len(output_parser["values"]) == 0: Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.") From 6307784a7940e4942ea964068b6a1135329071ed Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 13 Nov 2018 16:50:00 +0100 Subject: [PATCH 16/20] Fix some English grammar. [CURA-5903] --- resources/definitions/fdmprinter.def.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 3b3194f1dc..e67608f579 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -4142,7 +4142,7 @@ "minimum_support_area": { "label": "Minimum Support Area", - "description": "Minimum area size for support polygons. Polygons which area is smaller than this value will not be generated.", + "description": "Minimum area size for support polygons. Polygons which have an area smaller than this value will not be generated.", "unit": "mm²", "type": "float", "default_value": 0.0, @@ -4395,7 +4395,7 @@ "minimum_interface_area": { "label": "Minimum Support Interface Area", - "description": "Minimum area size for support interface polygons. Polygons which area are smaller than this value will not be generated.", + "description": "Minimum area size for support interface polygons. Polygons which have an area smaller than this value will not be generated.", "unit": "mm²", "type": "float", "default_value": 1.0, @@ -4409,7 +4409,7 @@ "minimum_roof_area": { "label": "Minimum Support Roof Area", - "description": "Minimum area size for the roofs of the support. Polygons which area are smaller than this value will not be generated.", + "description": "Minimum area size for the roofs of the support. Polygons which have an area smaller than this value will not be generated.", "unit": "mm²", "type": "float", "default_value": 1.0, @@ -4423,7 +4423,7 @@ "minimum_bottom_area": { "label": "Minimum Support Floor Area", - "description": "Minimum area size for the floors of the support. Polygons which area are smaller than this value will not be generated.", + "description": "Minimum area size for the floors of the support. Polygons which have an area smaller than this value will not be generated.", "unit": "mm²", "type": "float", "default_value": 1.0, From 13a9bb7eba2abf2a2790f732165b557ff7d4ec71 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 13 Nov 2018 17:03:27 +0100 Subject: [PATCH 17/20] Remove enables for min. support area in case of TreeSupport. [CURA-5903] --- resources/definitions/fdmprinter.def.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index e67608f579..325e4b6c79 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -4402,7 +4402,7 @@ "minimum_value": "0", "minimum_value_warning": "minimum_support_area", "limit_to_extruder": "support_interface_extruder_nr", - "enabled": "support_interface_enable and (support_enable or support_tree_enable)", + "enabled": "support_interface_enable and support_enable", "settable_per_mesh": true, "children": { @@ -4417,7 +4417,7 @@ "minimum_value": "0", "minimum_value_warning": "minimum_support_area", "limit_to_extruder": "support_roof_extruder_nr", - "enabled": "support_roof_enable and (support_enable or support_tree_enable)", + "enabled": "support_roof_enable and support_enable", "settable_per_mesh": true }, "minimum_bottom_area": @@ -4431,7 +4431,7 @@ "minimum_value": "0", "minimum_value_warning": "minimum_support_area", "limit_to_extruder": "support_bottom_extruder_nr", - "enabled": "support_bottom_enable and (support_enable or support_tree_enable)", + "enabled": "support_bottom_enable and support_enable", "settable_per_mesh": true } } From 163f102dda027bf1337134e40d5db13450aae5e3 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 15 Nov 2018 09:38:14 +0100 Subject: [PATCH 18/20] Make extension menu items translatable If these extension plug-ins don't set their menu names, the plug-in name is used as the menu name. The plug-in names are not translated, so this appears as an untranslated string then. --- plugins/ChangeLogPlugin/ChangeLog.py | 3 ++- plugins/PostProcessingPlugin/PostProcessingPlugin.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/ChangeLogPlugin/ChangeLog.py b/plugins/ChangeLogPlugin/ChangeLog.py index 723c83a021..eeec5edf9b 100644 --- a/plugins/ChangeLogPlugin/ChangeLog.py +++ b/plugins/ChangeLogPlugin/ChangeLog.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from UM.i18n import i18nCatalog @@ -29,6 +29,7 @@ class ChangeLog(Extension, QObject,): self._change_logs = None Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated) Application.getInstance().getPreferences().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium + self.setMenuName(catalog.i18nc("@item:inmenu", "Changelog")) self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog) def getChangeLogs(self): diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index 1a1ea92d10..11ee610bec 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -32,7 +32,8 @@ class PostProcessingPlugin(QObject, Extension): def __init__(self, parent = None) -> None: QObject.__init__(self, parent) Extension.__init__(self) - self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup) + self.setMenuName(i18n_catalog.i18nc("@item:inmenu", "Post Processing")) + self.addMenuItem(i18n_catalog.i18nc("@item:inmenu", "Modify G-Code"), self.showPopup) self._view = None # Loaded scripts are all scripts that can be used From 78e64944307168a886a1c0ab01a4b39e4d3144c4 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 15 Nov 2018 15:13:36 +0100 Subject: [PATCH 19/20] Read SDK version from new semver field The sdk_version field should stay the ordinary plain number (the major version number of the semver field) so that older Cura versions don't break. Newly built packages will get built with both sdk_version_semver and the normal sdk_version, so that the packages can be read with any Cura version from 3.6 onwards. Contributes to issue CURA-5940. --- plugins/Toolbox/src/Toolbox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 4f3722d7ba..e491a06770 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -511,7 +511,8 @@ class Toolbox(QObject, Extension): # version, we also need to check if the current one has a lower SDK version. If so, this package should also # be upgradable. elif remote_version == local_version: - can_upgrade = local_package.get("sdk_version", 0) < remote_package.get("sdk_version", 0) + # First read sdk_version_semver. If that doesn't exist, read just sdk_version (old version system). + can_upgrade = local_package.get("sdk_version_semver", local_package.get("sdk_version", 0)) < remote_package.get("sdk_version_semver", remote_package.get("sdk_version", 0)) return can_upgrade From e9216936d741acf435f260b1bd5b10baffddb594 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 15 Nov 2018 15:20:21 +0100 Subject: [PATCH 20/20] Compare SDK version as Version instances This allows it to compare versions that are remote as integer and local as string, or vice-versa. Much more robust. Contributes to issue CURA-5940. --- plugins/Toolbox/src/Toolbox.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index e491a06770..562a964f01 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -512,7 +512,9 @@ class Toolbox(QObject, Extension): # be upgradable. elif remote_version == local_version: # First read sdk_version_semver. If that doesn't exist, read just sdk_version (old version system). - can_upgrade = local_package.get("sdk_version_semver", local_package.get("sdk_version", 0)) < remote_package.get("sdk_version_semver", remote_package.get("sdk_version", 0)) + remote_sdk_version = Version(remote_package.get("sdk_version_semver", remote_package.get("sdk_version", 0))) + local_sdk_version = Version(local_package.get("sdk_version_semver", local_package.get("sdk_version", 0))) + can_upgrade = local_sdk_version < remote_sdk_version return can_upgrade