mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-05-01 08:14:22 +08:00

Previously we would only accept intents that had a translation. If we could not find one, we would use "unknown" as the intent category. However, we didn't really do this consistently. In some places it would show unkown and in others we'd show the intent type. This should make the behavior the same across the board. It will try to get a translation for the intent category and show that. If it's unable to find that it will use the category instead. Note that it will use the python title function to ensure it has nice capitalisation CURA-9297
445 lines
22 KiB
Python
445 lines
22 KiB
Python
# 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)
|
|
def createQualityChanges(self, base_name: str) -> 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)
|
|
|
|
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", "Default"),
|
|
"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
|