diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 7e5e98adef..8cab6b9d56 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -114,6 +114,8 @@ from UM.FlameProfiler import pyqtSlot if TYPE_CHECKING: from plugins.SliceInfoPlugin.SliceInfo import SliceInfo + from cura.Machines.MaterialManager import MaterialManager + from cura.Machines.QualityManager import QualityManager numpy.seterr(all = "ignore") @@ -807,20 +809,20 @@ class CuraApplication(QtApplication): self._machine_manager = MachineManager(self) return self._machine_manager - def getExtruderManager(self, *args): + def getExtruderManager(self, *args) -> ExtruderManager: if self._extruder_manager is None: self._extruder_manager = ExtruderManager() return self._extruder_manager - def getVariantManager(self, *args): + def getVariantManager(self, *args) -> VariantManager: return self._variant_manager @pyqtSlot(result = QObject) - def getMaterialManager(self, *args): + def getMaterialManager(self, *args) -> "MaterialManager": return self._material_manager @pyqtSlot(result = QObject) - def getQualityManager(self, *args): + def getQualityManager(self, *args) -> "QualityManager": return self._quality_manager def getObjectsModel(self, *args): @@ -829,23 +831,23 @@ class CuraApplication(QtApplication): return self._object_manager @pyqtSlot(result = QObject) - def getMultiBuildPlateModel(self, *args): + def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel: if self._multi_build_plate_model is None: self._multi_build_plate_model = MultiBuildPlateModel(self) return self._multi_build_plate_model @pyqtSlot(result = QObject) - def getBuildPlateModel(self, *args): + def getBuildPlateModel(self, *args) -> BuildPlateModel: if self._build_plate_model is None: self._build_plate_model = BuildPlateModel(self) return self._build_plate_model - def getCuraSceneController(self, *args): + def getCuraSceneController(self, *args) -> CuraSceneController: if self._cura_scene_controller is None: self._cura_scene_controller = CuraSceneController.createCuraSceneController() return self._cura_scene_controller - def getSettingInheritanceManager(self, *args): + def getSettingInheritanceManager(self, *args) -> SettingInheritanceManager: if self._setting_inheritance_manager is None: self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager() return self._setting_inheritance_manager diff --git a/cura/Machines/ContainerNode.py b/cura/Machines/ContainerNode.py index 944579e354..0d44c7c4a3 100644 --- a/cura/Machines/ContainerNode.py +++ b/cura/Machines/ContainerNode.py @@ -24,29 +24,34 @@ if TYPE_CHECKING: # This is used in Variant, Material, and Quality Managers. # class ContainerNode: - __slots__ = ("metadata", "container", "children_map") + __slots__ = ("_metadata", "container", "children_map") def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None: - self.metadata = metadata + self._metadata = metadata self.container = None - self.children_map = OrderedDict() #type: OrderedDict[str, Union[QualityGroup, ContainerNode]] + self.children_map = OrderedDict() # type: ignore # This is because it's children are supposed to override it. ## Get an entry value from the metadata def getMetaDataEntry(self, entry: str, default: Any = None) -> Any: - if self.metadata is None: + if self._metadata is None: return default - return self.metadata.get(entry, default) + return self._metadata.get(entry, default) + + def getMetadata(self) -> Dict[str, Any]: + if self._metadata is None: + return {} + return self._metadata def getChildNode(self, child_key: str) -> Optional["ContainerNode"]: return self.children_map.get(child_key) def getContainer(self) -> Optional["InstanceContainer"]: - if self.metadata is None: + if self._metadata is None: Logger.log("e", "Cannot get container for a ContainerNode without metadata.") return None if self.container is None: - container_id = self.metadata["id"] + container_id = self._metadata["id"] from UM.Settings.ContainerRegistry import ContainerRegistry container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id) if not container_list: diff --git a/cura/Machines/MaterialGroup.py b/cura/Machines/MaterialGroup.py index 8a73796a7a..e05647e674 100644 --- a/cura/Machines/MaterialGroup.py +++ b/cura/Machines/MaterialGroup.py @@ -24,8 +24,8 @@ class MaterialGroup: def __init__(self, name: str, root_material_node: "MaterialNode") -> None: self.name = name self.is_read_only = False - self.root_material_node = root_material_node # type: MaterialNode - self.derived_material_node_list = [] # type: List[MaterialNode] + self.root_material_node = root_material_node # type: MaterialNode + self.derived_material_node_list = [] # type: List[MaterialNode] def __str__(self) -> str: return "%s[%s]" % (self.__class__.__name__, self.name) diff --git a/cura/Machines/MaterialManager.py b/cura/Machines/MaterialManager.py index 1463f2e40e..98a4eeb330 100644 --- a/cura/Machines/MaterialManager.py +++ b/cura/Machines/MaterialManager.py @@ -4,8 +4,7 @@ from collections import defaultdict, OrderedDict import copy import uuid -import json -from typing import Dict, Optional, TYPE_CHECKING +from typing import Dict, Optional, TYPE_CHECKING, Any, Set, List, cast, Tuple from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot @@ -39,26 +38,35 @@ if TYPE_CHECKING: # class MaterialManager(QObject): - materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated. - favoritesUpdated = pyqtSignal() # Emitted whenever the favorites are changed + materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated. + favoritesUpdated = pyqtSignal() # Emitted whenever the favorites are changed def __init__(self, container_registry, parent = None): super().__init__(parent) self._application = Application.getInstance() self._container_registry = container_registry # type: ContainerRegistry - self._fallback_materials_map = dict() # material_type -> generic material metadata - self._material_group_map = dict() # root_material_id -> MaterialGroup - self._diameter_machine_nozzle_buildplate_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> MaterialNode) + # Material_type -> generic material metadata + self._fallback_materials_map = dict() # type: Dict[str, Dict[str, Any]] + + # Root_material_id -> MaterialGroup + self._material_group_map = dict() # type: Dict[str, MaterialGroup] + + # Approximate diameter str + self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]] # We're using these two maps to convert between the specific diameter material id and the generic material id # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant # i.e. generic_pla -> generic_pla_175 - self._material_diameter_map = defaultdict(dict) # root_material_id -> approximate diameter str -> root_material_id for that diameter - self._diameter_material_map = dict() # material id including diameter (generic_pla_175) -> material root id (generic_pla) + # root_material_id -> approximate diameter str -> root_material_id for that diameter + self._material_diameter_map = defaultdict(dict) # type: Dict[str, Dict[str, str]] + + # Material id including diameter (generic_pla_175) -> material root id (generic_pla) + self._diameter_material_map = dict() # type: Dict[str, str] # This is used in Legacy UM3 send material function and the material management page. - self._guid_material_groups_map = defaultdict(list) # GUID -> a list of material_groups + # GUID -> a list of material_groups + self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]] # The machine definition ID for the non-machine-specific materials. # This is used as the last fallback option if the given machine-specific material(s) cannot be found. @@ -77,15 +85,15 @@ class MaterialManager(QObject): self._container_registry.containerAdded.connect(self._onContainerMetadataChanged) self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged) - self._favorites = set() + self._favorites = set() # type: Set[str] - def initialize(self): + def initialize(self) -> None: # Find all materials and put them in a matrix for quick search. material_metadatas = {metadata["id"]: metadata for metadata in self._container_registry.findContainersMetadata(type = "material") if - metadata.get("GUID")} + metadata.get("GUID")} # type: Dict[str, Dict[str, Any]] - self._material_group_map = dict() + self._material_group_map = dict() # type: Dict[str, MaterialGroup] # Map #1 # root_material_id -> MaterialGroup @@ -94,7 +102,7 @@ class MaterialManager(QObject): if material_id == "empty_material": continue - root_material_id = material_metadata.get("base_file") + root_material_id = material_metadata.get("base_file", "") if root_material_id not in self._material_group_map: self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id])) self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id) @@ -110,26 +118,26 @@ class MaterialManager(QObject): # Map #1.5 # GUID -> material group list - self._guid_material_groups_map = defaultdict(list) + self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]] for root_material_id, material_group in self._material_group_map.items(): - guid = material_group.root_material_node.metadata["GUID"] + guid = material_group.root_material_node.getMetaDataEntry("GUID", "") self._guid_material_groups_map[guid].append(material_group) # Map #2 # Lookup table for material type -> fallback material metadata, only for read-only materials - grouped_by_type_dict = dict() + grouped_by_type_dict = dict() # type: Dict[str, Any] material_types_without_fallback = set() for root_material_id, material_node in self._material_group_map.items(): - material_type = material_node.root_material_node.metadata["material"] + material_type = material_node.root_material_node.getMetaDataEntry("material", "") if material_type not in grouped_by_type_dict: grouped_by_type_dict[material_type] = {"generic": None, "others": []} material_types_without_fallback.add(material_type) - brand = material_node.root_material_node.metadata["brand"] + brand = material_node.root_material_node.getMetaDataEntry("brand", "") if brand.lower() == "generic": to_add = True if material_type in grouped_by_type_dict: - diameter = material_node.root_material_node.metadata.get("approximate_diameter") + diameter = material_node.root_material_node.getMetaDataEntry("approximate_diameter", "") if diameter != self._default_approximate_diameter_for_quality_search: to_add = False # don't add if it's not the default diameter @@ -138,7 +146,7 @@ class MaterialManager(QObject): # - if it's in the list, it means that is a new material without fallback # - if it is not, then it is a custom material with a fallback material (parent) if material_type in material_types_without_fallback: - grouped_by_type_dict[material_type] = material_node.root_material_node.metadata + grouped_by_type_dict[material_type] = material_node.root_material_node._metadata material_types_without_fallback.remove(material_type) # Remove the materials that have no fallback materials @@ -155,15 +163,15 @@ class MaterialManager(QObject): self._diameter_material_map = dict() # Group the material IDs by the same name, material, brand, and color but with different diameters. - material_group_dict = dict() + material_group_dict = dict() # type: Dict[Tuple[Any], Dict[str, str]] keys_to_fetch = ("name", "material", "brand", "color") for root_material_id, machine_node in self._material_group_map.items(): - root_material_metadata = machine_node.root_material_node.metadata + root_material_metadata = machine_node.root_material_node._metadata - key_data = [] + key_data_list = [] # type: List[Any] for key in keys_to_fetch: - key_data.append(root_material_metadata.get(key)) - key_data = tuple(key_data) + key_data_list.append(machine_node.root_material_node.getMetaDataEntry(key)) + key_data = cast(Tuple[Any], tuple(key_data_list)) # type: Tuple[Any] # If the key_data doesn't exist, it doesn't matter if the material is read only... if key_data not in material_group_dict: @@ -172,8 +180,8 @@ class MaterialManager(QObject): # ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it if not machine_node.is_read_only: continue - approximate_diameter = root_material_metadata.get("approximate_diameter") - material_group_dict[key_data][approximate_diameter] = root_material_metadata["id"] + approximate_diameter = machine_node.root_material_node.getMetaDataEntry("approximate_diameter", "") + material_group_dict[key_data][approximate_diameter] = machine_node.root_material_node.getMetaDataEntry("id", "") # Map [root_material_id][diameter] -> root_material_id for this diameter for data_dict in material_group_dict.values(): @@ -192,7 +200,7 @@ class MaterialManager(QObject): # Map #4 # "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer - self._diameter_machine_nozzle_buildplate_material_map = dict() + self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]] for material_metadata in material_metadatas.values(): self.__addMaterialMetadataIntoLookupTree(material_metadata) @@ -203,7 +211,7 @@ class MaterialManager(QObject): self._favorites.add(item) self.favoritesUpdated.emit() - def __addMaterialMetadataIntoLookupTree(self, material_metadata: dict) -> None: + def __addMaterialMetadataIntoLookupTree(self, material_metadata: Dict[str, Any]) -> None: material_id = material_metadata["id"] # We don't store empty material in the lookup tables @@ -290,7 +298,7 @@ class MaterialManager(QObject): return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id) def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str: - return self._diameter_material_map.get(root_material_id) + return self._diameter_material_map.get(root_material_id, "") def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]: return self._guid_material_groups_map.get(guid) @@ -351,7 +359,7 @@ class MaterialManager(QObject): # A convenience function to get available materials for the given machine with the extruder position. # def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack", - extruder_stack: "ExtruderStack") -> Optional[dict]: + extruder_stack: "ExtruderStack") -> Optional[Dict[str, MaterialNode]]: buildplate_name = machine.getBuildplateName() nozzle_name = None if extruder_stack.variant.getId() != "empty_variant": @@ -368,7 +376,7 @@ class MaterialManager(QObject): # 2. cannot find any material InstanceContainers with the given settings. # def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str], - buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["InstanceContainer"]: + buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["MaterialNode"]: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map: @@ -377,7 +385,7 @@ class MaterialManager(QObject): return None # If there are nozzle materials, get the nozzle-specific material - machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] + machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] # type: Dict[str, MaterialNode] machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id) nozzle_node = None buildplate_node = None @@ -426,7 +434,7 @@ class MaterialManager(QObject): # Look at the guid to material dictionary root_material_id = None for material_group in self._guid_material_groups_map[material_guid]: - root_material_id = material_group.root_material_node.metadata["id"] + root_material_id = cast(str, material_group.root_material_node.getMetaDataEntry("id", "")) break if not root_material_id: @@ -502,7 +510,7 @@ class MaterialManager(QObject): # Sets the new name for the given material. # @pyqtSlot("QVariant", str) - def setMaterialName(self, material_node: "MaterialNode", name: str): + def setMaterialName(self, material_node: "MaterialNode", name: str) -> None: root_material_id = material_node.getMetaDataEntry("base_file") if root_material_id is None: return @@ -520,7 +528,7 @@ class MaterialManager(QObject): # Removes the given material. # @pyqtSlot("QVariant") - def removeMaterial(self, material_node: "MaterialNode"): + def removeMaterial(self, material_node: "MaterialNode") -> None: root_material_id = material_node.getMetaDataEntry("base_file") if root_material_id is not None: self.removeMaterialByRootId(root_material_id) @@ -530,8 +538,8 @@ class MaterialManager(QObject): # Returns the root material ID of the duplicated material if successful. # @pyqtSlot("QVariant", result = str) - def duplicateMaterial(self, material_node, new_base_id = None, new_metadata = None) -> Optional[str]: - root_material_id = material_node.metadata["base_file"] + def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]: + root_material_id = cast(str, material_node.getMetaDataEntry("base_file", "")) material_group = self.getMaterialGroup(root_material_id) if not material_group: @@ -586,7 +594,7 @@ class MaterialManager(QObject): # # Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID. - # + # Returns the ID of the newly created material. @pyqtSlot(result = str) def createMaterial(self) -> str: from UM.i18n import i18nCatalog @@ -619,7 +627,7 @@ class MaterialManager(QObject): return new_id @pyqtSlot(str) - def addFavorite(self, root_material_id: str): + def addFavorite(self, root_material_id: str) -> None: self._favorites.add(root_material_id) self.favoritesUpdated.emit() @@ -628,7 +636,7 @@ class MaterialManager(QObject): self._application.saveSettings() @pyqtSlot(str) - def removeFavorite(self, root_material_id: str): + def removeFavorite(self, root_material_id: str) -> None: self._favorites.remove(root_material_id) self.favoritesUpdated.emit() diff --git a/cura/Machines/MaterialNode.py b/cura/Machines/MaterialNode.py index 04423d7b2c..a4dcb0564f 100644 --- a/cura/Machines/MaterialNode.py +++ b/cura/Machines/MaterialNode.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional, Dict - +from typing import Optional, Dict, Any +from collections import OrderedDict from .ContainerNode import ContainerNode @@ -14,6 +14,12 @@ from .ContainerNode import ContainerNode class MaterialNode(ContainerNode): __slots__ = ("material_map", "children_map") - def __init__(self, metadata: Optional[dict] = None) -> None: + def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None: super().__init__(metadata = metadata) self.material_map = {} # type: Dict[str, MaterialNode] # material_root_id -> material_node + + # We overide this as we want to indicate that MaterialNodes can only contain other material nodes. + self.children_map = OrderedDict() # type: OrderedDict[str, "MaterialNode"] + + def getChildNode(self, child_key: str) -> Optional["MaterialNode"]: + return self.children_map.get(child_key) \ No newline at end of file diff --git a/cura/Machines/Models/BaseMaterialsModel.py b/cura/Machines/Models/BaseMaterialsModel.py index 1b20e1188c..9799a35ed1 100644 --- a/cura/Machines/Models/BaseMaterialsModel.py +++ b/cura/Machines/Models/BaseMaterialsModel.py @@ -113,7 +113,7 @@ class BaseMaterialsModel(ListModel): ## This is another convenience function which is shared by all material # models so it's put here to avoid having so much duplicated code. def _createMaterialItem(self, root_material_id, container_node): - metadata = container_node.metadata + metadata = container_node.getMetadata() item = { "root_material_id": root_material_id, "id": metadata["id"], diff --git a/cura/Machines/Models/FavoriteMaterialsModel.py b/cura/Machines/Models/FavoriteMaterialsModel.py index be3f0f605f..18fe310c44 100644 --- a/cura/Machines/Models/FavoriteMaterialsModel.py +++ b/cura/Machines/Models/FavoriteMaterialsModel.py @@ -23,7 +23,7 @@ class FavoriteMaterialsModel(BaseMaterialsModel): item_list = [] for root_material_id, container_node in self._available_materials.items(): - metadata = container_node.metadata + metadata = container_node.getMetadata() # Do not include the materials from a to-be-removed package if bool(metadata.get("removed", False)): diff --git a/cura/Machines/Models/GenericMaterialsModel.py b/cura/Machines/Models/GenericMaterialsModel.py index 27e6fdfd7c..c276b865bf 100644 --- a/cura/Machines/Models/GenericMaterialsModel.py +++ b/cura/Machines/Models/GenericMaterialsModel.py @@ -23,7 +23,7 @@ class GenericMaterialsModel(BaseMaterialsModel): item_list = [] for root_material_id, container_node in self._available_materials.items(): - metadata = container_node.metadata + metadata = container_node.getMetadata() # Do not include the materials from a to-be-removed package if bool(metadata.get("removed", False)): diff --git a/cura/Machines/Models/MaterialBrandsModel.py b/cura/Machines/Models/MaterialBrandsModel.py index 3f917abb16..f8d92dc65f 100644 --- a/cura/Machines/Models/MaterialBrandsModel.py +++ b/cura/Machines/Models/MaterialBrandsModel.py @@ -41,21 +41,19 @@ class MaterialBrandsModel(BaseMaterialsModel): # Part 1: Generate the entire tree of brands -> material types -> spcific materials for root_material_id, container_node in self._available_materials.items(): - metadata = container_node.metadata - # Do not include the materials from a to-be-removed package - if bool(metadata.get("removed", False)): + if bool(container_node.getMetaDataEntry("removed", False)): continue # Add brands we haven't seen yet to the dict, skipping generics - brand = metadata["brand"] + brand = container_node.getMetaDataEntry("brand", "") if brand.lower() == "generic": continue if brand not in brand_group_dict: brand_group_dict[brand] = {} # Add material types we haven't seen yet to the dict - material_type = metadata["material"] + material_type = container_node.getMetaDataEntry("material", "") if material_type not in brand_group_dict[brand]: brand_group_dict[brand][material_type] = [] diff --git a/cura/Machines/QualityChangesGroup.py b/cura/Machines/QualityChangesGroup.py index 2d0e655ed8..3dcf2ab1c8 100644 --- a/cura/Machines/QualityChangesGroup.py +++ b/cura/Machines/QualityChangesGroup.py @@ -17,7 +17,7 @@ class QualityChangesGroup(QualityGroup): super().__init__(name, quality_type, parent) self._container_registry = Application.getInstance().getContainerRegistry() - def addNode(self, node: "QualityNode"): + def addNode(self, node: "QualityNode") -> None: extruder_position = node.getMetaDataEntry("position") if extruder_position is None and self.node_for_global is not None or extruder_position in self.nodes_for_extruders: #We would be overwriting another node. diff --git a/cura/Machines/QualityGroup.py b/cura/Machines/QualityGroup.py index 90ef63af51..535ba453f8 100644 --- a/cura/Machines/QualityGroup.py +++ b/cura/Machines/QualityGroup.py @@ -6,6 +6,7 @@ from typing import Dict, Optional, List, Set from PyQt5.QtCore import QObject, pyqtSlot from cura.Machines.ContainerNode import ContainerNode + # # A QualityGroup represents a group of containers that must be applied to each ContainerStack when it's used. # Some concrete examples are Quality and QualityChanges: when we select quality type "normal", this quality type @@ -34,7 +35,7 @@ class QualityGroup(QObject): return self.name def getAllKeys(self) -> Set[str]: - result = set() #type: Set[str] + result = set() # type: Set[str] for node in [self.node_for_global] + list(self.nodes_for_extruders.values()): if node is None: continue diff --git a/cura/Machines/QualityManager.py b/cura/Machines/QualityManager.py index 4ae58a71b2..e081d4db3e 100644 --- a/cura/Machines/QualityManager.py +++ b/cura/Machines/QualityManager.py @@ -221,12 +221,12 @@ class QualityManager(QObject): for node in nodes_to_check: if node and node.quality_type_map: quality_node = list(node.quality_type_map.values())[0] - is_global_quality = parseBool(quality_node.metadata.get("global_quality", False)) + is_global_quality = parseBool(quality_node.getMetaDataEntry("global_quality", False)) if not is_global_quality: continue for quality_type, quality_node in node.quality_type_map.items(): - quality_group = QualityGroup(quality_node.metadata["name"], quality_type) + quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type) quality_group.node_for_global = quality_node quality_group_dict[quality_type] = quality_group break @@ -310,13 +310,13 @@ class QualityManager(QObject): if has_extruder_specific_qualities: # Only include variant qualities; skip non global qualities quality_node = list(node.quality_type_map.values())[0] - is_global_quality = parseBool(quality_node.metadata.get("global_quality", False)) + is_global_quality = parseBool(quality_node.getMetaDataEntry("global_quality", False)) if is_global_quality: continue for quality_type, quality_node in node.quality_type_map.items(): if quality_type not in quality_group_dict: - quality_group = QualityGroup(quality_node.metadata["name"], quality_type) + quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type) quality_group_dict[quality_type] = quality_group quality_group = quality_group_dict[quality_type] @@ -350,7 +350,7 @@ class QualityManager(QObject): for node in nodes_to_check: if node and node.quality_type_map: for quality_type, quality_node in node.quality_type_map.items(): - quality_group = QualityGroup(quality_node.metadata["name"], quality_type) + quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type) quality_group.node_for_global = quality_node quality_group_dict[quality_type] = quality_group break diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 2b8ff4a234..e1a1495dac 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -4,12 +4,12 @@ import os import urllib.parse import uuid -from typing import Any -from typing import Dict, Union, Optional +from typing import Dict, Union, Any, TYPE_CHECKING, List -from PyQt5.QtCore import QObject, QUrl, QVariant +from PyQt5.QtCore import QObject, QUrl from PyQt5.QtWidgets import QMessageBox + from UM.i18n import i18nCatalog from UM.FlameProfiler import pyqtSlot from UM.Logger import Logger @@ -21,6 +21,18 @@ from UM.Settings.ContainerStack import ContainerStack from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.InstanceContainer import InstanceContainer + +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication + from cura.Machines.ContainerNode import ContainerNode + from cura.Machines.MaterialNode import MaterialNode + from cura.Machines.QualityChangesGroup import QualityChangesGroup + from UM.PluginRegistry import PluginRegistry + from UM.Settings.ContainerRegistry import ContainerRegistry + from cura.Settings.MachineManager import MachineManager + from cura.Machines.MaterialManager import MaterialManager + from cura.Machines.QualityManager import QualityManager + catalog = i18nCatalog("cura") @@ -31,20 +43,20 @@ catalog = i18nCatalog("cura") # when a certain action happens. This can be done through this class. class ContainerManager(QObject): - def __init__(self, application): + def __init__(self, application: "CuraApplication") -> None: if ContainerManager.__instance is not None: raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) ContainerManager.__instance = self super().__init__(parent = application) - self._application = application - self._plugin_registry = self._application.getPluginRegistry() - 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 = {} # type: Dict[str, Dict[str, Any]] + self._application = application # type: CuraApplication + self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry + self._container_registry = self._application.getContainerRegistry() # type: ContainerRegistry + self._machine_manager = self._application.getMachineManager() # type: MachineManager + self._material_manager = self._application.getMaterialManager() # type: MaterialManager + self._quality_manager = self._application.getQualityManager() # type: QualityManager + self._container_name_filters = {} # type: Dict[str, Dict[str, Any]] @pyqtSlot(str, str, result=str) def getContainerMetaDataEntry(self, container_id: str, entry_names: str) -> str: @@ -69,21 +81,23 @@ class ContainerManager(QObject): # by using "/" as a separator. For example, to change an entry "foo" in a # dictionary entry "bar", you can specify "bar/foo" as entry name. # - # \param container_id \type{str} The ID of the container to change. + # \param container_node \type{ContainerNode} # \param entry_name \type{str} The name of the metadata entry to change. # \param entry_value The new value of the entry. # - # \return True if successful, False if not. # TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this. # Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want? @pyqtSlot("QVariant", str, str) - def setContainerMetaDataEntry(self, container_node, entry_name, entry_value): - root_material_id = container_node.metadata["base_file"] + def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool: + root_material_id = container_node.getMetaDataEntry("base_file", "") if self._container_registry.isReadOnly(root_material_id): Logger.log("w", "Cannot set metadata of read-only container %s.", root_material_id) return False material_group = self._material_manager.getMaterialGroup(root_material_id) + if material_group is None: + Logger.log("w", "Unable to find material group for: %s.", root_material_id) + return False entries = entry_name.split("/") entry_name = entries.pop() @@ -91,11 +105,11 @@ class ContainerManager(QObject): sub_item_changed = False if entries: root_name = entries.pop(0) - root = material_group.root_material_node.metadata.get(root_name) + root = material_group.root_material_node.getMetaDataEntry(root_name) item = root for _ in range(len(entries)): - item = item.get(entries.pop(0), { }) + item = item.get(entries.pop(0), {}) if item[entry_name] != entry_value: sub_item_changed = True @@ -109,9 +123,10 @@ class ContainerManager(QObject): container.setMetaDataEntry(entry_name, entry_value) if sub_item_changed: #If it was only a sub-item that has changed then the setMetaDataEntry won't correctly notice that something changed, and we must manually signal that the metadata changed. container.metaDataChanged.emit(container) + return True @pyqtSlot(str, result = str) - def makeUniqueName(self, original_name): + def makeUniqueName(self, original_name: str) -> str: return self._container_registry.uniqueName(original_name) ## Get a list of string that can be used as name filters for a Qt File Dialog @@ -125,7 +140,7 @@ class ContainerManager(QObject): # # \return A string list with name filters. @pyqtSlot(str, result = "QStringList") - def getContainerNameFilters(self, type_name): + def getContainerNameFilters(self, type_name: str) -> List[str]: if not self._container_name_filters: self._updateContainerNameFilters() @@ -257,7 +272,7 @@ class ContainerManager(QObject): # # \return \type{bool} True if successful, False if not. @pyqtSlot(result = bool) - def updateQualityChanges(self): + def updateQualityChanges(self) -> bool: global_stack = self._machine_manager.activeMachine if not global_stack: return False @@ -313,10 +328,10 @@ class ContainerManager(QObject): # \param material_id \type{str} the id of the material for which to get the linked materials. # \return \type{list} a list of names of materials with the same GUID @pyqtSlot("QVariant", bool, result = "QStringList") - def getLinkedMaterials(self, material_node, exclude_self = False): - guid = material_node.metadata["GUID"] + def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False): + guid = material_node.getMetaDataEntry("GUID", "") - self_root_material_id = material_node.metadata["base_file"] + self_root_material_id = material_node.getMetaDataEntry("base_file") material_group_list = self._material_manager.getMaterialGroupListByGUID(guid) linked_material_names = [] @@ -324,15 +339,19 @@ class ContainerManager(QObject): for material_group in material_group_list: if exclude_self and material_group.name == self_root_material_id: continue - linked_material_names.append(material_group.root_material_node.metadata["name"]) + linked_material_names.append(material_group.root_material_node.getMetaDataEntry("name", "")) return linked_material_names ## Unlink a material from all other materials by creating a new GUID # \param material_id \type{str} the id of the material to create a new GUID for. @pyqtSlot("QVariant") - def unlinkMaterial(self, material_node): + def unlinkMaterial(self, material_node: "MaterialNode") -> None: # Get the material group - material_group = self._material_manager.getMaterialGroup(material_node.metadata["base_file"]) + material_group = self._material_manager.getMaterialGroup(material_node.getMetaDataEntry("base_file", "")) + + if material_group is None: + Logger.log("w", "Unable to find material group for %s", material_node) + return # Generate a new GUID new_guid = str(uuid.uuid4()) @@ -344,7 +363,7 @@ class ContainerManager(QObject): if container is not None: container.setMetaDataEntry("GUID", new_guid) - def _performMerge(self, merge_into, merge, clear_settings = True): + def _performMerge(self, merge_into: InstanceContainer, merge: InstanceContainer, clear_settings: bool = True) -> None: if merge == merge_into: return @@ -400,7 +419,7 @@ class ContainerManager(QObject): ## Import single profile, file_url does not have to end with curaprofile @pyqtSlot(QUrl, result="QVariantMap") - def importProfile(self, file_url): + def importProfile(self, file_url: QUrl): if not file_url.isValid(): return path = file_url.toLocalFile() @@ -409,7 +428,7 @@ class ContainerManager(QObject): return self._container_registry.importProfile(path) @pyqtSlot(QObject, QUrl, str) - def exportQualityChangesGroup(self, quality_changes_group, file_url: QUrl, file_type: str): + def exportQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", file_url: QUrl, file_type: str) -> None: if not file_url.isValid(): return path = file_url.toLocalFile() diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 1003ab5c86..5af22583ed 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -15,7 +15,7 @@ from UM.Settings.SettingFunction import SettingFunction from UM.Settings.ContainerStack import ContainerStack from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext -from typing import Optional, TYPE_CHECKING, Dict, List, Any +from typing import Optional, TYPE_CHECKING, Dict, List, Any, Union if TYPE_CHECKING: from cura.Settings.ExtruderStack import ExtruderStack @@ -39,9 +39,12 @@ class ExtruderManager(QObject): self._application = cura.CuraApplication.CuraApplication.getInstance() # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders. - self._extruder_trains = {} # type: Dict[str, Dict[str, ExtruderStack]] + self._extruder_trains = {} # type: Dict[str, Dict[str, "ExtruderStack"]] self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack - self._selected_object_extruders = [] # type: List[ExtruderStack] + + # TODO; I have no idea why this is a union of ID's and extruder stacks. This needs to be fixed at some point. + self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]] + self._addCurrentMachineExtruders() Selection.selectionChanged.connect(self.resetSelectedObjectExtruders) @@ -80,7 +83,7 @@ class ExtruderManager(QObject): ## Gets a dict with the extruder stack ids with the extruder number as the key. @pyqtProperty("QVariantMap", notify = extrudersChanged) def extruderIds(self) -> Dict[str, str]: - extruder_stack_ids = {} + extruder_stack_ids = {} # type: Dict[str, str] global_container_stack = self._application.getGlobalContainerStack() if global_container_stack: @@ -115,7 +118,7 @@ class ExtruderManager(QObject): ## Provides a list of extruder IDs used by the current selected objects. @pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged) - def selectedObjectExtruders(self) -> List[str]: + def selectedObjectExtruders(self) -> List[Union[str, "ExtruderStack"]]: if not self._selected_object_extruders: object_extruders = set() @@ -140,7 +143,7 @@ class ExtruderManager(QObject): elif current_extruder_trains: object_extruders.add(current_extruder_trains[0].getId()) - self._selected_object_extruders = list(object_extruders) + self._selected_object_extruders = list(object_extruders) # type: List[Union[str, "ExtruderStack"]] return self._selected_object_extruders @@ -149,7 +152,7 @@ class ExtruderManager(QObject): # This will trigger a recalculation of the extruders used for the # selection. def resetSelectedObjectExtruders(self) -> None: - self._selected_object_extruders = [] + self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]] self.selectedObjectExtrudersChanged.emit() @pyqtSlot(result = QObject) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index ed543fcee1..28a3e5cd62 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -892,7 +892,11 @@ class MachineManager(QObject): extruder_nr = node.callDecoration("getActiveExtruderPosition") if extruder_nr is not None and int(extruder_nr) > extruder_count - 1: - node.callDecoration("setActiveExtruder", extruder_manager.getExtruderStack(extruder_count - 1).getId()) + extruder = extruder_manager.getExtruderStack(extruder_count - 1) + if extruder is not None: + node.callDecoration("setActiveExtruder", extruder.getId()) + else: + Logger.log("w", "Could not find extruder to set active.") # Make sure one of the extruder stacks is active extruder_manager.setActiveExtruderIndex(0) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 6d55e0643d..36a725d148 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -934,7 +934,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): root_material_id) if material_node is not None and material_node.getContainer() is not None: - extruder_stack.material = material_node.getContainer() + extruder_stack.material = material_node.getContainer() # type: InstanceContainer def _applyChangesToMachine(self, global_stack, extruder_stack_dict): # Clear all first