Merge branch 'feature_intent_container_tree' of github.com:Ultimaker/Cura into feature_intent_interface

This commit is contained in:
Jaime van Kessel 2019-08-29 16:28:26 +02:00
commit b00b8c8c58
27 changed files with 603 additions and 239 deletions

View file

@ -31,8 +31,13 @@ class BaseMaterialsModel(ListModel):
self._container_registry = self._application.getInstance().getContainerRegistry()
self._machine_manager = self._application.getMachineManager()
self._extruder_position = 0
self._extruder_stack = None
self._enabled = True
# Update the stack and the model data when the machine changes
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
self._updateExtruderStack()
# Update this model when switching machines, when adding materials or changing their metadata.
self._machine_manager.activeStackChanged.connect(self._update)
@ -55,12 +60,8 @@ class BaseMaterialsModel(ListModel):
self.addRoleName(Qt.UserRole + 15, "container_node")
self.addRoleName(Qt.UserRole + 16, "is_favorite")
self._extruder_position = 0
self._extruder_stack = None
self._available_materials = None # type: Optional[Dict[str, MaterialNode]]
self._favorite_ids = set() # type: Set[str]
self._enabled = True
def _updateExtruderStack(self):
global_stack = self._machine_manager.activeMachine
@ -95,7 +96,7 @@ class BaseMaterialsModel(ListModel):
self._update()
self.enabledChanged.emit()
@pyqtProperty(bool, fset=setEnabled, notify=enabledChanged)
@pyqtProperty(bool, fset = setEnabled, notify = enabledChanged)
def enabled(self):
return self._enabled
@ -107,7 +108,10 @@ class BaseMaterialsModel(ListModel):
return
if material.variant.container_id != self._extruder_stack.variant.getId():
return
if material.variant.machine.container_id != cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack().definition.getId():
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack:
return
if material.variant.machine.container_id != global_stack.definition.getId():
return
self._update()
@ -167,4 +171,3 @@ class BaseMaterialsModel(ListModel):
"is_favorite": root_material_id in self._favorite_ids
}
return item

View file

@ -19,11 +19,7 @@ class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel):
Logger.log("d", "No active GlobalStack, set %s as empty.", self.__class__.__name__)
return
variant_names = [extruder.variant.getName() for extruder in active_global_stack.extruders.values()]
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in active_global_stack.extruders.values()]
extruder_enabled = [extruder.isEnabled for extruder in active_global_stack.extruders.values()]
machine_node = ContainerTree.getInstance().machines[active_global_stack.definition.getId()]
quality_changes_list = machine_node.getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
quality_changes_list = ContainerTree.getInstance().getCurrentQualityChangesGroups()
item_list = []
for quality_changes_group in sorted(quality_changes_list, key = lambda qgc: qgc.name.lower()):

View file

@ -33,6 +33,8 @@ class IntentModel(ListModel):
self._intent_category = "engineering"
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
machine_manager.globalContainerChanged.connect(self._update)
ContainerRegistry.getInstance().containerAdded.connect(self._onChanged)
ContainerRegistry.getInstance().containerRemoved.connect(self._onChanged)
self._layer_height_unit = "" # This is cached

View file

