123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- # Copyright (c) 2020 Ultimaker B.V.
- # Cura is released under the terms of the LGPLv3 or higher.
- from typing import Any, cast, Dict, Optional, TYPE_CHECKING
- from PyQt6.QtCore import pyqtSlot, QObject, Qt, QTimer
- from UM.Logger import Logger
- from UM.Qt.ListModel import ListModel
- from UM.Settings.InstanceContainer import InstanceContainer # To create new profiles.
- import cura.CuraApplication # Imported this way to prevent circular imports.
- from cura.Settings.ContainerManager import ContainerManager
- from cura.Machines.ContainerTree import ContainerTree
- from cura.Settings.cura_empty_instance_containers import empty_quality_changes_container
- from cura.Settings.IntentManager import IntentManager
- from cura.Machines.Models.MachineModelUtils import fetchLayerHeight
- from cura.Machines.Models.IntentTranslations import intent_translations
- from UM.i18n import i18nCatalog
- catalog = i18nCatalog("cura")
- if TYPE_CHECKING:
- from UM.Settings.Interfaces import ContainerInterface
- from cura.Machines.QualityChangesGroup import QualityChangesGroup
- from cura.Settings.ExtruderStack import ExtruderStack
- from cura.Settings.GlobalStack import GlobalStack
- class QualityManagementModel(ListModel):
- """This the QML model for the quality management page."""
- NameRole = Qt.ItemDataRole.UserRole + 1
- IsReadOnlyRole = Qt.ItemDataRole.UserRole + 2
- QualityGroupRole = Qt.ItemDataRole.UserRole + 3
- QualityTypeRole = Qt.ItemDataRole.UserRole + 4
- QualityChangesGroupRole = Qt.ItemDataRole.UserRole + 5
- IntentCategoryRole = Qt.ItemDataRole.UserRole + 6
- SectionNameRole = Qt.ItemDataRole.UserRole + 7
- def __init__(self, parent: Optional["QObject"] = None) -> None:
- super().__init__(parent)
- self.addRoleName(self.NameRole, "name")
- self.addRoleName(self.IsReadOnlyRole, "is_read_only")
- self.addRoleName(self.QualityGroupRole, "quality_group")
- self.addRoleName(self.QualityTypeRole, "quality_type")
- self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
- self.addRoleName(self.IntentCategoryRole, "intent_category")
- self.addRoleName(self.SectionNameRole, "section_name")
- application = cura.CuraApplication.CuraApplication.getInstance()
- container_registry = application.getContainerRegistry()
- self._machine_manager = application.getMachineManager()
- self._machine_manager.activeQualityGroupChanged.connect(self._onChange)
- self._machine_manager.activeStackChanged.connect(self._onChange)
- self._machine_manager.extruderChanged.connect(self._onChange)
- self._machine_manager.globalContainerChanged.connect(self._onChange)
- self._extruder_manager = application.getExtruderManager()
- self._extruder_manager.extrudersChanged.connect(self._onChange)
- container_registry.containerAdded.connect(self._qualityChangesListChanged)
- container_registry.containerRemoved.connect(self._qualityChangesListChanged)
- container_registry.containerMetaDataChanged.connect(self._qualityChangesListChanged)
- self._update_timer = QTimer()
- self._update_timer.setInterval(100)
- self._update_timer.setSingleShot(True)
- self._update_timer.timeout.connect(self._update)
- self._onChange()
- def _onChange(self) -> None:
- self._update_timer.start()
- @pyqtSlot(QObject)
- def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
- """Deletes a custom profile. It will be gone forever.
- :param quality_changes_group: The quality changes group representing the profile to delete.
- """
- Logger.log("i", "Removing quality changes group {group_name}".format(group_name = quality_changes_group.name))
- removed_quality_changes_ids = set()
- container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
- for metadata in [quality_changes_group.metadata_for_global] + list(quality_changes_group.metadata_per_extruder.values()):
- container_id = metadata["id"]
- container_registry.removeContainer(container_id)
- removed_quality_changes_ids.add(container_id)
- # Reset all machines that have activated this custom profile.
- for global_stack in container_registry.findContainerStacks(type = "machine"):
- if global_stack.qualityChanges.getId() in removed_quality_changes_ids:
- global_stack.qualityChanges = empty_quality_changes_container
- for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"):
- if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids:
- extruder_stack.qualityChanges = empty_quality_changes_container
- @pyqtSlot(QObject, str, result = str)
- def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str:
- """Rename a custom profile.
- Because the names must be unique, the new name may not actually become the name that was given. The actual
- name is returned by this function.
- :param quality_changes_group: The custom profile that must be renamed.
- :param new_name: The desired name for the profile.
- :return: The actual new name of the profile, after making the name unique.
- """
- Logger.log("i", "Renaming QualityChangesGroup {old_name} to {new_name}.".format(old_name = quality_changes_group.name, new_name = new_name))
- if new_name == quality_changes_group.name:
- Logger.log("i", "QualityChangesGroup name {name} unchanged.".format(name = quality_changes_group.name))
- return new_name
- application = cura.CuraApplication.CuraApplication.getInstance()
- container_registry = application.getContainerRegistry()
- new_name = container_registry.uniqueName(new_name)
- # CURA-6842
- # FIXME: setName() will trigger metaDataChanged signal that are connected with type Qt.AutoConnection. In this
- # case, setName() will trigger direct connections which in turn causes the quality changes group and the models
- # to update. Because multiple containers need to be renamed, and every time a container gets renamed, updates
- # gets triggered and this results in partial updates. For example, if we rename the global quality changes
- # container first, the rest of the system still thinks that I have selected "my_profile" instead of
- # "my_new_profile", but an update already gets triggered, and the quality changes group that's selected will
- # have no container for the global stack, because "my_profile" just got renamed to "my_new_profile". This results
- # in crashes because the rest of the system assumes that all data in a QualityChangesGroup will be correct.
- #
- # Renaming the container for the global stack in the end seems to be ok, because the assumption is mostly based
- # on the quality changes container for the global stack.
- for metadata in quality_changes_group.metadata_per_extruder.values():
- extruder_container = cast(InstanceContainer, container_registry.findContainers(id = metadata["id"])[0])
- extruder_container.setName(new_name)
- global_container = cast(InstanceContainer, container_registry.findContainers(id = quality_changes_group.metadata_for_global["id"])[0])
- global_container.setName(new_name)
- quality_changes_group.name = new_name
- application.getMachineManager().activeQualityChanged.emit()
- application.getMachineManager().activeQualityGroupChanged.emit()
- return new_name
- @pyqtSlot(str, "QVariantMap")
- def duplicateQualityChanges(self, new_name: str, quality_model_item: Dict[str, Any]) -> None:
- """Duplicates a given quality profile OR quality changes profile.
- :param new_name: The desired name of the new profile. This will be made unique, so it might end up with a
- different name.
- :param quality_model_item: The item of this model to duplicate, as dictionary. See the descriptions of the
- roles of this list model.
- """
- global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
- if not global_stack:
- Logger.log("i", "No active global stack, cannot duplicate quality (changes) profile.")
- return
- container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
- new_name = container_registry.uniqueName(new_name)
- intent_category = quality_model_item["intent_category"]
- quality_group = quality_model_item["quality_group"]
- quality_changes_group = quality_model_item["quality_changes_group"]
- if quality_changes_group is None:
- new_quality_changes = self._createQualityChanges(quality_group.quality_type, intent_category, new_name,
- global_stack, extruder_stack = None)
- container_registry.addContainer(new_quality_changes)
- for extruder in global_stack.extruderList:
- new_extruder_quality_changes = self._createQualityChanges(quality_group.quality_type, intent_category,
- new_name,
- global_stack, extruder_stack = extruder)
- container_registry.addContainer(new_extruder_quality_changes)
- else:
- for metadata in [quality_changes_group.metadata_for_global] + list(quality_changes_group.metadata_per_extruder.values()):
- containers = container_registry.findContainers(id = metadata["id"])
- if not containers:
- continue
- container = containers[0]
- new_id = container_registry.uniqueName(container.getId())
- container_registry.addContainer(container.duplicate(new_id, new_name))
- @pyqtSlot(str)
- @pyqtSlot(str, bool)
- def createQualityChanges(self, base_name: str, activate_after_success: bool = False) -> None:
- """Create quality changes containers from the user containers in the active stacks.
- This will go through the global and extruder stacks and create quality_changes containers from the user
- containers in each stack. These then replace the quality_changes containers in the stack and clear the user
- settings.
- :param base_name: The new name for the quality changes profile. The final name of the profile might be
- different from this, because it needs to be made unique.
- """
- machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
- global_stack = machine_manager.activeMachine
- if not global_stack:
- return
- active_quality_name = machine_manager.activeQualityOrQualityChangesName
- if active_quality_name == "":
- Logger.log("w", "No quality container found in stack %s, cannot create profile", global_stack.getId())
- return
- machine_manager.blurSettings.emit()
- if base_name is None or base_name == "":
- base_name = active_quality_name
- container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
- unique_name = container_registry.uniqueName(base_name)
- # Go through the active stacks and create quality_changes containers from the user containers.
- container_manager = ContainerManager.getInstance()
- stack_list = [global_stack] + global_stack.extruderList
- for stack in stack_list:
- quality_container = stack.quality
- quality_changes_container = stack.qualityChanges
- if not quality_container or not quality_changes_container:
- Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
- continue
- extruder_stack = None
- intent_category = None
- if stack.getMetaDataEntry("position") is not None:
- extruder_stack = stack
- intent_category = stack.intent.getMetaDataEntry("intent_category")
- new_changes = self._createQualityChanges(quality_container.getMetaDataEntry("quality_type"), intent_category, unique_name, global_stack, extruder_stack)
- container_manager._performMerge(new_changes, quality_changes_container, clear_settings = False)
- container_manager._performMerge(new_changes, stack.userChanges)
- container_registry.addContainer(new_changes)
- if activate_after_success:
- # At this point, the QualityChangesGroup object for the new changes may not exist yet.
- # This can be forced by asking for all of them. At that point it's just as well to loop.
- for quality_changes in ContainerTree.getInstance().getCurrentQualityChangesGroups():
- if quality_changes.name == unique_name:
- machine_manager.setQualityChangesGroup(quality_changes)
- break
- def _createQualityChanges(self, quality_type: str, intent_category: Optional[str], new_name: str, machine: "GlobalStack", extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer":
- """Create a quality changes container with the given set-up.
- :param quality_type: The quality type of the new container.
- :param intent_category: The intent category of the new container.
- :param new_name: The name of the container. This name must be unique.
- :param machine: The global stack to create the profile for.
- :param extruder_stack: The extruder stack to create the profile for. If not provided, only a global container will be created.
- """
- container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
- base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId()
- new_id = base_id + "_" + new_name
- new_id = new_id.lower().replace(" ", "_")
- new_id = container_registry.uniqueName(new_id)
- # Create a new quality_changes container for the quality.
- quality_changes = InstanceContainer(new_id)
- quality_changes.setName(new_name)
- quality_changes.setMetaDataEntry("type", "quality_changes")
- quality_changes.setMetaDataEntry("quality_type", quality_type)
- if intent_category is not None:
- quality_changes.setMetaDataEntry("intent_category", intent_category)
- # If we are creating a container for an extruder, ensure we add that to the container.
- if extruder_stack is not None:
- quality_changes.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
- # If the machine specifies qualities should be filtered, ensure we match the current criteria.
- machine_definition_id = ContainerTree.getInstance().machines[machine.definition.getId()].quality_definition
- quality_changes.setDefinition(machine_definition_id)
- quality_changes.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.getInstance().SettingVersion)
- return quality_changes
- def _qualityChangesListChanged(self, container: "ContainerInterface") -> None:
- """Triggered when any container changed.
- This filters the updates to the container manager: When it applies to the list of quality changes, we need to
- update our list.
- """
- if container.getMetaDataEntry("type") == "quality_changes":
- self._update()
- @pyqtSlot("QVariantMap", result = str)
- def getQualityItemDisplayName(self, quality_model_item: Dict[str, Any]) -> str:
- quality_group = quality_model_item["quality_group"]
- is_read_only = quality_model_item["is_read_only"]
- intent_category = quality_model_item["intent_category"]
- quality_level_name = "Not Supported"
- if quality_group is not None:
- quality_level_name = quality_group.name
- display_name = quality_level_name
- if intent_category != "default":
- intent_display_name = catalog.i18nc("@label", intent_category.capitalize())
- display_name = "{intent_name} - {the_rest}".format(intent_name = intent_display_name,
- the_rest = display_name)
- # A custom quality
- if not is_read_only:
- display_name = "{custom_profile_name} - {the_rest}".format(custom_profile_name = quality_model_item["name"],
- the_rest = display_name)
- return display_name
- def _update(self):
- Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
- global_stack = self._machine_manager.activeMachine
- if not global_stack:
- self.setItems([])
- return
- container_tree = ContainerTree.getInstance()
- quality_group_dict = container_tree.getCurrentQualityGroups()
- quality_changes_group_list = container_tree.getCurrentQualityChangesGroups()
- available_quality_types = set(quality_type for quality_type, quality_group in quality_group_dict.items()
- if quality_group.is_available)
- if not available_quality_types and not quality_changes_group_list:
- # Nothing to show
- self.setItems([])
- return
- item_list = []
- # Create quality group items (intent category = "default")
- for quality_group in quality_group_dict.values():
- if not quality_group.is_available:
- continue
- layer_height = fetchLayerHeight(quality_group)
- item = {"name": quality_group.name,
- "is_read_only": True,
- "quality_group": quality_group,
- "quality_type": quality_group.quality_type,
- "quality_changes_group": None,
- "intent_category": "default",
- "section_name": catalog.i18nc("@label", "Balanced"),
- "layer_height": layer_height, # layer_height is only used for sorting
- }
- item_list.append(item)
- # Sort by layer_height for built-in qualities
- item_list = sorted(item_list, key = lambda x: x["layer_height"])
- # Create intent items (non-default)
- available_intent_list = IntentManager.getInstance().getCurrentAvailableIntents()
- available_intent_list = [i for i in available_intent_list if i[0] != "default"]
- result = []
- for intent_category, quality_type in available_intent_list:
- if not quality_group_dict[quality_type].is_available:
- continue
-
- result.append({
- "name": quality_group_dict[quality_type].name, # Use the quality name as the display name
- "is_read_only": True,
- "quality_group": quality_group_dict[quality_type],
- "quality_type": quality_type,
- "quality_changes_group": None,
- "intent_category": intent_category,
- "section_name": catalog.i18nc("@label", intent_translations.get(intent_category, {}).get("name", catalog.i18nc("@label", intent_category.title()))),
- })
- # Sort by quality_type for each intent category
- intent_translations_list = list(intent_translations)
- def getIntentWeight(intent_category):
- try:
- return intent_translations_list.index(intent_category)
- except ValueError:
- return 99
- result = sorted(result, key = lambda x: (getIntentWeight(x["intent_category"]), x["quality_type"]))
- item_list += result
- # Create quality_changes group items
- quality_changes_item_list = []
- for quality_changes_group in quality_changes_group_list:
- # CURA-6913 Note that custom qualities can be based on "not supported", so the quality group can be None.
- quality_group = quality_group_dict.get(quality_changes_group.quality_type)
- quality_type = quality_changes_group.quality_type
- if not quality_changes_group.is_available:
- continue
- item = {"name": quality_changes_group.name,
- "is_read_only": False,
- "quality_group": quality_group,
- "quality_type": quality_type,
- "quality_changes_group": quality_changes_group,
- "intent_category": quality_changes_group.intent_category,
- "section_name": catalog.i18nc("@label", "Custom profiles"),
- }
- quality_changes_item_list.append(item)
- # Sort quality_changes items by names and append to the item list
- quality_changes_item_list = sorted(quality_changes_item_list, key = lambda x: x["name"].upper())
- item_list += quality_changes_item_list
- self.setItems(item_list)
- @pyqtSlot(str, result = "QVariantList")
- def getFileNameFilters(self, io_type):
- """Gets a list of the possible file filters that the plugins have registered they can read or write.
- The convenience meta-filters "All Supported Types" and "All Files" are added when listing readers,
- but not when listing writers.
- :param io_type: name of the needed IO type
- :return: A list of strings indicating file name filters for a file dialog.
- TODO: Duplicated code here from InstanceContainersModel. Refactor and remove this later.
- """
- from UM.i18n import i18nCatalog
- catalog = i18nCatalog("uranium")
- #TODO: This function should be in UM.Resources!
- filters = []
- all_types = []
- for plugin_id, meta_data in self._getIOPlugins(io_type):
- for io_plugin in meta_data[io_type]:
- filters.append(io_plugin["description"] + " (*." + io_plugin["extension"] + ")")
- all_types.append("*.{0}".format(io_plugin["extension"]))
- if "_reader" in io_type:
- # if we're listing readers, add the option to show all supported files as the default option
- filters.insert(0, catalog.i18nc("@item:inlistbox", "All Supported Types ({0})", " ".join(all_types)))
- filters.append(catalog.i18nc("@item:inlistbox", "All Files (*)")) # Also allow arbitrary files, if the user so prefers.
- return filters
- def _getIOPlugins(self, io_type):
- """Gets a list of profile reader or writer plugins
- :return: List of tuples of (plugin_id, meta_data).
- """
- from UM.PluginRegistry import PluginRegistry
- pr = PluginRegistry.getInstance()
- active_plugin_ids = pr.getActivePlugins()
- result = []
- for plugin_id in active_plugin_ids:
- meta_data = pr.getMetaData(plugin_id)
- if io_type in meta_data:
- result.append( (plugin_id, meta_data) )
- return result
|