@ -0,0 +1,169 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import copy # To duplicate materials.
from PyQt5.QtCore import QObject, pyqtSlot # To allow the preference page proxy to be used from the actual preferences page.
from typing import Any, Dict, Optional, TYPE_CHECKING
import uuid # To generate new GUIDs for new materials.
from UM.i18n import i18nCatalog
from UM.Logger import Logger
import cura.CuraApplication # Imported like this to prevent circular imports.
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To find the sets of materials belonging to each other, and currently loaded extruder stacks.
if TYPE_CHECKING:
from cura.Machines.MaterialNode import MaterialNode
catalog = i18nCatalog("cura")
## Proxy class to the materials page in the preferences.
#
# This class handles the actions in that page, such as creating new materials,
# renaming them, etc.
class MaterialManagementModel(QObject):
## Can a certain material be deleted, or is it still in use in one of the
# container stacks anywhere?
#
# We forbid the user from deleting a material if it's in use in any stack.
# Deleting it while it's in use can lead to corrupted stacks. In the
# future we might enable this functionality again (deleting the material
# from those stacks) but for now it is easier to prevent the user from
# doing this.
# \param material_node The ContainerTree node of the material to check.
# \return Whether or not the material can be removed.
@pyqtSlot("QVariant", result = bool)
def canMaterialBeRemoved(self, material_node: "MaterialNode"):
container_registry = CuraContainerRegistry.getInstance()
ids_to_remove = {metadata.get("id", "") for metadata in container_registry.findInstanceContainersMetadata(base_file = material_node.base_file)}
for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"):
if extruder_stack.material.getId() in ids_to_remove:
return False
return True
## Change the user-visible name of a material.
# \param material_node The ContainerTree node of the material to rename.
# \param name The new name for the material.
@pyqtSlot("QVariant", str)
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
container_registry = CuraContainerRegistry.getInstance()
root_material_id = material_node.base_file
if container_registry.isReadOnly(root_material_id):
Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
return
return container_registry.findContainers(id = root_material_id)[0].setName(name)
## Deletes a material from Cura.
#
# This function does not do any safety checking any more. Please call this
# function only if:
# - The material is not read-only.
# - The material is not used in any stacks.
# If the material was not lazy-loaded yet, this will fully load the
# container. When removing this material node, all other materials with
# the same base fill will also be removed.
# \param material_node The material to remove.
@pyqtSlot("QVariant")
def removeMaterial(self, material_node: "MaterialNode") -> None:
container_registry = CuraContainerRegistry.getInstance()
materials_this_base_file = container_registry.findContainersMetadata(base_file = material_node.base_file)
for material_metadata in materials_this_base_file:
container_registry.removeContainer(material_metadata["id"])
## Creates a duplicate of a material with the same GUID and base_file
# metadata.
# \param material_node The node representing the material to duplicate.
# \param new_base_id A new material ID for the base material. The IDs of
# the submaterials will be based off this one. If not provided, a material
# ID will be generated automatically.
# \param new_metadata Metadata for the new material. If not provided, this
# will be duplicated from the original material.
# \return The root material ID of the duplicate material.
@pyqtSlot("QVariant", result = str)
def duplicateMaterial(self, material_node: "MaterialNode", new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
container_registry = CuraContainerRegistry.getInstance()
root_materials = container_registry.findContainers(id = material_node.base_file)
if not root_materials:
Logger.log("i", "Unable to duplicate the root material with ID {root_id}, because it doesn't exist.".format(root_id = material_node.base_file))
return None
root_material = root_materials[0]
# Ensure that all settings are saved.
application = cura.CuraApplication.CuraApplication.getInstance()
application.saveSettings()
# Create a new ID and container to hold the data.
if new_base_id is None:
new_base_id = container_registry.uniqueName(root_material.getId())
new_root_material = copy.deepcopy(root_material)
new_root_material.getMetaData()["id"] = new_base_id
new_root_material.getMetaData()["base_file"] = new_base_id
if new_metadata is not None:
new_root_material.getMetaData().update(new_metadata)
new_containers = [new_root_material]
# Clone all submaterials.
for container_to_copy in container_registry.findInstanceContainers(base_file = material_node.base_file):
if container_to_copy.getId() == material_node.base_file:
continue # We already have that one. Skip it.
new_id = new_base_id
definition = container_to_copy.getMetaDataEntry("definition")
if definition != "fdmprinter":
new_id += "_" + definition
variant_name = container_to_copy.getMetaDataEntry("variant_name")
if variant_name:
new_id += "_" + variant_name.replace(" ", "_")
new_container = copy.deepcopy(container_to_copy)
new_container.getMetaData()["id"] = new_id
new_container.getMetaData()["base_file"] = new_base_id
if new_metadata is not None:
new_container.getMetaData().update(new_metadata)
new_containers.append(new_container)
for container_to_add in new_containers:
container_to_add.setDirty(True)
container_registry.addContainer(container_to_add)
# If the duplicated material was favorite then the new material should also be added to the favorites.
favorites_set = set(application.getPreferences().getValue("cura/favorite_materials").split(";"))
if material_node.base_file in favorites_set:
favorites_set.add(new_base_id)
application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites_set))
return new_base_id
## Create a new material by cloning the preferred material for the current
# material diameter and generate a new GUID.
#
# The material type is explicitly left to be the one from the preferred
# material, since this allows the user to still have SOME profiles to work
# with.
# \return The ID of the newly created material.
@pyqtSlot(result = str)
def createMaterial(self) -> str:
# Ensure all settings are saved.
application = cura.CuraApplication.CuraApplication.getInstance()
application.saveSettings()
# Find the preferred material.
extruder_stack = application.getMachineManager().activeStack
active_variant_name = extruder_stack.variant.getName()
approximate_diameter = int(extruder_stack.approximateMaterialDiameter)
global_container_stack = application.getGlobalContainerStack()
if not global_container_stack:
return ""
machine_node = ContainerTree.getInstance().machines[global_container_stack.definition.getId()]
preferred_material_node = machine_node.variants[active_variant_name].preferredMaterial(approximate_diameter)
# Create a new ID & new metadata for the new material.
new_id = CuraContainerRegistry.getInstance().uniqueName("custom_material")
new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
"brand": catalog.i18nc("@label", "Custom"),
"GUID": str(uuid.uuid4()),
}
self.duplicateMaterial(preferred_material_node, new_base_id = new_id, new_metadata = new_metadata)
return new_id

View file

@ -1,11 +1,22 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSlot
from typing import Any, Dict, Optional, TYPE_CHECKING
from PyQt5.QtCore import pyqtSlot, QObject, Qt
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.Machines.ContainerTree import ContainerTree
from cura.Settings.cura_empty_instance_containers import empty_quality_changes_container
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
#
# This the QML model for the quality management page.
@ -24,17 +35,136 @@ class QualityManagementModel(ListModel):
self.addRoleName(self.QualityGroupRole, "quality_group")
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
from cura.CuraApplication import CuraApplication
self._container_registry = CuraApplication.getInstance().getContainerRegistry()
self._machine_manager = CuraApplication.getInstance().getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
self._quality_manager = CuraApplication.getInstance().getQualityManager()
application = cura.CuraApplication.CuraApplication.getInstance()
container_registry = application.getContainerRegistry()
self._machine_manager = application.getMachineManager()
self._extruder_manager = application.getExtruderManager()
self._machine_manager.globalContainerChanged.connect(self._update)
self._quality_manager.qualitiesUpdated.connect(self._update)
container_registry.containerAdded.connect(self._qualityChangesListChanged)
container_registry.containerRemoved.connect(self._qualityChangesListChanged)
container_registry.containerMetaDataChanged.connect(self._qualityChangesListChanged)
self._update()
## Deletes a custom profile. It will be gone forever.
# \param quality_changes_group The quality changes group representing the
# profile to delete.
@pyqtSlot(QObject)
def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
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
## 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.
@pyqtSlot(QObject, str, result = str)
def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str:
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()
new_name = application.getContainerRegistry().uniqueName(new_name)
for node in quality_changes_group.getAllNodes():
container = node.container
if container:
container.setName(new_name)
quality_changes_group.name = new_name
application.getMachineManager().activeQualityChanged.emit()
application.getMachineManager().activeQualityGroupChanged.emit()
return new_name
## 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.
@pyqtSlot(str, "QVariantMap")
def duplicateQualityChanges(self, new_name: str, quality_model_item: Dict[str, Any]) -> None:
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)
quality_group = quality_model_item["quality_group"]
quality_changes_group = quality_model_item["quality_changes_group"]
if quality_changes_group is None:
# Create global quality changes only.
new_quality_changes = self._createQualityChanges(quality_group.quality_type, new_name, global_stack, extruder_stack = None)
container_registry.addContainer(new_quality_changes)
else:
for metadata in [quality_changes_group.metadata_for_global] + 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))
## Create a quality changes container with the given set-up.
# \param quality_type The quality type 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.
def _createQualityChanges(self, quality_type: str, new_name: str, machine: "GlobalStack", extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer":
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 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
## 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.
def _qualityChangesListChanged(self, container: "ContainerInterface") -> None:
if container.getMetaDataEntry("type") == "quality_changes":
self._update()
def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
@ -43,12 +173,13 @@ class QualityManagementModel(ListModel):
self.setItems([])
return
quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups()
quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(global_stack)
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_dict:
if not available_quality_types and not quality_changes_group_list:
# Nothing to show
self.setItems([])
return
@ -69,7 +200,7 @@ class QualityManagementModel(ListModel):
# Create quality_changes group items
quality_changes_item_list = []
for quality_changes_group in quality_changes_group_dict.values():
for quality_changes_group in quality_changes_group_list:
quality_group = quality_group_dict.get(quality_changes_group.quality_type)
item = {"name": quality_changes_group.name,
"is_read_only": False,

View file

@ -1,9 +1,9 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
from UM.Application import Application
import cura.CuraApplication
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerRegistry import ContainerRegistry
@ -35,15 +35,13 @@ class QualitySettingsModel(ListModel):
self.addRoleName(self.CategoryRole, "category")
self._container_registry = ContainerRegistry.getInstance()
self._application = Application.getInstance()
self._quality_manager = self._application.getQualityManager()
self._application = cura.CuraApplication.CuraApplication.getInstance()
self._application.getMachineManager().activeStackChanged.connect(self._update)
self._selected_position = self.GLOBAL_STACK_POSITION #Must be either GLOBAL_STACK_POSITION or an extruder position (0, 1, etc.)
self._selected_quality_item = None # The selected quality in the quality management page
self._i18n_catalog = None
self._quality_manager.qualitiesUpdated.connect(self._update)
self._update()
selectedPositionChanged = pyqtSignal()
@ -93,21 +91,33 @@ class QualitySettingsModel(ListModel):
quality_node = quality_group.nodes_for_extruders.get(str(self._selected_position))
settings_keys = quality_group.getAllKeys()
quality_containers = []
if quality_node is not None and quality_node.getContainer() is not None:
quality_containers.append(quality_node.getContainer())
if quality_node is not None and quality_node.container is not None:
quality_containers.append(quality_node.container)
# Here, if the user has selected a quality changes, then "quality_changes_group" will not be None, and we fetch
# the settings in that quality_changes_group.
if quality_changes_group is not None:
if self._selected_position == self.GLOBAL_STACK_POSITION:
quality_changes_node = quality_changes_group.node_for_global
container_registry = ContainerRegistry.getInstance()
global_containers = container_registry.findContainers(id = quality_changes_group.metadata_for_global["id"])
global_container = None if len(global_containers) == 0 else global_containers[0]
extruders_containers = {pos: container_registry.findContainers(id = quality_changes_group.metadata_per_extruder[pos]["id"]) for pos in quality_changes_group.metadata_per_extruder}
extruders_container = {pos: None if not containers else containers[0] for pos, containers in extruders_containers.items()}
if self._selected_position == self.GLOBAL_STACK_POSITION and global_container:
quality_changes_metadata = global_container.getMetaData()
else:
quality_changes_node = quality_changes_group.nodes_for_extruders.get(str(self._selected_position))
if quality_changes_node is not None and quality_changes_node.container is not None: # it can be None if number of extruders are changed during runtime
quality_containers.insert(0, quality_changes_node.container)
settings_keys.update(quality_changes_group.getAllKeys())
quality_changes_metadata = extruders_container.get(str(self._selected_position))
if quality_changes_metadata is not None: # It can be None if number of extruders are changed during runtime.
container = container_registry.findContainers(id = quality_changes_metadata["id"])
if container:
quality_containers.insert(0, container[0])
# We iterate over all definitions instead of settings in a quality/qualtiy_changes group is because in the GUI,
if global_container:
settings_keys.update(global_container.getAllKeys())
for container in extruders_container.values():
if container:
settings_keys.update(container.getAllKeys())
# We iterate over all definitions instead of settings in a quality/quality_changes group is because in the GUI,
# the settings are grouped together by categories, and we had to go over all the definitions to figure out
# which setting belongs in which category.
current_category = ""