Merge branch 'feature_intent'

Here we go!

Contributes to issue CURA-6864.
This commit is contained in:
Ghostkeeper 2019-10-08 13:17:54 +02:00
commit cf14fa6820
No known key found for this signature in database
GPG key ID: 86BEF881AE2CF276
154 changed files with 5293 additions and 4600 deletions

View file

@ -15,7 +15,7 @@ from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qm
from UM.i18n import i18nCatalog
from UM.Application import Application
from UM.Decorators import override
from UM.Decorators import override, deprecated
from UM.FlameProfiler import pyqtSlot
from UM.Logger import Logger
from UM.Message import Message
@ -23,7 +23,6 @@ from UM.Platform import Platform
from UM.PluginError import PluginNotFoundError
from UM.Resources import Resources
from UM.Preferences import Preferences
from UM.Qt.Bindings import MainWindow
from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
import UM.Util
from UM.View.SelectionPass import SelectionPass # For typing.
@ -47,7 +46,6 @@ from UM.Scene.Selection import Selection
from UM.Scene.ToolHandle import ToolHandle
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
from UM.Settings.SettingFunction import SettingFunction
@ -73,7 +71,10 @@ from cura.Scene.GCodeListDecorator import GCodeListDecorator
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene import ZOffsetDecorator
from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.MachineErrorChecker import MachineErrorChecker
import cura.Machines.MaterialManager #Imported like this to prevent circular imports.
import cura.Machines.QualityManager #Imported like this to prevent circular imports.
from cura.Machines.VariantManager import VariantManager
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
@ -85,6 +86,7 @@ from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachine
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
from cura.Machines.Models.NozzleModel import NozzleModel
from cura.Machines.Models.QualityManagementModel import QualityManagementModel
@ -92,6 +94,8 @@ from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfile
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
from cura.Machines.Models.UserChangesModel import UserChangesModel
from cura.Machines.Models.IntentModel import IntentModel
from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
@ -101,8 +105,10 @@ from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.MachineManager import MachineManager
from cura.Settings.MachineNameValidator import MachineNameValidator
from cura.Settings.IntentManager import IntentManager
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
@ -130,13 +136,10 @@ from . import CuraActions
from . import PrintJobPreviewImageProvider
from cura import ApplicationMetadata, UltimakerCloudAuthentication
from cura.Settings.GlobalStack import GlobalStack
if TYPE_CHECKING:
from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
from cura.Settings.GlobalStack import GlobalStack
numpy.seterr(all = "ignore")
@ -161,6 +164,7 @@ class CuraApplication(QtApplication):
ExtruderStack = Resources.UserType + 9
DefinitionChangesContainer = Resources.UserType + 10
SettingVisibilityPreset = Resources.UserType + 11
IntentInstanceContainer = Resources.UserType + 12
Q_ENUMS(ResourceTypes)
@ -197,13 +201,12 @@ class CuraApplication(QtApplication):
self.empty_container = None # type: EmptyInstanceContainer
self.empty_definition_changes_container = None # type: EmptyInstanceContainer
self.empty_variant_container = None # type: EmptyInstanceContainer
self.empty_intent_container = None # type: EmptyInstanceContainer
self.empty_material_container = None # type: EmptyInstanceContainer
self.empty_quality_container = None # type: EmptyInstanceContainer
self.empty_quality_changes_container = None # type: EmptyInstanceContainer
self._variant_manager = None
self._material_manager = None
self._quality_manager = None
self._machine_manager = None
self._extruder_manager = None
self._container_manager = None
@ -220,6 +223,8 @@ class CuraApplication(QtApplication):
self._machine_error_checker = None
self._machine_settings_manager = MachineSettingsManager(self, parent = self)
self._material_management_model = None
self._quality_management_model = None
self._discovered_printer_model = DiscoveredPrintersModel(self, parent = self)
self._first_start_machine_actions_model = FirstStartMachineActionsModel(self, parent = self)
@ -346,7 +351,7 @@ class CuraApplication(QtApplication):
# Adds expected directory names and search paths for Resources.
def __addExpectedResourceDirsAndSearchPaths(self):
# this list of dir names will be used by UM to detect an old cura directory
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants"]:
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]:
Resources.addExpectedDirNameInData(dir_name)
Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
@ -404,6 +409,7 @@ class CuraApplication(QtApplication):
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
Resources.addStorageType(self.ResourceTypes.SettingVisibilityPreset, "setting_visibility")
Resources.addStorageType(self.ResourceTypes.IntentInstanceContainer, "intent")
self._container_registry.addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality")
self._container_registry.addResourceType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
@ -413,6 +419,7 @@ class CuraApplication(QtApplication):
self._container_registry.addResourceType(self.ResourceTypes.ExtruderStack, "extruder_train")
self._container_registry.addResourceType(self.ResourceTypes.MachineStack, "machine")
self._container_registry.addResourceType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
self._container_registry.addResourceType(self.ResourceTypes.IntentInstanceContainer, "intent")
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
Resources.addType(self.ResourceTypes.Firmware, "firmware")
@ -431,6 +438,9 @@ class CuraApplication(QtApplication):
self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_variant_container)
self.empty_variant_container = cura.Settings.cura_empty_instance_containers.empty_variant_container
self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_intent_container)
self.empty_intent_container = cura.Settings.cura_empty_instance_containers.empty_intent_container
self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_material_container)
self.empty_material_container = cura.Settings.cura_empty_instance_containers.empty_material_container
@ -445,14 +455,15 @@ class CuraApplication(QtApplication):
def __setLatestResouceVersionsForVersionUpgrade(self):
self._version_upgrade_manager.setCurrentVersions(
{
("quality", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
("quality_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityChangesInstanceContainer, "application/x-uranium-instancecontainer"),
("machine_stack", ContainerStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.MachineStack, "application/x-cura-globalstack"),
("extruder_train", ContainerStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.ExtruderStack, "application/x-cura-extruderstack"),
("preferences", Preferences.Version * 1000000 + self.SettingVersion): (Resources.Preferences, "application/x-uranium-preferences"),
("user", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer"),
("definition_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.DefinitionChangesContainer, "application/x-uranium-instancecontainer"),
("variant", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.VariantInstanceContainer, "application/x-uranium-instancecontainer"),
("quality", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
("quality_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityChangesInstanceContainer, "application/x-uranium-instancecontainer"),
("intent", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.IntentInstanceContainer, "application/x-uranium-instancecontainer"),
("machine_stack", GlobalStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.MachineStack, "application/x-cura-globalstack"),
("extruder_train", ExtruderStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.ExtruderStack, "application/x-cura-extruderstack"),
("preferences", Preferences.Version * 1000000 + self.SettingVersion): (Resources.Preferences, "application/x-uranium-preferences"),
("user", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer"),
("definition_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.DefinitionChangesContainer, "application/x-uranium-instancecontainer"),
("variant", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.VariantInstanceContainer, "application/x-uranium-instancecontainer"),
}
)
@ -504,6 +515,8 @@ class CuraApplication(QtApplication):
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading machines..."))
self._container_registry.allMetadataLoaded.connect(ContainerRegistry.getInstance)
with self._container_registry.lockFile():
self._container_registry.loadAllMetadata()
@ -732,21 +745,6 @@ class CuraApplication(QtApplication):
def run(self):
super().run()
container_registry = self._container_registry
Logger.log("i", "Initializing variant manager")
self._variant_manager = VariantManager(container_registry)
self._variant_manager.initialize()
Logger.log("i", "Initializing material manager")
from cura.Machines.MaterialManager import MaterialManager
self._material_manager = MaterialManager(container_registry, parent = self)
self._material_manager.initialize()
Logger.log("i", "Initializing quality manager")
from cura.Machines.QualityManager import QualityManager
self._quality_manager = QualityManager(self, parent = self)
self._quality_manager.initialize()
Logger.log("i", "Initializing machine manager")
self._machine_manager = MachineManager(self, parent = self)
@ -926,16 +924,22 @@ class CuraApplication(QtApplication):
self._extruder_manager = ExtruderManager()
return self._extruder_manager
@deprecated("Use the ContainerTree structure instead.", since = "4.3")
def getVariantManager(self, *args) -> VariantManager:
return self._variant_manager
return VariantManager.getInstance()
# Can't deprecate this function since the deprecation marker collides with pyqtSlot!
@pyqtSlot(result = QObject)
def getMaterialManager(self, *args) -> "MaterialManager":
return self._material_manager
def getMaterialManager(self, *args) -> cura.Machines.MaterialManager.MaterialManager:
return cura.Machines.MaterialManager.MaterialManager.getInstance()
# Can't deprecate this function since the deprecation marker collides with pyqtSlot!
@pyqtSlot(result = QObject)
def getQualityManager(self, *args) -> "QualityManager":
return self._quality_manager
def getQualityManager(self, *args) -> cura.Machines.QualityManager.QualityManager:
return cura.Machines.QualityManager.QualityManager.getInstance()
def getIntentManager(self, *args) -> IntentManager:
return IntentManager.getInstance()
def getObjectsModel(self, *args):
if self._object_manager is None:
@ -983,6 +987,18 @@ class CuraApplication(QtApplication):
def getMachineActionManager(self, *args):
return self._machine_action_manager
@pyqtSlot(result = QObject)
def getMaterialManagementModel(self) -> MaterialManagementModel:
if not self._material_management_model:
self._material_management_model = MaterialManagementModel(parent = self)
return self._material_management_model
@pyqtSlot(result = QObject)
def getQualityManagementModel(self) -> QualityManagementModel:
if not self._quality_management_model:
self._quality_management_model = QualityManagementModel(parent = self)
return self._quality_management_model
def getSimpleModeSettingsManager(self, *args):
if self._simple_mode_settings_manager is None:
self._simple_mode_settings_manager = SimpleModeSettingsManager()
@ -1036,6 +1052,7 @@ class CuraApplication(QtApplication):
qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 0, "SceneController", self.getCuraSceneController)
qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager)
qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
qmlRegisterSingletonType(IntentManager, "Cura", 1, 6, "IntentManager", self.getIntentManager)
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager", self.getSettingInheritanceManager)
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
@ -1060,7 +1077,8 @@ class CuraApplication(QtApplication):
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
qmlRegisterSingletonType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel", self.getQualityManagementModel)
qmlRegisterSingletonType(MaterialManagementModel, "Cura", 1, 5, "MaterialManagementModel", self.getMaterialManagementModel)
qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel")
@ -1069,6 +1087,8 @@ class CuraApplication(QtApplication):
qmlRegisterSingletonType(CustomQualityProfilesDropDownMenuModel, "Cura", 1, 0,
"CustomQualityProfilesDropDownMenuModel", self.getCustomQualityProfilesDropDownMenuModel)
qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel")
qmlRegisterType(IntentModel, "Cura", 1, 6, "IntentModel")
qmlRegisterType(IntentCategoryModel, "Cura", 1, 6, "IntentCategoryModel")
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel")

View file

@ -1,64 +1,76 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Any, Dict, Union, TYPE_CHECKING
from collections import OrderedDict
from typing import Any, Dict, Optional
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Logger import Logger
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Decorators import deprecated
##
# A metadata / container combination. Use getContainer() to get the container corresponding to the metadata.
#
# ContainerNode is a multi-purpose class. It has two main purposes:
# 1. It encapsulates an InstanceContainer. It contains that InstanceContainer's
# - metadata (Always)
# - container (lazy-loaded when needed)
# 2. It also serves as a node in a hierarchical InstanceContainer lookup table/tree.
# This is used in Variant, Material, and Quality Managers.
## A node in the container tree. It represents one container.
#
# The container it represents is referenced by its container_id. During normal
# use of the tree, this container is not constructed. Only when parts of the
# tree need to get loaded in the container stack should it get constructed.
class ContainerNode:
__slots__ = ("_metadata", "_container", "children_map")
## Creates a new node for the container tree.
# \param container_id The ID of the container that this node should
# represent.
def __init__(self, container_id: str) -> None:
self.container_id = container_id
self._container = None # type: Optional[InstanceContainer]
self.children_map = {} # type: Dict[str, ContainerNode] # Mapping from container ID to container node.
def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
self._metadata = metadata
self._container = None # type: Optional[InstanceContainer]
self.children_map = OrderedDict() # type: ignore # This is because it's children are supposed to override it.
## Gets the metadata of the container that this node represents.
# Getting the metadata from the container directly is about 10x as fast.
# \return The metadata of the container in this node.
def getMetadata(self):
return ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id)[0]
## Get an entry value from the metadata
## Get an entry from the metadata of the container that this node contains.
#
# This is just a convenience function.
# \param entry The metadata entry key to return.
# \param default If the metadata is not present or the container is not
# found, the value of this default is returned.
# \return The value of the metadata entry, or the default if it was not
# present.
def getMetaDataEntry(self, entry: str, default: Any = None) -> Any:
if self._metadata is None:
container_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id)
if len(container_metadata) == 0:
return default
return self._metadata.get(entry, default)
return container_metadata[0].get(entry, default)
def getMetadata(self) -> Dict[str, Any]:
if self._metadata is None:
return {}
return self._metadata
## Get the child with the specified container ID.
# \param child_id The container ID to get from among the children.
# \return The child node, or ``None`` if no child is present with the
# specified ID.
@deprecated("Iterate over the children instead of requesting them one by one.", "4.3")
def getChildNode(self, child_id: str) -> Optional["ContainerNode"]:
return self.children_map.get(child_id)
def getChildNode(self, child_key: str) -> Optional["ContainerNode"]:
return self.children_map.get(child_key)
@deprecated("Use `.container` instead.", "4.3")
def getContainer(self) -> Optional[InstanceContainer]:
return self.container
def getContainer(self) -> Optional["InstanceContainer"]:
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"]
from UM.Settings.ContainerRegistry import ContainerRegistry
container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
if not container_list:
Logger.log("e", "Failed to lazy-load container [{container_id}]. Cannot find it.".format(container_id = container_id))
## The container that this node's container ID refers to.
#
# This can be used to finally instantiate the container in order to put it
# in the container stack.
# \return A container.
@property
def container(self) -> Optional[InstanceContainer]:
if not self._container:
container_list = ContainerRegistry.getInstance().findInstanceContainers(id = self.container_id)
if len(container_list) == 0:
Logger.log("e", "Failed to lazy-load container [{container_id}]. Cannot find it.".format(container_id = self.container_id))
error_message = ConfigurationErrorMessage.getInstance()
error_message.addFaultyContainers(container_id)
error_message.addFaultyContainers(self.container_id)
return None
self._container = container_list[0]
return self._container
def __str__(self) -> str:
return "%s[%s]" % (self.__class__.__name__, self.getMetaDataEntry("id"))
return "%s[%s]" % (self.__class__.__name__, self.container_id)

View file

@ -0,0 +1,129 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry # To listen to containers being added.
from UM.Settings.Interfaces import ContainerInterface
from UM.Signal import Signal
import cura.CuraApplication # Imported like this to prevent circular dependencies.
from cura.Machines.MachineNode import MachineNode
from cura.Settings.GlobalStack import GlobalStack # To listen only to global stacks being added.
from typing import Dict, List, TYPE_CHECKING
import time
import UM.FlameProfiler
if TYPE_CHECKING:
from cura.Machines.QualityGroup import QualityGroup
from cura.Machines.QualityChangesGroup import QualityChangesGroup
## This class contains a look-up tree for which containers are available at
# which stages of configuration.
#
# The tree starts at the machine definitions. For every distinct definition
# there will be one machine node here.
#
# All of the fallbacks for material choices, quality choices, etc. should be
# encoded in this tree. There must always be at least one child node (for
# nodes that have children) but that child node may be a node representing the
# empty instance container.
class ContainerTree:
__instance = None
@classmethod
def getInstance(cls):
if cls.__instance is None:
cls.__instance = ContainerTree()
return cls.__instance
def __init__(self) -> None:
self.machines = {} # type: Dict[str, MachineNode] # Mapping from definition ID to machine nodes.
self.materialsChanged = Signal() # Emitted when any of the material nodes in the tree got changed.
container_registry = ContainerRegistry.getInstance()
container_registry.containerAdded.connect(self._machineAdded)
self._loadAll()
## Get the quality groups available for the currently activated printer.
#
# This contains all quality groups, enabled or disabled. To check whether
# the quality group can be activated, test for the
# ``QualityGroup.is_available`` property.
# \return For every quality type, one quality group.
def getCurrentQualityGroups(self) -> Dict[str, "QualityGroup"]:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return {}
variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList]
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList]
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
return self.machines[global_stack.definition.getId()].getQualityGroups(variant_names, material_bases, extruder_enabled)
## Get the quality changes groups available for the currently activated
# printer.
#
# This contains all quality changes groups, enabled or disabled. To check
# whether the quality changes group can be activated, test for the
# ``QualityChangesGroup.is_available`` property.
# \return A list of all quality changes groups.
def getCurrentQualityChangesGroups(self) -> List["QualityChangesGroup"]:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return []
variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList]
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList]
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
# Add a machine node by the id of it's definition.
# This is automatically called by the _machineAdded function, but it's sometimes needed to add a machine node
# faster than would have been done when waiting on any signals (for instance; when creating an entirely new machine)
@UM.FlameProfiler.profile
def addMachineNodeByDefinitionId(self, definition_id: str) -> None:
if definition_id in self.machines:
return # Already have this definition ID.
start_time = time.time()
self.machines[definition_id] = MachineNode(definition_id)
self.machines[definition_id].materialsChanged.connect(self.materialsChanged)
Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - start_time))
## Builds the initial container tree.
def _loadAll(self):
Logger.log("i", "Building container tree.")
start_time = time.time()
all_stacks = ContainerRegistry.getInstance().findContainerStacks()
for stack in all_stacks:
if not isinstance(stack, GlobalStack):
continue # Only want to load global stacks. We don't need to create a tree for extruder definitions.
definition_id = stack.definition.getId()
if definition_id not in self.machines:
definition_start_time = time.time()
self.machines[definition_id] = MachineNode(definition_id)
self.machines[definition_id].materialsChanged.connect(self.materialsChanged)
Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - definition_start_time))
Logger.log("d", "Building the container tree took %s seconds", time.time() - start_time)
## When a printer gets added, we need to build up the tree for that container.
def _machineAdded(self, container_stack: ContainerInterface) -> None:
if not isinstance(container_stack, GlobalStack):
return # Not our concern.
self.addMachineNodeByDefinitionId(container_stack.definition.getId())
## For debugging purposes, visualise the entire container tree as it stands
# now.
def _visualise_tree(self) -> str:
lines = ["% CONTAINER TREE"] # Start with array and then combine into string, for performance.
for machine in self.machines.values():
lines.append(" # " + machine.container_id)
for variant in machine.variants.values():
lines.append(" * " + variant.container_id)
for material in variant.materials.values():
lines.append(" + " + material.container_id)
for quality in material.qualities.values():
lines.append(" - " + quality.container_id)
for intent in quality.intents.values():
lines.append(" . " + intent.container_id)
return "\n".join(lines)

View file

@ -0,0 +1,21 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Machines.ContainerNode import ContainerNode
if TYPE_CHECKING:
from cura.Machines.QualityNode import QualityNode
## This class represents an intent profile in the container tree.
#
# This class has no more subnodes.
class IntentNode(ContainerNode):
def __init__(self, container_id: str, quality: "QualityNode") -> None:
super().__init__(container_id)
self.quality = quality
self.intent_category = ContainerRegistry.getInstance().findContainersMetadata(id = container_id)[0].get("intent_category", "default")

View file

@ -0,0 +1,186 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Dict, List
from UM.Logger import Logger
from UM.Signal import Signal
from UM.Util import parseBool
from UM.Settings.ContainerRegistry import ContainerRegistry # To find all the variants for this machine.
import cura.CuraApplication # Imported like this to prevent circular dependencies.
from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.QualityChangesGroup import QualityChangesGroup # To construct groups of quality changes profiles that belong together.
from cura.Machines.QualityGroup import QualityGroup # To construct groups of quality profiles that belong together.
from cura.Machines.QualityNode import QualityNode
from cura.Machines.VariantNode import VariantNode
import UM.FlameProfiler
## This class represents a machine in the container tree.
#
# The subnodes of these nodes are variants.
class MachineNode(ContainerNode):
def __init__(self, container_id: str) -> None:
super().__init__(container_id)
self.variants = {} # type: Dict[str, VariantNode] # Mapping variant names to their nodes.
self.global_qualities = {} # type: Dict[str, QualityNode] # Mapping quality types to the global quality for those types.
self.materialsChanged = Signal() # Emitted when one of the materials underneath this machine has been changed.
container_registry = ContainerRegistry.getInstance()
try:
my_metadata = container_registry.findContainersMetadata(id = container_id)[0]
except IndexError:
Logger.log("Unable to find metadata for container %s", container_id)
my_metadata = {}
# Some of the metadata is cached upon construction here.
# ONLY DO THAT FOR METADATA THAT DOESN'T CHANGE DURING RUNTIME!
# Otherwise you need to keep it up-to-date during runtime.
self.has_materials = parseBool(my_metadata.get("has_materials", "true"))
self.has_variants = parseBool(my_metadata.get("has_variants", "false"))
self.has_machine_quality = parseBool(my_metadata.get("has_machine_quality", "false"))
self.quality_definition = my_metadata.get("quality_definition", container_id) if self.has_machine_quality else "fdmprinter"
self.exclude_materials = my_metadata.get("exclude_materials", [])
self.preferred_variant_name = my_metadata.get("preferred_variant_name", "")
self.preferred_material = my_metadata.get("preferred_material", "")
self.preferred_quality_type = my_metadata.get("preferred_quality_type", "")
self._loadAll()
## Get the available quality groups for this machine.
#
# This returns all quality groups, regardless of whether they are
# available to the combination of extruders or not. On the resulting
# quality groups, the is_available property is set to indicate whether the
# quality group can be selected according to the combination of extruders
# in the parameters.
# \param variant_names The names of the variants loaded in each extruder.
# \param material_bases The base file names of the materials loaded in
# each extruder.
# \param extruder_enabled Whether or not the extruders are enabled. This
# allows the function to set the is_available properly.
# \return For each available quality type, a QualityGroup instance.
def getQualityGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> Dict[str, QualityGroup]:
if len(variant_names) != len(material_bases) or len(variant_names) != len(extruder_enabled):
Logger.log("e", "The number of extruders in the list of variants (" + str(len(variant_names)) + ") is not equal to the number of extruders in the list of materials (" + str(len(material_bases)) + ") or the list of enabled extruders (" + str(len(extruder_enabled)) + ").")
return {}
# For each extruder, find which quality profiles are available. Later we'll intersect the quality types.
qualities_per_type_per_extruder = [{}] * len(variant_names) # type: List[Dict[str, QualityNode]]
for extruder_nr, variant_name in enumerate(variant_names):
if not extruder_enabled[extruder_nr]:
continue # No qualities are available in this extruder. It'll get skipped when calculating the available quality types.
material_base = material_bases[extruder_nr]
if variant_name not in self.variants or material_base not in self.variants[variant_name].materials:
# The printer has no variant/material-specific quality profiles. Use the global quality profiles.
qualities_per_type_per_extruder[extruder_nr] = self.global_qualities
else:
# Use the actually specialised quality profiles.
qualities_per_type_per_extruder[extruder_nr] = {node.quality_type: node for node in self.variants[variant_name].materials[material_base].qualities.values()}
# Create the quality group for each available type.
quality_groups = {}
for quality_type, global_quality_node in self.global_qualities.items():
if not global_quality_node.container:
Logger.log("w", "Node {0} doesn't have a container.".format(global_quality_node.container_id))
continue
quality_groups[quality_type] = QualityGroup(name = global_quality_node.getMetaDataEntry("name", "Unnamed profile"), quality_type = quality_type)
quality_groups[quality_type].node_for_global = global_quality_node
for extruder, qualities_per_type in enumerate(qualities_per_type_per_extruder):
if quality_type in qualities_per_type:
quality_groups[quality_type].nodes_for_extruders[extruder] = qualities_per_type[quality_type]
available_quality_types = set(quality_groups.keys())
for extruder_nr, qualities_per_type in enumerate(qualities_per_type_per_extruder):
if not extruder_enabled[extruder_nr]:
continue
available_quality_types.intersection_update(qualities_per_type.keys())
for quality_type in available_quality_types:
quality_groups[quality_type].is_available = True
return quality_groups
## Returns all of the quality changes groups available to this printer.
#
# The quality changes groups store which quality type and intent category
# they were made for, but not which material and nozzle. Instead for the
# quality type and intent category, the quality changes will always be
# available but change the quality type and intent category when
# activated.
#
# The quality changes group does depend on the printer: Which quality
# definition is used.
#
# The quality changes groups that are available do depend on the quality
# types that are available, so it must still be known which extruders are
# enabled and which materials and variants are loaded in them. This allows
# setting the correct is_available flag.
# \param variant_names The names of the variants loaded in each extruder.
# \param material_bases The base file names of the materials loaded in
# each extruder.
# \param extruder_enabled For each extruder whether or not they are
# enabled.
# \return List of all quality changes groups for the printer.
def getQualityChangesGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> List[QualityChangesGroup]:
machine_quality_changes = ContainerRegistry.getInstance().findContainersMetadata(type = "quality_changes", definition = self.quality_definition) # All quality changes for each extruder.
groups_by_name = {} #type: Dict[str, QualityChangesGroup] # Group quality changes profiles by their display name. The display name must be unique for quality changes. This finds profiles that belong together in a group.
for quality_changes in machine_quality_changes:
name = quality_changes["name"]
if name not in groups_by_name:
# CURA-6599
# For some reason, QML will get null or fail to convert type for MachineManager.activeQualityChangesGroup() to
# a QObject. Setting the object ownership to QQmlEngine.CppOwnership doesn't work, but setting the object
# parent to application seems to work.
from cura.CuraApplication import CuraApplication
groups_by_name[name] = QualityChangesGroup(name, quality_type = quality_changes["quality_type"],
intent_category = quality_changes.get("intent_category", "default"),
parent = CuraApplication.getInstance())
elif groups_by_name[name].intent_category == "default": # Intent category should be stored as "default" if everything is default or as the intent if any of the extruder have an actual intent.
groups_by_name[name].intent_category = quality_changes.get("intent_category", "default")
if "position" in quality_changes: # An extruder profile.
groups_by_name[name].metadata_per_extruder[int(quality_changes["position"])] = quality_changes
else: # Global profile.
groups_by_name[name].metadata_for_global = quality_changes
quality_groups = self.getQualityGroups(variant_names, material_bases, extruder_enabled)
for quality_changes_group in groups_by_name.values():
if quality_changes_group.quality_type not in quality_groups:
quality_changes_group.is_available = False
else:
# Quality changes group is available iff the quality group it depends on is available. Irrespective of whether the intent category is available.
quality_changes_group.is_available = quality_groups[quality_changes_group.quality_type].is_available
return list(groups_by_name.values())
## Gets the preferred global quality node, going by the preferred quality
# type.
#
# If the preferred global quality is not in there, an arbitrary global
# quality is taken.
# If there are no global qualities, an empty quality is returned.
def preferredGlobalQuality(self) -> "QualityNode":
return self.global_qualities.get(self.preferred_quality_type, next(iter(self.global_qualities.values())))
## (Re)loads all variants under this printer.
@UM.FlameProfiler.profile
def _loadAll(self) -> None:
container_registry = ContainerRegistry.getInstance()
if not self.has_variants:
self.variants["empty"] = VariantNode("empty_variant", machine = self)
else:
# Find all the variants for this definition ID.
variants = container_registry.findInstanceContainersMetadata(type = "variant", definition = self.container_id, hardware_type = "nozzle")
for variant in variants:
variant_name = variant["name"]
if variant_name not in self.variants:
self.variants[variant_name] = VariantNode(variant["id"], machine = self)
self.variants[variant_name].materialsChanged.connect(self.materialsChanged)
if not self.variants:
self.variants["empty"] = VariantNode("empty_variant", machine = self)
# Find the global qualities for this printer.
global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.quality_definition, global_quality = "True") # First try specific to this printer.
if len(global_qualities) == 0: # This printer doesn't override the global qualities.
global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter", global_quality = "True") # Otherwise pick the global global qualities.
for global_quality in global_qualities:
self.global_qualities[global_quality["quality_type"]] = QualityNode(global_quality["id"], parent = self)

View file

@ -1,23 +1,23 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from collections import defaultdict, OrderedDict
from collections import defaultdict
import copy
import uuid
from typing import Dict, Optional, TYPE_CHECKING, Any, Set, List, cast, Tuple
from typing import Dict, Optional, TYPE_CHECKING, Any, List, cast
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
from UM.Application import Application
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Decorators import deprecated
from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.SettingFunction import SettingFunction
from UM.Util import parseBool
import cura.CuraApplication # Imported like this to prevent circular imports.
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from .MaterialNode import MaterialNode
from .MaterialGroup import MaterialGroup
from .VariantType import VariantType
if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer
@ -37,30 +37,26 @@ if TYPE_CHECKING:
# because it's simple.
#
class MaterialManager(QObject):
__instance = None
@classmethod
@deprecated("Use the ContainerTree structure instead.", since = "4.3")
def getInstance(cls) -> "MaterialManager":
if cls.__instance is None:
cls.__instance = MaterialManager()
return cls.__instance
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):
def __init__(self, parent = None):
super().__init__(parent)
self._application = Application.getInstance()
self._container_registry = container_registry # type: ContainerRegistry
# 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
# 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]
@ -68,218 +64,19 @@ class MaterialManager(QObject):
# 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.
self._default_machine_definition_id = "fdmprinter"
self._default_approximate_diameter_for_quality_search = "3"
# When a material gets added/imported, there can be more than one InstanceContainers. In those cases, we don't
# want to react on every container/metadata changed signal. The timer here is to buffer it a bit so we don't
# react too many time.
self._update_timer = QTimer(self)
self._update_timer.setInterval(300)
self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._updateMaps)
self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
self._favorites = set() # type: Set[str]
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")} # type: Dict[str, Dict[str, Any]]
self._material_group_map = dict()
# Map #1
# root_material_id -> MaterialGroup
for material_id, material_metadata in material_metadatas.items():
# We don't store empty material in the lookup tables
if material_id == "empty_material":
continue
root_material_id = material_metadata.get("base_file", "")
if root_material_id not in material_metadatas: #Not a registered material profile. Don't store this in the look-up tables.
continue
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)
group = self._material_group_map[root_material_id]
# Store this material in the group of the appropriate root material.
if material_id != root_material_id:
new_node = MaterialNode(material_metadata)
group.derived_material_node_list.append(new_node)
# Order this map alphabetically so it's easier to navigate in a debugger
self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0]))
# Map #1.5
# GUID -> material group list
self._guid_material_groups_map = defaultdict(list)
for root_material_id, material_group in self._material_group_map.items():
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() # 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.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.getMetaDataEntry("brand", "")
if brand.lower() == "generic":
to_add = True
if material_type in grouped_by_type_dict:
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
if to_add:
# Checking this first allow us to differentiate between not read only materials:
# - 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
material_types_without_fallback.remove(material_type)
# Remove the materials that have no fallback materials
for material_type in material_types_without_fallback:
del grouped_by_type_dict[material_type]
self._fallback_materials_map = grouped_by_type_dict
# Map #3
# There can be multiple material profiles for the same material with different diameters, such as "generic_pla"
# and "generic_pla_175". This is inconvenient when we do material-specific quality lookup because a quality can
# be for either "generic_pla" or "generic_pla_175", but not both. This map helps to get the correct material ID
# for quality search.
self._material_diameter_map = defaultdict(dict)
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() # 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
key_data_list = [] # type: List[Any]
for key in keys_to_fetch:
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:
material_group_dict[key_data] = dict()
else:
# ...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 = 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():
for root_material_id1 in data_dict.values():
if root_material_id1 in self._material_diameter_map:
continue
diameter_map = data_dict
for root_material_id2 in data_dict.values():
self._material_diameter_map[root_material_id2] = diameter_map
default_root_material_id = data_dict.get(self._default_approximate_diameter_for_quality_search)
if default_root_material_id is None:
default_root_material_id = list(data_dict.values())[0] # no default diameter present, just take "the" only one
for root_material_id in data_dict.values():
self._diameter_material_map[root_material_id] = default_root_material_id
# Map #4
# "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
self._diameter_machine_nozzle_buildplate_material_map = dict()
for material_metadata in material_metadatas.values():
self.__addMaterialMetadataIntoLookupTree(material_metadata)
favorites = self._application.getPreferences().getValue("cura/favorite_materials")
for item in favorites.split(";"):
self._favorites.add(item)
self._favorites = set(cura.CuraApplication.CuraApplication.getInstance().getPreferences().getValue("cura/favorite_materials").split(";"))
self.materialsUpdated.emit()
def __addMaterialMetadataIntoLookupTree(self, material_metadata: Dict[str, Any]) -> None:
material_id = material_metadata["id"]
self._update_timer = QTimer(self)
self._update_timer.setInterval(300)
# We don't store empty material in the lookup tables
if material_id == "empty_material":
return
self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self.materialsUpdated)
root_material_id = material_metadata["base_file"]
definition = material_metadata["definition"]
approximate_diameter = str(material_metadata["approximate_diameter"])
if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[
approximate_diameter]
if definition not in machine_nozzle_buildplate_material_map:
machine_nozzle_buildplate_material_map[definition] = MaterialNode()
# This is a list of information regarding the intermediate nodes:
# nozzle -> buildplate
nozzle_name = material_metadata.get("variant_name")
buildplate_name = material_metadata.get("buildplate_name")
intermediate_node_info_list = [(nozzle_name, VariantType.NOZZLE),
(buildplate_name, VariantType.BUILD_PLATE),
]
variant_manager = self._application.getVariantManager()
machine_node = machine_nozzle_buildplate_material_map[definition]
current_node = machine_node
current_intermediate_node_info_idx = 0
error_message = None # type: Optional[str]
while current_intermediate_node_info_idx < len(intermediate_node_info_list):
variant_name, variant_type = intermediate_node_info_list[current_intermediate_node_info_idx]
if variant_name is not None:
# The new material has a specific variant, so it needs to be added to that specific branch in the tree.
variant = variant_manager.getVariantNode(definition, variant_name, variant_type)
if variant is None:
error_message = "Material {id} contains a variant {name} that does not exist.".format(
id = material_metadata["id"], name = variant_name)
break
# Update the current node to advance to a more specific branch
if variant_name not in current_node.children_map:
current_node.children_map[variant_name] = MaterialNode()
current_node = current_node.children_map[variant_name]
current_intermediate_node_info_idx += 1
if error_message is not None:
Logger.log("e", "%s It will not be added into the material lookup tree.", error_message)
self._container_registry.addWrongContainerId(material_metadata["id"])
return
# Add the material to the current tree node, which is the deepest (the most specific) branch we can find.
# Sanity check: Make sure that there is no duplicated materials.
if root_material_id in current_node.material_map:
Logger.log("e", "Duplicated material [%s] with root ID [%s]. It has already been added.",
material_id, root_material_id)
ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id)
return
current_node.material_map[root_material_id] = MaterialNode(material_metadata)
def _updateMaps(self):
Logger.log("i", "Updating material lookup data ...")
self.initialize()
container_registry = ContainerRegistry.getInstance()
container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
container_registry.containerAdded.connect(self._onContainerMetadataChanged)
container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
def _onContainerMetadataChanged(self, container):
self._onContainerChanged(container)
@ -290,13 +87,22 @@ class MaterialManager(QObject):
return
# update the maps
self._update_timer.start()
def getMaterialGroup(self, root_material_id: str) -> Optional[MaterialGroup]:
return self._material_group_map.get(root_material_id)
def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str:
return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id)
original_material = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(id=root_material_id)[0]
if original_material["approximate_diameter"] == approximate_diameter:
return root_material_id
matching_materials = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "material", brand = original_material["brand"], definition = original_material["definition"], material = original_material["material"], color_name = original_material["color_name"])
for material in matching_materials:
if material["approximate_diameter"] == approximate_diameter:
return material["id"]
return root_material_id
def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
return self._diameter_material_map.get(root_material_id, "")
@ -308,73 +114,26 @@ class MaterialManager(QObject):
def getAllMaterialGroups(self) -> Dict[str, "MaterialGroup"]:
return self._material_group_map
#
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
#
def getAvailableMaterials(self, machine_definition: "DefinitionContainer", nozzle_name: Optional[str],
buildplate_name: Optional[str], diameter: float) -> Dict[str, 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:
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter)
return dict()
machine_definition_id = machine_definition.getId()
# If there are nozzle-and-or-buildplate materials, get the nozzle-and-or-buildplate material
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter]
machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
default_machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
nozzle_node = None
buildplate_node = None
if nozzle_name is not None and machine_node is not None:
nozzle_node = machine_node.getChildNode(nozzle_name)
# Get buildplate node if possible
if nozzle_node is not None and buildplate_name is not None:
buildplate_node = nozzle_node.getChildNode(buildplate_name)
nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]
# Fallback mechanism of finding materials:
# 1. buildplate-specific material
# 2. nozzle-specific material
# 3. machine-specific material
# 4. generic material (for fdmprinter)
machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", [])
material_id_metadata_dict = dict() # type: Dict[str, MaterialNode]
excluded_materials = set()
for current_node in nodes_to_check:
if current_node is None:
continue
# Only exclude the materials that are explicitly specified in the "exclude_materials" field.
# Do not exclude other materials that are of the same type.
for material_id, node in current_node.material_map.items():
if material_id in machine_exclude_materials:
excluded_materials.add(material_id)
continue
if material_id not in material_id_metadata_dict:
material_id_metadata_dict[material_id] = node
if excluded_materials:
Logger.log("d", "Exclude materials {excluded_materials} for machine {machine_definition_id}".format(excluded_materials = ", ".join(excluded_materials), machine_definition_id = machine_definition_id))
return material_id_metadata_dict
## Gives a dictionary of all root material IDs and their associated
# MaterialNodes from the ContainerTree that are available for the given
# printer and variant.
def getAvailableMaterials(self, definition_id: str, nozzle_name: Optional[str]) -> Dict[str, MaterialNode]:
return ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials
#
# 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[str, MaterialNode]]:
buildplate_name = machine.getBuildplateName()
extruder_stack: "ExtruderStack") -> Dict[str, MaterialNode]:
nozzle_name = None
if extruder_stack.variant.getId() != "empty_variant":
nozzle_name = extruder_stack.variant.getName()
diameter = extruder_stack.getApproximateMaterialDiameter()
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter)
materials = self.getAvailableMaterials(machine.definition.getId(), nozzle_name)
compatible_material_diameter = extruder_stack.getApproximateMaterialDiameter()
result = {key: material for key, material in materials.items() if material.container and float(material.container.getMetaDataEntry("approximate_diameter")) == compatible_material_diameter}
return result
#
# Gets MaterialNode for the given extruder and machine with the given material name.
@ -384,41 +143,22 @@ class MaterialManager(QObject):
#
def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str],
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:
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]",
diameter, rounded_diameter, root_material_id)
container_tree = ContainerTree.getInstance()
machine_node = container_tree.machines.get(machine_definition_id)
if machine_node is None:
Logger.log("w", "Could not find machine with definition %s in the container tree", machine_definition_id)
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] # type: Dict[str, MaterialNode]
machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
nozzle_node = None
buildplate_node = None
variant_node = machine_node.variants.get(nozzle_name)
if variant_node is None:
Logger.log("w", "Could not find variant %s for machine with definition %s in the container tree", nozzle_name, machine_definition_id )
return None
# Fallback for "fdmprinter" if the machine-specific materials cannot be found
if machine_node is None:
machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
if machine_node is not None and nozzle_name is not None:
nozzle_node = machine_node.getChildNode(nozzle_name)
if nozzle_node is not None and buildplate_name is not None:
buildplate_node = nozzle_node.getChildNode(buildplate_name)
material_node = variant_node.materials.get(root_material_id)
# Fallback mechanism of finding materials:
# 1. buildplate-specific material
# 2. nozzle-specific material
# 3. machine-specific material
# 4. generic material (for fdmprinter)
nodes_to_check = [buildplate_node, nozzle_node, machine_node,
machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)]
material_node = None
for node in nodes_to_check:
if node is not None:
material_node = node.material_map.get(root_material_id)
if material_node:
break
if material_node is None:
Logger.log("w", "Could not find material %s for machine with definition %s and variant %s in the container tree", root_material_id, machine_definition_id, nozzle_name)
return None
return material_node
@ -430,27 +170,12 @@ class MaterialManager(QObject):
#
def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str,
buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]:
node = None
machine_definition = global_stack.definition
extruder_definition = global_stack.extruders[position].definition
if parseBool(machine_definition.getMetaDataEntry("has_materials", False)):
material_diameter = extruder_definition.getProperty("material_diameter", "value")
if isinstance(material_diameter, SettingFunction):
material_diameter = material_diameter(global_stack)
extruder = global_stack.extruderList[int(position)]
variant_name = extruder.variant.getName()
approximate_diameter = extruder.getApproximateMaterialDiameter()
# 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 = cast(str, material_group.root_material_node.getMetaDataEntry("id", ""))
break
if not root_material_id:
Logger.log("i", "Cannot find materials with guid [%s] ", material_guid)
return None
node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
material_diameter, root_material_id)
return node
return self.getMaterialNode(machine_definition.getId(), variant_name, buildplate_name, approximate_diameter, material_guid)
# There are 2 ways to get fallback materials;
# - A fallback by type (@sa getFallbackMaterialIdByMaterialType), which adds the generic version of this material
@ -475,7 +200,7 @@ class MaterialManager(QObject):
return results
#
# Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
# Built-in quality profiles may be based on generic material IDs such as "generic_pla".
# For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
# the generic material IDs to search for qualities.
#
@ -501,200 +226,101 @@ class MaterialManager(QObject):
## Get default material for given global stack, extruder position and extruder nozzle name
# you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder)
def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, nozzle_name: Optional[str],
extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]:
node = None
buildplate_name = global_stack.getBuildplateName()
machine_definition = global_stack.definition
# The extruder-compatible material diameter in the extruder definition may not be the correct value because
# the user can change it in the definition_changes container.
if extruder_definition is None:
extruder_stack_or_definition = global_stack.extruders[position]
is_extruder_stack = True
extruder_definition: Optional["DefinitionContainer"] = None) -> "MaterialNode":
definition_id = global_stack.definition.getId()
machine_node = ContainerTree.getInstance().machines[definition_id]
if nozzle_name in machine_node.variants:
nozzle_node = machine_node.variants[nozzle_name]
else:
extruder_stack_or_definition = extruder_definition
is_extruder_stack = False
Logger.log("w", "Could not find variant {nozzle_name} for machine with definition {definition_id} in the container tree".format(nozzle_name = nozzle_name, definition_id = definition_id))
nozzle_node = next(iter(machine_node.variants))
if extruder_stack_or_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
if is_extruder_stack:
material_diameter = extruder_stack_or_definition.getCompatibleMaterialDiameter()
else:
material_diameter = extruder_stack_or_definition.getProperty("material_diameter", "value")
if not parseBool(global_stack.getMetaDataEntry("has_materials", False)):
return next(iter(nozzle_node.materials))
if isinstance(material_diameter, SettingFunction):
material_diameter = material_diameter(global_stack)
approximate_material_diameter = str(round(material_diameter))
root_material_id = machine_definition.getMetaDataEntry("preferred_material")
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter)
node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
material_diameter, root_material_id)
return node
if extruder_definition is not None:
material_diameter = extruder_definition.getProperty("material_diameter", "value")
else:
material_diameter = global_stack.extruders[position].getCompatibleMaterialDiameter()
approximate_material_diameter = round(material_diameter)
return nozzle_node.preferredMaterial(approximate_material_diameter)
def removeMaterialByRootId(self, root_material_id: str):
material_group = self.getMaterialGroup(root_material_id)
if not material_group:
Logger.log("i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id)
return
container_registry = CuraContainerRegistry.getInstance()
results = container_registry.findContainers(id = root_material_id)
if not results:
container_registry.addWrongContainerId(root_material_id)
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
# Sort all nodes with respect to the container ID lengths in the ascending order so the base material container
# will be the first one to be removed. We need to do this to ensure that all containers get loaded & deleted.
nodes_to_remove = sorted(nodes_to_remove, key = lambda x: len(x.getMetaDataEntry("id", "")))
# Try to load all containers first. If there is any faulty ones, they will be put into the faulty container
# list, so removeContainer() can ignore those ones.
for node in nodes_to_remove:
container_id = node.getMetaDataEntry("id", "")
results = self._container_registry.findContainers(id = container_id)
if not results:
self._container_registry.addWrongContainerId(container_id)
for node in nodes_to_remove:
self._container_registry.removeContainer(node.getMetaDataEntry("id", ""))
for result in results:
container_registry.removeContainer(result.getMetaDataEntry("id", ""))
#
# Methods for GUI
#
@pyqtSlot("QVariant", result=bool)
@pyqtSlot("QVariant", result = bool)
def canMaterialBeRemoved(self, material_node: "MaterialNode"):
# Check if the material is active in any extruder train. In that case, the material shouldn't be removed!
# In the future we might enable this again, but right now, it's causing a ton of issues if we do (since it
# corrupts the configuration)
root_material_id = material_node.getMetaDataEntry("base_file")
material_group = self.getMaterialGroup(root_material_id)
if not material_group:
return False
root_material_id = material_node.base_file
ids_to_remove = {metadata.get("id", "") for metadata in CuraContainerRegistry.getInstance().findInstanceContainersMetadata(base_file = root_material_id)}
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
ids_to_remove = [node.getMetaDataEntry("id", "") for node in nodes_to_remove]
for extruder_stack in self._container_registry.findContainerStacks(type="extruder_train"):
for extruder_stack in CuraContainerRegistry.getInstance().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:
root_material_id = material_node.getMetaDataEntry("base_file")
if root_material_id is None:
return
if self._container_registry.isReadOnly(root_material_id):
Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
return
material_group = self.getMaterialGroup(root_material_id)
if material_group:
container = material_group.root_material_node.getContainer()
if container:
container.setName(name)
return cura.CuraApplication.CuraApplication.getMaterialManagementModel().setMaterialName(material_node, name)
## Deletes a material from Cura.
#
# Removes the given material.
#
# 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:
root_material_id = material_node.getMetaDataEntry("base_file")
if root_material_id is not None:
self.removeMaterialByRootId(root_material_id)
return cura.CuraApplication.CuraApplication.getMaterialManagementModel().setMaterialName(material_node)
#
# Creates a duplicate of a material, which has the same GUID and base_file metadata.
# Returns the root material ID of the duplicated material if successful.
#
def duplicateMaterialByRootId(self, root_material_id: str, new_base_id: Optional[str] = None, new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]:
result = cura.CuraApplication.CuraApplication.getInstance().getMaterialManagementModel().duplicateMaterialByBaseFile(root_material_id, new_base_id, new_metadata)
if result is None:
return "ERROR"
return result
## 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]:
root_material_id = cast(str, material_node.getMetaDataEntry("base_file", ""))
material_group = self.getMaterialGroup(root_material_id)
if not material_group:
Logger.log("i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id)
return None
base_container = material_group.root_material_node.getContainer()
if not base_container:
return None
# Ensure all settings are saved.
self._application.saveSettings()
# Create a new ID & container to hold the data.
new_containers = []
if new_base_id is None:
new_base_id = self._container_registry.uniqueName(base_container.getId())
new_base_container = copy.deepcopy(base_container)
new_base_container.getMetaData()["id"] = new_base_id
new_base_container.getMetaData()["base_file"] = new_base_id
if new_metadata is not None:
for key, value in new_metadata.items():
new_base_container.getMetaData()[key] = value
new_containers.append(new_base_container)
# Clone all of them.
for node in material_group.derived_material_node_list:
container_to_copy = node.getContainer()
if not container_to_copy:
continue
# Create unique IDs for every clone.
new_id = new_base_id
if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
new_id += "_" + container_to_copy.getMetaDataEntry("definition")
if container_to_copy.getMetaDataEntry("variant_name"):
nozzle_name = container_to_copy.getMetaDataEntry("variant_name")
new_id += "_" + nozzle_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:
for key, value in new_metadata.items():
new_container.getMetaData()[key] = value
new_containers.append(new_container)
for container_to_add in new_containers:
container_to_add.setDirty(True)
self._container_registry.addContainer(container_to_add)
# if the duplicated material was favorite then the new material should also be added to favorite.
if root_material_id in self.getFavorites():
self.addFavorite(new_base_id)
return new_base_id
def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Optional[Dict[str, Any]] = None) -> str:
result = cura.CuraApplication.CuraApplication.getInstance().getMaterialManagementModel().duplicateMaterial(material_node, new_base_id, new_metadata)
if result is None:
return "ERROR"
return result
## Create a new material by cloning the preferred material for the current
# material diameter and generate a new GUID.
#
# 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.
# 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:
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
# Ensure all settings are saved.
self._application.saveSettings()
machine_manager = self._application.getMachineManager()
extruder_stack = machine_manager.activeStack
machine_definition = self._application.getGlobalContainerStack().definition
root_material_id = machine_definition.getMetaDataEntry("preferred_material", default = "generic_pla")
approximate_diameter = str(extruder_stack.approximateMaterialDiameter)
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
material_group = self.getMaterialGroup(root_material_id)
if not material_group: # This should never happen
Logger.log("w", "Cannot get the material group of %s.", root_material_id)
return ""
# Create a new ID & container to hold the data.
new_id = self._container_registry.uniqueName("custom_material")
new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
"brand": catalog.i18nc("@label", "Custom"),
"GUID": str(uuid.uuid4()),
}
self.duplicateMaterial(material_group.root_material_node,
new_base_id = new_id,
new_metadata = new_metadata)
return new_id
return cura.CuraApplication.CuraApplication.getMaterialManagementModel().createMaterial()
@pyqtSlot(str)
def addFavorite(self, root_material_id: str) -> None:
@ -702,8 +328,8 @@ class MaterialManager(QObject):
self.materialsUpdated.emit()
# Ensure all settings are saved.
self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
self._application.saveSettings()
cura.CuraApplication.CuraApplication.getInstance().getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
cura.CuraApplication.CuraApplication.getInstance().saveSettings()
@pyqtSlot(str)
def removeFavorite(self, root_material_id: str) -> None:
@ -715,8 +341,8 @@ class MaterialManager(QObject):
self.materialsUpdated.emit()
# Ensure all settings are saved.
self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
self._application.saveSettings()
cura.CuraApplication.CuraApplication.getInstance().getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
cura.CuraApplication.CuraApplication.getInstance().saveSettings()
@pyqtSlot()
def getFavorites(self):

View file

@ -1,25 +1,136 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Dict, Any
from collections import OrderedDict
from .ContainerNode import ContainerNode
from typing import Any, Optional, TYPE_CHECKING
from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import ContainerInterface
from UM.Signal import Signal
from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.QualityNode import QualityNode
import UM.FlameProfiler
if TYPE_CHECKING:
from typing import Dict
from cura.Machines.VariantNode import VariantNode
## Represents a material in the container tree.
#
# A MaterialNode is a node in the material lookup tree/map/table. It contains 2 (extra) fields:
# - material_map: a one-to-one map of "material_root_id" to material_node.
# - children_map: the key-value map for child nodes of this node. This is used in a lookup tree.
#
#
# Its subcontainers are quality profiles.
class MaterialNode(ContainerNode):
__slots__ = ("material_map", "children_map")
def __init__(self, container_id: str, variant: "VariantNode") -> None:
super().__init__(container_id)
self.variant = variant
self.qualities = {} # type: Dict[str, QualityNode] # Mapping container IDs to quality profiles.
self.materialChanged = Signal() # Triggered when the material is removed or its metadata is updated.
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
container_registry = ContainerRegistry.getInstance()
my_metadata = container_registry.findContainersMetadata(id = container_id)[0]
self.base_file = my_metadata["base_file"]
self.material_type = my_metadata["material"]
self.guid = my_metadata["GUID"]
self._loadAll()
container_registry.containerRemoved.connect(self._onRemoved)
container_registry.containerMetaDataChanged.connect(self._onMetadataChanged)
# We overide this as we want to indicate that MaterialNodes can only contain other material nodes.
self.children_map = OrderedDict() # type: OrderedDict[str, "MaterialNode"]
## Finds the preferred quality for this printer with this material and this
# variant loaded.
#
# If the preferred quality is not available, an arbitrary quality is
# returned. If there is a configuration mistake (like a typo in the
# preferred quality) this returns a random available quality. If there are
# no available qualities, this will return the empty quality node.
# \return The node for the preferred quality, or any arbitrary quality if
# there is no match.
def preferredQuality(self) -> QualityNode:
for quality_id, quality_node in self.qualities.items():
if self.variant.machine.preferred_quality_type == quality_node.quality_type:
return quality_node
fallback = next(iter(self.qualities.values())) # Should only happen with empty quality node.
Logger.log("w", "Could not find preferred quality type {preferred_quality_type} for material {material_id} and variant {variant_id}, falling back to {fallback}.".format(
preferred_quality_type = self.variant.machine.preferred_quality_type,
material_id = self.container_id,
variant_id = self.variant.container_id,
fallback = fallback.container_id
))
return fallback
def getChildNode(self, child_key: str) -> Optional["MaterialNode"]:
return self.children_map.get(child_key)
@UM.FlameProfiler.profile
def _loadAll(self) -> None:
container_registry = ContainerRegistry.getInstance()
# Find all quality profiles that fit on this material.
if not self.variant.machine.has_machine_quality: # Need to find the global qualities.
qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter")
elif not self.variant.machine.has_materials:
qualities = container_registry.findInstanceContainersMetadata(type="quality", definition=self.variant.machine.quality_definition)
else:
if self.variant.machine.has_variants:
# Need to find the qualities that specify a material profile with the same material type.
qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.variant.machine.quality_definition, variant = self.variant.variant_name, material = self.container_id) # First try by exact material ID.
else:
qualities = container_registry.findInstanceContainersMetadata(type="quality", definition=self.variant.machine.quality_definition, material=self.container_id)
if not qualities:
my_material_type = self.material_type
if self.variant.machine.has_variants:
qualities_any_material = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.variant.machine.quality_definition, variant = self.variant.variant_name)
else:
qualities_any_material = container_registry.findInstanceContainersMetadata(type="quality", definition = self.variant.machine.quality_definition)
for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", material = my_material_type):
qualities.extend((quality for quality in qualities_any_material if quality.get("material") == material_metadata["id"]))
if not qualities: # No quality profiles found. Go by GUID then.
my_guid = self.guid
for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", guid = my_guid):
qualities.extend((quality for quality in qualities_any_material if quality["material"] == material_metadata["id"]))
if not qualities:
# There are still some machines that should use global profiles in the extruder, so do that now.
# These are mostly older machines that haven't received updates (so single extruder machines without specific qualities
# but that do have materials and profiles specific to that machine)
qualities.extend([quality for quality in qualities_any_material if quality.get("global_quality", "False") != "False"])
for quality in qualities:
quality_id = quality["id"]
if quality_id not in self.qualities:
self.qualities[quality_id] = QualityNode(quality_id, parent = self)
if not self.qualities:
self.qualities["empty_quality"] = QualityNode("empty_quality", parent = self)
## Triggered when any container is removed, but only handles it when the
# container is removed that this node represents.
# \param container The container that was allegedly removed.
def _onRemoved(self, container: ContainerInterface) -> None:
if container.getId() == self.container_id:
# Remove myself from my parent.
if self.base_file in self.variant.materials:
del self.variant.materials[self.base_file]
if not self.variant.materials:
self.variant.materials["empty_material"] = MaterialNode("empty_material", variant = self.variant)
self.materialChanged.emit(self)
## Triggered when any metadata changed in any container, but only handles
# it when the metadata of this node is changed.
# \param container The container whose metadata changed.
# \param kwargs Key-word arguments provided when changing the metadata.
# These are ignored. As far as I know they are never provided to this
# call.
def _onMetadataChanged(self, container: ContainerInterface, **kwargs: Any) -> None:
if container.getId() != self.container_id:
return
new_metadata = container.getMetaData()
old_base_file = self.base_file
if new_metadata["base_file"] != old_base_file:
self.base_file = new_metadata["base_file"]
if old_base_file in self.variant.materials: # Move in parent node.
del self.variant.materials[old_base_file]
self.variant.materials[self.base_file] = self
old_material_type = self.material_type
self.material_type = new_metadata["material"]
old_guid = self.guid
self.guid = new_metadata["GUID"]
if self.base_file != old_base_file or self.material_type != old_material_type or self.guid != old_guid: # List of quality profiles could've changed.
self.qualities = {}
self._loadAll() # Re-load the quality profiles for this node.
self.materialChanged.emit(self)

View file

@ -1,18 +1,21 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Dict, Set
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
from UM.Qt.ListModel import ListModel
import cura.CuraApplication # Imported like this to prevent a circular reference.
from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.MaterialNode import MaterialNode
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
## This is the base model class for GenericMaterialsModel and MaterialBrandsModel.
# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately.
# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
from cura.Machines.MaterialNode import MaterialNode
class BaseMaterialsModel(ListModel):
extruderPositionChanged = pyqtSignal()
@ -24,19 +27,25 @@ class BaseMaterialsModel(ListModel):
self._application = CuraApplication.getInstance()
self._available_materials = {} # type: Dict[str, MaterialNode]
self._favorite_ids = set() # type: Set[str]
# Make these managers available to all material models
self._container_registry = self._application.getInstance().getContainerRegistry()
self._machine_manager = self._application.getMachineManager()
self._material_manager = self._application.getMaterialManager()
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
# Update this model when switching machines, when adding materials or changing their metadata.
self._machine_manager.activeStackChanged.connect(self._update)
# Update this model when list of materials changes
self._material_manager.materialsUpdated.connect(self._update)
ContainerTree.getInstance().materialsChanged.connect(self._materialsListChanged)
self._application.getMaterialManagementModel().favoritesChanged.connect(self._update)
self.addRoleName(Qt.UserRole + 1, "root_material_id")
self.addRoleName(Qt.UserRole + 2, "id")
@ -55,13 +64,6 @@ 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
if global_stack is None:
@ -100,31 +102,57 @@ 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
## Triggered when a list of materials changed somewhere in the container
# tree. This change may trigger an _update() call when the materials
# changed for the configuration that this model is looking for.
def _materialsListChanged(self, material: MaterialNode) -> None:
if self._extruder_stack is None:
return
if material.variant.container_id != self._extruder_stack.variant.getId():
return
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()
## Triggered when the list of favorite materials is changed.
def _favoritesChanged(self, material_base_file: str) -> None:
if material_base_file in self._available_materials:
self._update()
## This is an abstract method that needs to be implemented by the specific
# models themselves.
def _update(self):
pass
self._favorite_ids = set(cura.CuraApplication.CuraApplication.getInstance().getPreferences().getValue("cura/favorite_materials").split(";"))
# Update the available materials (ContainerNode) for the current active machine and extruder setup.
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack.hasMaterials:
return # There are no materials for this machine, so nothing to do.
extruder_stack = global_stack.extruders.get(str(self._extruder_position))
if not extruder_stack:
return
nozzle_name = extruder_stack.variant.getName()
materials = ContainerTree.getInstance().machines[global_stack.definition.getId()].variants[nozzle_name].materials
approximate_material_diameter = extruder_stack.getApproximateMaterialDiameter()
self._available_materials = {key: material for key, material in materials.items() if float(material.container.getMetaDataEntry("approximate_diameter")) == approximate_material_diameter}
## This method is used by all material models in the beginning of the
# _update() method in order to prevent errors. It's the same in all models
# so it's placed here for easy access.
def _canUpdate(self):
global_stack = self._machine_manager.activeMachine
if global_stack is None or not self._enabled:
return False
try:
extruder_stack = global_stack.extruderList[self._extruder_position]
except IndexError:
return False
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
if self._available_materials is None:
extruder_position = str(self._extruder_position)
if extruder_position not in global_stack.extruders:
return False
return True
@ -132,7 +160,10 @@ 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.getMetadata()
metadata_list = CuraContainerRegistry.getInstance().findContainersMetadata(id = container_node.container_id)
if not metadata_list:
return None
metadata = metadata_list[0]
item = {
"root_material_id": root_material_id,
"id": metadata["id"],
@ -153,4 +184,3 @@ class BaseMaterialsModel(ListModel):
"is_favorite": root_material_id in self._favorite_ids
}
return item

View file

@ -1,4 +1,4 @@
# 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 Qt
@ -21,31 +21,9 @@ class BuildPlateModel(ListModel):
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.ContainerNodeRole, "container_node")
self._application = Application.getInstance()
self._variant_manager = self._application._variant_manager
self._machine_manager = self._application.getMachineManager()
self._machine_manager.globalContainerChanged.connect(self._update)
self._update()
def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = self._machine_manager._global_container_stack
if not global_stack:
self.setItems([])
return
has_variants = parseBool(global_stack.getMetaDataEntry("has_variant_buildplates", False))
if not has_variants:
self.setItems([])
return
variant_dict = self._variant_manager.getVariantNodes(global_stack, variant_type = VariantType.BUILD_PLATE)
item_list = []
for name, variant_node in variant_dict.items():
item = {"name": name,
"container_node": variant_node}
item_list.append(item)
self.setItems(item_list)
self.setItems([])
return

View file

@ -1,31 +1,48 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING
from UM.Logger import Logger
import cura.CuraApplication # Imported this way to prevent circular references.
from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
from UM.Settings.Interfaces import ContainerInterface
#
# This model is used for the custom profile items in the profile drop down menu.
#
## This model is used for the custom profile items in the profile drop down
# menu.
class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel):
def _update(self):
def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
container_registry.containerAdded.connect(self._qualityChangesListChanged)
container_registry.containerRemoved.connect(self._qualityChangesListChanged)
container_registry.containerMetaDataChanged.connect(self._qualityChangesListChanged)
def _qualityChangesListChanged(self, container: "ContainerInterface") -> None:
if container.getMetaDataEntry("type") == "quality_changes":
self._update()
def _update(self) -> None:
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
active_global_stack = self._machine_manager.activeMachine
active_global_stack = cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeMachine
if active_global_stack is None:
self.setItems([])
Logger.log("d", "No active GlobalStack, set %s as empty.", self.__class__.__name__)
return
quality_changes_group_dict = self._quality_manager.getQualityChangesGroups(active_global_stack)
quality_changes_list = ContainerTree.getInstance().getCurrentQualityChangesGroups()
item_list = []
for key in sorted(quality_changes_group_dict, key = lambda name: name.upper()):
quality_changes_group = quality_changes_group_dict[key]
for quality_changes_group in sorted(quality_changes_list, key = lambda qgc: qgc.name.lower()):
item = {"name": quality_changes_group.name,
"layer_height": "",
"layer_height_without_unit": "",

View file

@ -1,28 +1,33 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
import cura.CuraApplication # To listen to changes to the preferences.
## Model that shows the list of favorite materials.
class FavoriteMaterialsModel(BaseMaterialsModel):
def __init__(self, parent = None):
super().__init__(parent)
cura.CuraApplication.CuraApplication.getInstance().getPreferences().preferenceChanged.connect(self._onFavoritesChanged)
self._update()
## Triggered when any preference changes, but only handles it when the list
# of favourites is changed.
def _onFavoritesChanged(self, preference_key: str) -> None:
if preference_key != "cura/favorite_materials":
return
self._update()
def _update(self):
if not self._canUpdate():
return
# Get updated list of favorites
self._favorite_ids = self._material_manager.getFavorites()
super()._update()
item_list = []
for root_material_id, container_node in self._available_materials.items():
metadata = container_node.getMetadata()
# Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)):
if bool(container_node.container.getMetaDataEntry("removed", False)):
continue
# Only add results for favorite materials
@ -30,7 +35,8 @@ class FavoriteMaterialsModel(BaseMaterialsModel):
continue
item = self._createMaterialItem(root_material_id, container_node)
item_list.append(item)
if item:
item_list.append(item)
# Sort the item list alphabetically by name
item_list = sorted(item_list, key = lambda d: d["brand"].upper())

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
@ -12,25 +12,22 @@ class GenericMaterialsModel(BaseMaterialsModel):
def _update(self):
if not self._canUpdate():
return
# Get updated list of favorites
self._favorite_ids = self._material_manager.getFavorites()
super()._update()
item_list = []
for root_material_id, container_node in self._available_materials.items():
metadata = container_node.getMetadata()
# Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)):
if bool(container_node.container.getMetaDataEntry("removed", False)):
continue
# Only add results for generic materials
if metadata["brand"].lower() != "generic":
if container_node.container.getMetaDataEntry("brand", "unknown").lower() != "generic":
continue
item = self._createMaterialItem(root_material_id, container_node)
item_list.append(item)
if item:
item_list.append(item)
# Sort the item list alphabetically by name
item_list = sorted(item_list, key = lambda d: d["name"].upper())

View file

@ -0,0 +1,74 @@
#Copyright (c) 2019 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt
import collections
from typing import TYPE_CHECKING
from cura.Machines.Models.IntentModel import IntentModel
from cura.Settings.IntentManager import IntentManager
from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerRegistry import ContainerRegistry #To update the list if anything changes.
from PyQt5.QtCore import pyqtProperty, pyqtSignal
import cura.CuraApplication
if TYPE_CHECKING:
from UM.Settings.ContainerRegistry import ContainerInterface
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
## Lists the intent categories that are available for the current printer
# configuration.
class IntentCategoryModel(ListModel):
NameRole = Qt.UserRole + 1
IntentCategoryRole = Qt.UserRole + 2
WeightRole = Qt.UserRole + 3
QualitiesRole = Qt.UserRole + 4
#Translations to user-visible string. Ordered by weight.
#TODO: Create a solution for this name and weight to be used dynamically.
name_translation = collections.OrderedDict() #type: "collections.OrderedDict[str,str]"
name_translation["default"] = catalog.i18nc("@label", "Default")
name_translation["engineering"] = catalog.i18nc("@label", "Engineering")
name_translation["smooth"] = catalog.i18nc("@label", "Smooth")
modelUpdated = pyqtSignal()
## Creates a new model for a certain intent category.
# \param The category to list the intent profiles for.
def __init__(self, intent_category: str) -> None:
super().__init__()
self._intent_category = intent_category
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.IntentCategoryRole, "intent_category")
self.addRoleName(self.WeightRole, "weight")
self.addRoleName(self.QualitiesRole, "qualities")
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChange)
ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChange)
cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeStackChanged.connect(self.update)
self.update()
## Updates the list of intents if an intent profile was added or removed.
def _onContainerChange(self, container: "ContainerInterface") -> None:
if container.getMetaDataEntry("type") == "intent":
self.update()
## Updates the list of intents.
def update(self) -> None:
available_categories = IntentManager.getInstance().currentAvailableIntentCategories()
result = []
for category in available_categories:
qualities = IntentModel()
qualities.setIntentCategory(category)
result.append({
"name": self.name_translation.get(category, catalog.i18nc("@label", "Unknown")),
"intent_category": category,
"weight": list(self.name_translation.keys()).index(category),
"qualities": qualities
})
result.sort(key = lambda k: k["weight"])
self.setItems(result)

View file

@ -0,0 +1,131 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Dict, Any, Set, List
from PyQt5.QtCore import Qt, QObject, pyqtProperty, pyqtSignal
import cura.CuraApplication
from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.MaterialNode import MaterialNode
from cura.Machines.Models.MachineModelUtils import fetchLayerHeight
from cura.Machines.QualityGroup import QualityGroup
class IntentModel(ListModel):
NameRole = Qt.UserRole + 1
QualityTypeRole = Qt.UserRole + 2
LayerHeightRole = Qt.UserRole + 3
AvailableRole = Qt.UserRole + 4
IntentRole = Qt.UserRole + 5
def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.QualityTypeRole, "quality_type")
self.addRoleName(self.LayerHeightRole, "layer_height")
self.addRoleName(self.AvailableRole, "available")
self.addRoleName(self.IntentRole, "intent_category")
self._intent_category = "engineering"
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
machine_manager.globalContainerChanged.connect(self._update)
machine_manager.extruderChanged.connect(self._update) # We also need to update if an extruder gets disabled
ContainerRegistry.getInstance().containerAdded.connect(self._onChanged)
ContainerRegistry.getInstance().containerRemoved.connect(self._onChanged)
self._layer_height_unit = "" # This is cached
self._update()
intentCategoryChanged = pyqtSignal()
def setIntentCategory(self, new_category: str) -> None:
if self._intent_category != new_category:
self._intent_category = new_category
self.intentCategoryChanged.emit()
self._update()
@pyqtProperty(str, fset = setIntentCategory, notify = intentCategoryChanged)
def intentCategory(self) -> str:
return self._intent_category
def _onChanged(self, container):
if container.getMetaDataEntry("type") == "intent":
self._update()
def _update(self) -> None:
new_items = [] # type: List[Dict[str, Any]]
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack:
self.setItems(new_items)
return
quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
material_nodes = self._getActiveMaterials()
added_quality_type_set = set() # type: Set[str]
for material_node in material_nodes:
intents = self._getIntentsForMaterial(material_node, quality_groups)
for intent in intents:
if intent["quality_type"] not in added_quality_type_set:
new_items.append(intent)
added_quality_type_set.add(intent["quality_type"])
# Now that we added all intents that we found something for, ensure that we set add ticks (and layer_heights)
# for all groups that we don't have anything for (and set it to not available)
for quality_type, quality_group in quality_groups.items():
# Add the intents that are of the correct category
if quality_type not in added_quality_type_set:
layer_height = fetchLayerHeight(quality_group)
new_items.append({"name": "Unavailable",
"quality_type": quality_type,
"layer_height": layer_height,
"intent_category": self._intent_category,
"available": False})
added_quality_type_set.add(quality_type)
new_items = sorted(new_items, key = lambda x: x["layer_height"])
self.setItems(new_items)
## Get the active materials for all extruders. No duplicates will be returned
def _getActiveMaterials(self) -> Set["MaterialNode"]:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return set()
container_tree = ContainerTree.getInstance()
machine_node = container_tree.machines[global_stack.definition.getId()]
nodes = set() # type: Set[MaterialNode]
for extruder in global_stack.extruderList:
active_variant_name = extruder.variant.getMetaDataEntry("name")
active_variant_node = machine_node.variants[active_variant_name]
active_material_node = active_variant_node.materials[extruder.material.getMetaDataEntry("base_file")]
nodes.add(active_material_node)
return nodes
def _getIntentsForMaterial(self, active_material_node: "MaterialNode", quality_groups: Dict[str, "QualityGroup"]) -> List[Dict[str, Any]]:
extruder_intents = [] # type: List[Dict[str, Any]]
for quality_id, quality_node in active_material_node.qualities.items():
if quality_node.quality_type not in quality_groups: # Don't add the empty quality type (or anything else that would crash, defensively).
continue
quality_group = quality_groups[quality_node.quality_type]
layer_height = fetchLayerHeight(quality_group)
for intent_id, intent_node in quality_node.intents.items():
if intent_node.intent_category != self._intent_category:
continue
extruder_intents.append({"name": quality_group.name,
"quality_type": quality_group.quality_type,
"layer_height": layer_height,
"available": quality_group.is_available,
"intent_category": self._intent_category
})
return extruder_intents
def __repr__(self):
return str(self.items)

View file

@ -0,0 +1,37 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING
from UM.Settings.SettingFunction import SettingFunction
if TYPE_CHECKING:
from cura.Machines.QualityGroup import QualityGroup
layer_height_unit = ""
def fetchLayerHeight(quality_group: "QualityGroup") -> float:
from cura.CuraApplication import CuraApplication
global_stack = CuraApplication.getInstance().getMachineManager().activeMachine
default_layer_height = global_stack.definition.getProperty("layer_height", "value")
# Get layer_height from the quality profile for the GlobalStack
if quality_group.node_for_global is None:
return float(default_layer_height)
container = quality_group.node_for_global.container
layer_height = default_layer_height
if container and container.hasProperty("layer_height", "value"):
layer_height = container.getProperty("layer_height", "value")
else:
# Look for layer_height in the GlobalStack from material -> definition
container = global_stack.definition
if container and container.hasProperty("layer_height", "value"):
layer_height = container.getProperty("layer_height", "value")
if isinstance(layer_height, SettingFunction):
layer_height = layer_height(global_stack)
return float(layer_height)

View file

@ -1,4 +1,4 @@
# 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 Qt, pyqtSignal
@ -29,8 +29,7 @@ class MaterialBrandsModel(BaseMaterialsModel):
def _update(self):
if not self._canUpdate():
return
# Get updated list of favorites
self._favorite_ids = self._material_manager.getFavorites()
super()._update()
brand_item_list = []
brand_group_dict = {}
@ -38,24 +37,25 @@ 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():
# Do not include the materials from a to-be-removed package
if bool(container_node.getMetaDataEntry("removed", False)):
if bool(container_node.container.getMetaDataEntry("removed", False)):
continue
# Add brands we haven't seen yet to the dict, skipping generics
brand = container_node.getMetaDataEntry("brand", "")
brand = container_node.container.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 = container_node.getMetaDataEntry("material", "")
material_type = container_node.container.getMetaDataEntry("material", "")
if material_type not in brand_group_dict[brand]:
brand_group_dict[brand][material_type] = []
# Now handle the individual materials
item = self._createMaterialItem(root_material_id, container_node)
brand_group_dict[brand][material_type].append(item)
if item:
brand_group_dict[brand][material_type].append(item)
# Part 2: Organize the tree into models
#

View file

@ -0,0 +1,215 @@
# 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, pyqtSignal, 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):
## Triggered when a favorite is added or removed.
# \param The base file of the material is provided as parameter when this
# emits.
favoritesChanged = pyqtSignal(str)
## 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") -> bool:
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 base_file: The base file of 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.
def duplicateMaterialByBaseFile(self, base_file: str, new_base_id: Optional[str] = None,
new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]:
container_registry = CuraContainerRegistry.getInstance()
root_materials = container_registry.findContainers(id = 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 = 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 = base_file):
if container_to_copy.getId() == 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 base_file in favorites_set:
favorites_set.add(new_base_id)
application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites_set))
return new_base_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: Optional[Dict[str, Any]] = None) -> Optional[str]:
return self.duplicateMaterialByBaseFile(material_node.base_file, new_base_id, new_metadata)
## 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
## Adds a certain material to the favorite materials.
# \param material_base_file The base file of the material to add.
@pyqtSlot(str)
def addFavorite(self, material_base_file: str) -> None:
application = cura.CuraApplication.CuraApplication.getInstance()
favorites = application.getPreferences().getValue("cura/favorite_materials").split(";")
if material_base_file not in favorites:
favorites.append(material_base_file)
application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites))
application.saveSettings()
self.favoritesChanged.emit(material_base_file)
## Removes a certain material from the favorite materials.
#
# If the material was not in the favorite materials, nothing happens.
@pyqtSlot(str)
def removeFavorite(self, material_base_file: str) -> None:
application = cura.CuraApplication.CuraApplication.getInstance()
favorites = application.getPreferences().getValue("cura/favorite_materials").split(";")
try:
favorites.remove(material_base_file)
application.getPreferences().setValue("cura/favorite_materials", ";".join(favorites))
application.saveSettings()
self.favoritesChanged.emit(material_base_file)
except ValueError: # Material was not in the favorites list.
Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file))

View file

@ -1,4 +1,4 @@
# 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 Qt
@ -6,9 +6,7 @@ from PyQt5.QtCore import Qt
from UM.Application import Application
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.Util import parseBool
from cura.Machines.VariantType import VariantType
from cura.Machines.ContainerTree import ContainerTree
class NozzleModel(ListModel):
@ -25,7 +23,6 @@ class NozzleModel(ListModel):
self._application = Application.getInstance()
self._machine_manager = self._application.getMachineManager()
self._variant_manager = self._application.getVariantManager()
self._machine_manager.globalContainerChanged.connect(self._update)
self._update()
@ -37,19 +34,14 @@ class NozzleModel(ListModel):
if global_stack is None:
self.setItems([])
return
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", False))
if not has_variants:
self.setItems([])
return
variant_node_dict = self._variant_manager.getVariantNodes(global_stack, VariantType.NOZZLE)
if not variant_node_dict:
if not machine_node.has_variants:
self.setItems([])
return
item_list = []
for hotend_name, container_node in sorted(variant_node_dict.items(), key = lambda i: i[0].upper()):
for hotend_name, container_node in sorted(machine_node.variants.items(), key = lambda i: i[0].upper()):
item = {"id": hotend_name,
"hotend_name": hotend_name,
"container_node": container_node
@ -57,4 +49,4 @@ class NozzleModel(ListModel):
item_list.append(item)
self.setItems(item_list)
self.setItems(item_list)

View file

@ -1,10 +1,28 @@
# 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 Qt, pyqtSlot
from typing import Any, cast, Dict, Optional, TYPE_CHECKING
from PyQt5.QtCore import pyqtSlot, QObject, Qt
from UM.Qt.ListModel import ListModel
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 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
#
# This the QML model for the quality management page.
@ -13,27 +31,245 @@ class QualityManagementModel(ListModel):
NameRole = Qt.UserRole + 1
IsReadOnlyRole = Qt.UserRole + 2
QualityGroupRole = Qt.UserRole + 3
QualityChangesGroupRole = Qt.UserRole + 4
QualityTypeRole = Qt.UserRole + 4
QualityChangesGroupRole = Qt.UserRole + 5
IntentCategoryRole = Qt.UserRole + 6
SectionNameRole = Qt.UserRole + 7
def __init__(self, parent = None):
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")
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()
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
## 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)
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:
# Create global quality changes only.
new_quality_changes = self._createQualityChanges(quality_group.quality_type, intent_category, new_name,
global_stack, extruder_stack = None)
container_registry.addContainer(new_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))
## 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.
@pyqtSlot(str)
def createQualityChanges(self, base_name: str) -> None:
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] + list(global_stack.extruders.values())
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)
## 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.
def _createQualityChanges(self, quality_type: str, intent_category: Optional[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 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
## 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()
@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__))
@ -42,18 +278,19 @@ class QualityManagementModel(ListModel):
self.setItems([])
return
quality_group_dict = self._quality_manager.getQualityGroups(global_stack)
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
item_list = []
# Create quality group items
# Create quality group items (intent category = "default")
for quality_group in quality_group_dict.values():
if not quality_group.is_available:
continue
@ -61,19 +298,45 @@ class QualityManagementModel(ListModel):
item = {"name": quality_group.name,
"is_read_only": True,
"quality_group": quality_group,
"quality_changes_group": None}
"quality_type": quality_group.quality_type,
"quality_changes_group": None,
"intent_category": "default",
"section_name": catalog.i18nc("@label", "Default"),
}
item_list.append(item)
# Sort by quality names
item_list = sorted(item_list, key = lambda x: x["name"].upper())
# 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:
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_category.capitalize()),
})
# Sort by quality_type for each intent category
result = sorted(result, key = lambda x: (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_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,
"quality_group": quality_group,
"quality_changes_group": quality_changes_group}
"quality_type": quality_group.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

View file

@ -1,14 +1,13 @@
# 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 Qt, QTimer
from UM.Application import Application
import cura.CuraApplication # Imported this way to prevent circular dependencies.
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.Settings.SettingFunction import SettingFunction
from cura.Machines.QualityManager import QualityGroup
from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.Models.MachineModelUtils import fetchLayerHeight
#
@ -36,14 +35,13 @@ class QualityProfilesDropDownMenuModel(ListModel):
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
self.addRoleName(self.IsExperimentalRole, "is_experimental")
self._application = Application.getInstance()
self._machine_manager = self._application.getMachineManager()
self._quality_manager = Application.getInstance().getQualityManager()
application = cura.CuraApplication.CuraApplication.getInstance()
machine_manager = application.getMachineManager()
self._application.globalContainerStackChanged.connect(self._onChange)
self._machine_manager.activeQualityGroupChanged.connect(self._onChange)
self._machine_manager.extruderChanged.connect(self._onChange)
self._quality_manager.qualitiesUpdated.connect(self._onChange)
application.globalContainerStackChanged.connect(self._onChange)
machine_manager.activeQualityGroupChanged.connect(self._onChange)
machine_manager.activeStackChanged.connect(self._onChange)
machine_manager.extruderChanged.connect(self._onChange)
self._layer_height_unit = "" # This is cached
@ -60,25 +58,36 @@ class QualityProfilesDropDownMenuModel(ListModel):
def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = self._machine_manager.activeMachine
# CURA-6836
# LabelBar is a repeater that creates labels for quality layer heights. Because of an optimization in
# UM.ListModel, the model will not remove all items and recreate new ones every time there's an update.
# Because LabelBar uses Repeater with Labels anchoring to "undefined" in certain cases, the anchoring will be
# kept the same as before.
self.setItems([])
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
self.setItems([])
Logger.log("d", "No active GlobalStack, set quality profile model as empty.")
return
if not self._layer_height_unit:
unit = global_stack.definition.getProperty("layer_height", "unit")
if not unit:
unit = ""
self._layer_height_unit = unit
# Check for material compatibility
if not self._machine_manager.activeMaterialsCompatible():
if not cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeMaterialsCompatible():
Logger.log("d", "No active material compatibility, set quality profile model as empty.")
self.setItems([])
return
quality_group_dict = self._quality_manager.getQualityGroups(global_stack)
quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups()
item_list = []
for key in sorted(quality_group_dict):
quality_group = quality_group_dict[key]
layer_height = self._fetchLayerHeight(quality_group)
for quality_group in quality_group_dict.values():
layer_height = fetchLayerHeight(quality_group)
item = {"name": quality_group.name,
"quality_type": quality_group.quality_type,
@ -94,32 +103,3 @@ class QualityProfilesDropDownMenuModel(ListModel):
item_list = sorted(item_list, key = lambda x: x["layer_height"])
self.setItems(item_list)
def _fetchLayerHeight(self, quality_group: "QualityGroup") -> float:
global_stack = self._machine_manager.activeMachine
if not self._layer_height_unit:
unit = global_stack.definition.getProperty("layer_height", "unit")
if not unit:
unit = ""
self._layer_height_unit = unit
default_layer_height = global_stack.definition.getProperty("layer_height", "value")
# Get layer_height from the quality profile for the GlobalStack
if quality_group.node_for_global is None:
return float(default_layer_height)
container = quality_group.node_for_global.getContainer()
layer_height = default_layer_height
if container and container.hasProperty("layer_height", "value"):
layer_height = container.getProperty("layer_height", "value")
else:
# Look for layer_height in the GlobalStack from material -> definition
container = global_stack.definition
if container and container.hasProperty("layer_height", "value"):
layer_height = container.getProperty("layer_height", "value")
if isinstance(layer_height, SettingFunction):
layer_height = layer_height(global_stack)
return float(layer_height)

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.getContainer() is not None: # it can be None if number of extruders are changed during runtime
quality_containers.insert(0, quality_changes_node.getContainer())
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 = ""

View file

@ -1,33 +1,37 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING
from typing import Any, Dict, Optional
from UM.Application import Application
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from .QualityGroup import QualityGroup
if TYPE_CHECKING:
from cura.Machines.QualityNode import QualityNode
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
class QualityChangesGroup(QualityGroup):
def __init__(self, name: str, quality_type: str, parent = None) -> None:
super().__init__(name, quality_type, parent)
self._container_registry = Application.getInstance().getContainerRegistry()
## Data struct to group several quality changes instance containers together.
#
# Each group represents one "custom profile" as the user sees it, which
# contains an instance container for the global stack and one instance
# container per extruder.
class QualityChangesGroup(QObject):
def addNode(self, node: "QualityNode") -> None:
extruder_position = node.getMetaDataEntry("position")
def __init__(self, name: str, quality_type: str, intent_category: str, parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._name = name
self.quality_type = quality_type
self.intent_category = intent_category
self.is_available = False
self.metadata_for_global = {} # type: Dict[str, Any]
self.metadata_per_extruder = {} # type: Dict[int, Dict[str, Any]]
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.
ConfigurationErrorMessage.getInstance().addFaultyContainers(node.getMetaDataEntry("id"))
return
nameChanged = pyqtSignal()
if extruder_position is None: # Then we're a global quality changes profile.
self.node_for_global = node
else: # This is an extruder's quality changes profile.
self.nodes_for_extruders[extruder_position] = node
def setName(self, name: str) -> None:
if self._name != name:
self._name = name
self.nameChanged.emit()
@pyqtProperty(str, fset = setName, notify = nameChanged)
def name(self) -> str:
return self._name
def __str__(self) -> str:
return "%s[<%s>, available = %s]" % (self.__class__.__name__, self.name, self.is_available)
return "{class_name}[{name}, available = {is_available}]".format(class_name = self.__class__.__name__, name = self.name, is_available = self.is_available)

View file

@ -1,32 +1,38 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Dict, Optional, List, Set
from PyQt5.QtCore import QObject, pyqtSlot
from UM.Logger import Logger
from UM.Util import parseBool
from cura.Machines.ContainerNode import ContainerNode
## A QualityGroup represents a group of quality containers that must be applied
# to each ContainerStack when it's used.
#
# 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
# must be applied to all stacks in a machine, although each stack can have different containers. Use an Ultimaker 3
# as an example, suppose we choose quality type "normal", the actual InstanceContainers on each stack may look
# as below:
# GlobalStack ExtruderStack 1 ExtruderStack 2
# quality container: um3_global_normal um3_aa04_pla_normal um3_aa04_abs_normal
# A concrete example: When there are two extruders and the user selects the
# quality type "normal", this quality type must be applied to all stacks in a
# machine, although each stack can have different containers. So one global
# profile gets put on the global stack and one extruder profile gets put on
# each extruder stack. This quality group then contains the following
# profiles (for instance):
# GlobalStack ExtruderStack 1 ExtruderStack 2
# quality container: um3_global_normal um3_aa04_pla_normal um3_aa04_abs_normal
#
# This QualityGroup is mainly used in quality and quality_changes to group the containers that can be applied to
# a machine, so when a quality/custom quality is selected, the container can be directly applied to each stack instead
# of looking them up again.
#
class QualityGroup(QObject):
def __init__(self, name: str, quality_type: str, parent = None) -> None:
super().__init__(parent)
# The purpose of these quality groups is to group the containers that can be
# applied to a configuration, so that when a quality level is selected, the
# container can directly be applied to each stack instead of looking them up
# again.
class QualityGroup:
## Constructs a new group.
# \param name The user-visible name for the group.
# \param quality_type The quality level that each profile in this group
# has.
def __init__(self, name: str, quality_type: str) -> None:
self.name = name
self.node_for_global = None # type: Optional[ContainerNode]
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]
@ -34,7 +40,6 @@ class QualityGroup(QObject):
self.is_available = False
self.is_experimental = False
@pyqtSlot(result = str)
def getName(self) -> str:
return self.name
@ -43,7 +48,7 @@ class QualityGroup(QObject):
for node in [self.node_for_global] + list(self.nodes_for_extruders.values()):
if node is None:
continue
container = node.getContainer()
container = node.container
if container:
result.update(container.getAllKeys())
return result
@ -60,12 +65,18 @@ class QualityGroup(QObject):
self.node_for_global = node
# Update is_experimental flag
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
if not node.container:
Logger.log("w", "Node {0} doesn't have a container.".format(node.container_id))
return
is_experimental = parseBool(node.container.getMetaDataEntry("is_experimental", False))
self.is_experimental |= is_experimental
def setExtruderNode(self, position: int, node: "ContainerNode") -> None:
self.nodes_for_extruders[position] = node
# Update is_experimental flag
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
if not node.container:
Logger.log("w", "Node {0} doesn't have a container.".format(node.container_id))
return
is_experimental = parseBool(node.container.getMetaDataEntry("is_experimental", False))
self.is_experimental |= is_experimental

View file

@ -1,17 +1,19 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING, Optional, cast, Dict, List, Set
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Logger import Logger
from UM.Util import parseBool
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Decorators import deprecated
import cura.CuraApplication
from cura.Settings.ExtruderStack import ExtruderStack
from cura.Machines.ContainerTree import ContainerTree # The implementation that replaces this manager, to keep the deprecated interface working.
from .QualityChangesGroup import QualityChangesGroup
from .QualityGroup import QualityGroup
from .QualityNode import QualityNode
@ -19,7 +21,6 @@ if TYPE_CHECKING:
from UM.Settings.Interfaces import DefinitionContainerInterface
from cura.Settings.GlobalStack import GlobalStack
from .QualityChangesGroup import QualityChangesGroup
from cura.CuraApplication import CuraApplication
#
@ -33,17 +34,24 @@ if TYPE_CHECKING:
# because it's simple.
#
class QualityManager(QObject):
__instance = None
@classmethod
@deprecated("Use the ContainerTree structure instead.", since = "4.3")
def getInstance(cls) -> "QualityManager":
if cls.__instance is None:
cls.__instance = QualityManager()
return cls.__instance
qualitiesUpdated = pyqtSignal()
def __init__(self, application: "CuraApplication", parent = None) -> None:
def __init__(self, parent = None) -> None:
super().__init__(parent)
self._application = application
self._material_manager = self._application.getMaterialManager()
self._container_registry = self._application.getContainerRegistry()
application = cura.CuraApplication.CuraApplication.getInstance()
self._container_registry = application.getContainerRegistry()
self._empty_quality_container = self._application.empty_quality_container
self._empty_quality_changes_container = self._application.empty_quality_changes_container
self._empty_quality_container = application.empty_quality_container
self._empty_quality_changes_container = application.empty_quality_changes_container
# For quality lookup
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # type: Dict[str, QualityNode]
@ -57,88 +65,6 @@ class QualityManager(QObject):
self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
# When a custom quality gets added/imported, there can be more than one InstanceContainers. In those cases,
# we don't want to react on every container/metadata changed signal. The timer here is to buffer it a bit so
# we don't react too many time.
self._update_timer = QTimer(self)
self._update_timer.setInterval(300)
self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._updateMaps)
def initialize(self) -> None:
# Initialize the lookup tree for quality profiles with following structure:
# <machine> -> <nozzle> -> <buildplate> -> <material>
# <machine> -> <material>
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # for quality lookup
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality")
for metadata in quality_metadata_list:
if metadata["id"] == "empty_quality":
continue
definition_id = metadata["definition"]
quality_type = metadata["quality_type"]
root_material_id = metadata.get("material")
nozzle_name = metadata.get("variant")
buildplate_name = metadata.get("buildplate")
is_global_quality = metadata.get("global_quality", False)
is_global_quality = is_global_quality or (root_material_id is None and nozzle_name is None and buildplate_name is None)
# Sanity check: material+variant and is_global_quality cannot be present at the same time
if is_global_quality and (root_material_id or nozzle_name):
ConfigurationErrorMessage.getInstance().addFaultyContainers(metadata["id"])
continue
if definition_id not in self._machine_nozzle_buildplate_material_quality_type_to_quality_dict:
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id] = QualityNode()
machine_node = cast(QualityNode, self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id])
if is_global_quality:
# For global qualities, save data in the machine node
machine_node.addQualityMetadata(quality_type, metadata)
continue
current_node = machine_node
intermediate_node_info_list = [nozzle_name, buildplate_name, root_material_id]
current_intermediate_node_info_idx = 0
while current_intermediate_node_info_idx < len(intermediate_node_info_list):
node_name = intermediate_node_info_list[current_intermediate_node_info_idx]
if node_name is not None:
# There is specific information, update the current node to go deeper so we can add this quality
# at the most specific branch in the lookup tree.
if node_name not in current_node.children_map:
current_node.children_map[node_name] = QualityNode()
current_node = cast(QualityNode, current_node.children_map[node_name])
current_intermediate_node_info_idx += 1
current_node.addQualityMetadata(quality_type, metadata)
# Initialize the lookup tree for quality_changes profiles with following structure:
# <machine> -> <quality_type> -> <name>
quality_changes_metadata_list = self._container_registry.findContainersMetadata(type = "quality_changes")
for metadata in quality_changes_metadata_list:
if metadata["id"] == "empty_quality_changes":
continue
machine_definition_id = metadata["definition"]
quality_type = metadata["quality_type"]
if machine_definition_id not in self._machine_quality_type_to_quality_changes_dict:
self._machine_quality_type_to_quality_changes_dict[machine_definition_id] = QualityNode()
machine_node = self._machine_quality_type_to_quality_changes_dict[machine_definition_id]
machine_node.addQualityChangesMetadata(quality_type, metadata)
Logger.log("d", "Lookup tables updated.")
self.qualitiesUpdated.emit()
def _updateMaps(self) -> None:
self.initialize()
def _onContainerMetadataChanged(self, container: InstanceContainer) -> None:
self._onContainerChanged(container)
@ -147,211 +73,28 @@ class QualityManager(QObject):
if container_type not in ("quality", "quality_changes"):
return
# update the cache table
self._update_timer.start()
# Updates the given quality groups' availabilities according to which extruders are being used/ enabled.
def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list) -> None:
used_extruders = set()
for i in range(machine.getProperty("machine_extruder_count", "value")):
try:
extruder = machine.extruderList[int(i)]
except IndexError:
pass
else:
if extruder.isEnabled:
used_extruders.add(str(i))
# Update the "is_available" flag for each quality group.
for quality_group in quality_group_list:
is_available = True
if quality_group.node_for_global is None:
is_available = False
if is_available:
for position in used_extruders:
if position not in quality_group.nodes_for_extruders:
is_available = False
break
quality_group.is_available = is_available
# Returns a dict of "custom profile name" -> QualityChangesGroup
def getQualityChangesGroups(self, machine: "GlobalStack") -> dict:
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
machine_node = self._machine_quality_type_to_quality_changes_dict.get(machine_definition_id)
if not machine_node:
Logger.log("i", "Cannot find node for machine def [%s] in QualityChanges lookup table", machine_definition_id)
return dict()
# Update availability for each QualityChangesGroup:
# A custom profile is always available as long as the quality_type it's based on is available
quality_group_dict = self.getQualityGroups(machine)
available_quality_type_list = [qt for qt, qg in quality_group_dict.items() if qg.is_available]
# Iterate over all quality_types in the machine node
quality_changes_group_dict = dict()
for quality_type, quality_changes_node in machine_node.quality_type_map.items():
for quality_changes_name, quality_changes_group in quality_changes_node.children_map.items():
quality_changes_group_dict[quality_changes_name] = quality_changes_group
quality_changes_group.is_available = quality_type in available_quality_type_list
return quality_changes_group_dict
def getQualityChangesGroups(self, machine: "GlobalStack") -> List[QualityChangesGroup]:
variant_names = [extruder.variant.getName() for extruder in machine.extruderList]
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in machine.extruderList]
extruder_enabled = [extruder.isEnabled for extruder in machine.extruderList]
machine_node = ContainerTree.getInstance().machines[machine.definition.getId()]
return machine_node.getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
## Gets the quality groups for the current printer.
#
# Gets all quality groups for the given machine. Both available and none available ones will be included.
# It returns a dictionary with "quality_type"s as keys and "QualityGroup"s as values.
# Whether a QualityGroup is available can be unknown via the field QualityGroup.is_available.
# For more details, see QualityGroup.
#
def getQualityGroups(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
# To find the quality container for the GlobalStack, check in the following fall-back manner:
# (1) the machine-specific node
# (2) the generic node
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
# Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder
# qualities, we should not fall back to use the global qualities.
has_extruder_specific_qualities = False
if machine_node:
if machine_node.children_map:
has_extruder_specific_qualities = True
default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(self._default_machine_definition_id)
nodes_to_check = [] # type: List[QualityNode]
if machine_node is not None:
nodes_to_check.append(machine_node)
if default_machine_node is not None:
nodes_to_check.append(default_machine_node)
# Iterate over all quality_types in the machine node
quality_group_dict = {}
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.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.getMetaDataEntry("name", ""), quality_type)
quality_group.setGlobalNode(quality_node)
quality_group_dict[quality_type] = quality_group
break
buildplate_name = machine.getBuildplateName()
# Iterate over all extruders to find quality containers for each extruder
for position, extruder in machine.extruders.items():
nozzle_name = None
if extruder.variant.getId() != "empty_variant":
nozzle_name = extruder.variant.getName()
# This is a list of root material IDs to use for searching for suitable quality profiles.
# The root material IDs in this list are in prioritized order.
root_material_id_list = []
has_material = False # flag indicating whether this extruder has a material assigned
root_material_id = None
if extruder.material.getId() != "empty_material":
has_material = True
root_material_id = extruder.material.getMetaDataEntry("base_file")
# Convert possible generic_pla_175 -> generic_pla
root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id)
root_material_id_list.append(root_material_id)
# Also try to get the fallback materials
fallback_ids = self._material_manager.getFallBackMaterialIdsByMaterial(extruder.material)
if fallback_ids:
root_material_id_list.extend(fallback_ids)
# Weed out duplicates while preserving the order.
seen = set() # type: Set[str]
root_material_id_list = [x for x in root_material_id_list if x not in seen and not seen.add(x)] # type: ignore
# Here we construct a list of nodes we want to look for qualities with the highest priority first.
# The use case is that, when we look for qualities for a machine, we first want to search in the following
# order:
# 1. machine-nozzle-buildplate-and-material-specific qualities if exist
# 2. machine-nozzle-and-material-specific qualities if exist
# 3. machine-nozzle-specific qualities if exist
# 4. machine-material-specific qualities if exist
# 5. machine-specific global qualities if exist, otherwise generic global qualities
# NOTE: We DO NOT fail back to generic global qualities if machine-specific global qualities exist.
# This is because when a machine defines its own global qualities such as Normal, Fine, etc.,
# it is intended to maintain those specific qualities ONLY. If we still fail back to the generic
# global qualities, there can be unimplemented quality types e.g. "coarse", and this is not
# correct.
# Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into
# the list with priorities as the order. Later, we just need to loop over each node in this list and fetch
# qualities from there.
node_info_list_0 = [nozzle_name, buildplate_name, root_material_id] # type: List[Optional[str]]
nodes_to_check = []
# This function tries to recursively find the deepest (the most specific) branch and add those nodes to
# the search list in the order described above. So, by iterating over that search node list, we first look
# in the more specific branches and then the less specific (generic) ones.
def addNodesToCheck(node: Optional[QualityNode], nodes_to_check_list: List[QualityNode], node_info_list, node_info_idx: int) -> None:
if node is None:
return
if node_info_idx < len(node_info_list):
node_name = node_info_list[node_info_idx]
if node_name is not None:
current_node = node.getChildNode(node_name)
if current_node is not None and has_material:
addNodesToCheck(current_node, nodes_to_check_list, node_info_list, node_info_idx + 1)
if has_material:
for rmid in root_material_id_list:
material_node = node.getChildNode(rmid)
if material_node:
nodes_to_check_list.append(material_node)
break
nodes_to_check_list.append(node)
addNodesToCheck(machine_node, nodes_to_check, node_info_list_0, 0)
# The last fall back will be the global qualities (either from the machine-specific node or the generic
# node), but we only use one. For details see the overview comments above.
if machine_node is not None and machine_node.quality_type_map:
nodes_to_check += [machine_node]
elif default_machine_node is not None:
nodes_to_check += [default_machine_node]
for node_idx, node in enumerate(nodes_to_check):
if node and node.quality_type_map:
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.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.getMetaDataEntry("name", ""), quality_type)
quality_group_dict[quality_type] = quality_group
quality_group = quality_group_dict[quality_type]
if position not in quality_group.nodes_for_extruders:
quality_group.setExtruderNode(position, quality_node)
# If the machine has its own specific qualities, for extruders, it should skip the global qualities
# and use the material/variant specific qualities.
if has_extruder_specific_qualities:
if node_idx == len(nodes_to_check) - 1:
break
# Update availabilities for each quality group
self._updateQualityGroupsAvailability(machine, quality_group_dict.values())
return quality_group_dict
# Both available and unavailable quality groups will be included. Whether
# a quality group is available can be known via the field
# ``QualityGroup.is_available``. For more details, see QualityGroup.
# \return A dictionary with quality types as keys and the quality groups
# for those types as values.
def getQualityGroups(self, global_stack: "GlobalStack") -> Dict[str, QualityGroup]:
# Gather up the variant names and material base files for each extruder.
variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList]
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList]
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
definition_id = global_stack.definition.getId()
return ContainerTree.getInstance().machines[definition_id].getQualityGroups(variant_names, material_bases, extruder_enabled)
def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
@ -367,167 +110,85 @@ class QualityManager(QObject):
# Iterate over all quality_types in the machine node
quality_group_dict = dict()
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.getMetaDataEntry("name", ""), quality_type)
quality_group.setGlobalNode(quality_node)
quality_group_dict[quality_type] = quality_group
break
if node and node.quality_type:
quality_group = QualityGroup(node.getMetaDataEntry("name", ""), node.quality_type)
quality_group.setGlobalNode(node)
quality_group_dict[node.quality_type] = quality_group
return quality_group_dict
## Get the quality group for the preferred quality type for a certain
# global stack.
#
# If the preferred quality type is not available, ``None`` will be
# returned.
# \param machine The global stack of the machine to get the preferred
# quality group for.
# \return The preferred quality group, or ``None`` if that is not
# available.
def getDefaultQualityType(self, machine: "GlobalStack") -> Optional[QualityGroup]:
preferred_quality_type = machine.definition.getMetaDataEntry("preferred_quality_type")
quality_group_dict = self.getQualityGroups(machine)
quality_group = quality_group_dict.get(preferred_quality_type)
return quality_group
machine_node = ContainerTree.getInstance().machines[machine.definition.getId()]
quality_groups = self.getQualityGroups(machine)
result = quality_groups.get(machine_node.preferred_quality_type)
if result is not None and result.is_available:
return result
return None # If preferred quality type is not available, leave it up for the caller.
#
# Methods for GUI
#
#
# Remove the given quality changes group.
#
## 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 [%s]", quality_changes_group.name)
removed_quality_changes_ids = set()
for node in quality_changes_group.getAllNodes():
container_id = node.getMetaDataEntry("id")
self._container_registry.removeContainer(container_id)
removed_quality_changes_ids.add(container_id)
# Reset all machines that have activated this quality changes to empty.
for global_stack in self._container_registry.findContainerStacks(type = "machine"):
if global_stack.qualityChanges.getId() in removed_quality_changes_ids:
global_stack.qualityChanges = self._empty_quality_changes_container
for extruder_stack in self._container_registry.findContainerStacks(type = "extruder_train"):
if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids:
extruder_stack.qualityChanges = self._empty_quality_changes_container
return cura.CuraApplication.CuraApplication.getInstance().getQualityManagementModel().removeQualityChangesGroup(quality_changes_group)
## Rename a custom profile.
#
# Rename a set of quality changes containers. Returns the new name.
#
# 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[%s] to [%s]", quality_changes_group.name, new_name)
if new_name == quality_changes_group.name:
Logger.log("i", "QualityChangesGroup name [%s] unchanged.", quality_changes_group.name)
return new_name
return cura.CuraApplication.CuraApplication.getInstance().getQualityManagementModel().renameQualityChangesGroup(quality_changes_group, new_name)
new_name = self._container_registry.uniqueName(new_name)
for node in quality_changes_group.getAllNodes():
container = node.getContainer()
if container:
container.setName(new_name)
quality_changes_group.name = new_name
self._application.getMachineManager().activeQualityChanged.emit()
self._application.getMachineManager().activeQualityGroupChanged.emit()
return new_name
#
# Duplicates the given quality.
#
## 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, quality_changes_name: str, quality_model_item) -> None:
global_stack = self._application.getGlobalContainerStack()
if not global_stack:
Logger.log("i", "No active global stack, cannot duplicate quality changes.")
return
def duplicateQualityChanges(self, quality_changes_name: str, quality_model_item: Dict[str, Any]) -> None:
return cura.CuraApplication.CuraApplication.getInstance().getQualityManagementModel().duplicateQualityChanges(quality_changes_name, quality_model_item)
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_name = self._container_registry.uniqueName(quality_changes_name)
new_quality_changes = self._createQualityChanges(quality_group.quality_type, new_name,
global_stack, None)
self._container_registry.addContainer(new_quality_changes)
else:
new_name = self._container_registry.uniqueName(quality_changes_name)
for node in quality_changes_group.getAllNodes():
container = node.getContainer()
if not container:
continue
new_id = self._container_registry.uniqueName(container.getId())
self._container_registry.addContainer(container.duplicate(new_id, new_name))
## Create quality changes containers from the user containers in the active stacks.
## 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.
# 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.
@pyqtSlot(str)
def createQualityChanges(self, base_name: str) -> None:
machine_manager = self._application.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
unique_name = self._container_registry.uniqueName(base_name)
# Go through the active stacks and create quality_changes containers from the user containers.
stack_list = [global_stack] + list(global_stack.extruders.values())
for stack in stack_list:
user_container = stack.userChanges
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
quality_type = quality_container.getMetaDataEntry("quality_type")
extruder_stack = None
if isinstance(stack, ExtruderStack):
extruder_stack = stack
new_changes = self._createQualityChanges(quality_type, unique_name, global_stack, extruder_stack)
from cura.Settings.ContainerManager import ContainerManager
ContainerManager.getInstance()._performMerge(new_changes, quality_changes_container, clear_settings = False)
ContainerManager.getInstance()._performMerge(new_changes, user_container)
self._container_registry.addContainer(new_changes)
#
# Create a quality changes container with the given setup.
#
def _createQualityChanges(self, quality_type: str, new_name: str, machine: "GlobalStack",
extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer":
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 = self._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 = getMachineDefinitionIDForQualitySearch(machine.definition)
quality_changes.setDefinition(machine_definition_id)
quality_changes.setMetaDataEntry("setting_version", self._application.SettingVersion)
return quality_changes
return cura.CuraApplication.CuraApplication.getInstance().getQualityManagementModel().createQualityChanges(base_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":
return cura.CuraApplication.CuraApplication.getInstance().getQualityManagementModel()._createQualityChanges(quality_type, new_name, machine, extruder_stack)
#
# Gets the machine definition ID that can be used to search for Quality containers that are suitable for the given

View file

@ -1,38 +1,44 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Dict, cast, Any
from typing import Union, TYPE_CHECKING
from .ContainerNode import ContainerNode
from .QualityChangesGroup import QualityChangesGroup
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.IntentNode import IntentNode
import UM.FlameProfiler
if TYPE_CHECKING:
from typing import Dict
from cura.Machines.MaterialNode import MaterialNode
from cura.Machines.MachineNode import MachineNode
## Represents a quality profile in the container tree.
#
# QualityNode is used for BOTH quality and quality_changes containers.
# This may either be a normal quality profile or a global quality profile.
#
# Its subcontainers are intent profiles.
class QualityNode(ContainerNode):
def __init__(self, container_id: str, parent: Union["MaterialNode", "MachineNode"]) -> None:
super().__init__(container_id)
self.parent = parent
self.intents = {} # type: Dict[str, IntentNode]
def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
super().__init__(metadata = metadata)
self.quality_type_map = {} # type: Dict[str, QualityNode] # quality_type -> QualityNode for InstanceContainer
my_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = container_id)[0]
self.quality_type = my_metadata["quality_type"]
# The material type of the parent doesn't need to be the same as this due to generic fallbacks.
self._material = my_metadata.get("material")
self._loadAll()
def getChildNode(self, child_key: str) -> Optional["QualityNode"]:
return self.children_map.get(child_key)
@UM.FlameProfiler.profile
def _loadAll(self) -> None:
container_registry = ContainerRegistry.getInstance()
def addQualityMetadata(self, quality_type: str, metadata: Dict[str, Any]):
if quality_type not in self.quality_type_map:
self.quality_type_map[quality_type] = QualityNode(metadata)
# Find all intent profiles that fit the current configuration.
from cura.Machines.MachineNode import MachineNode
if not isinstance(self.parent, MachineNode): # Not a global profile.
for intent in container_registry.findInstanceContainersMetadata(type = "intent", definition = self.parent.variant.machine.quality_definition, variant = self.parent.variant.variant_name, material = self._material, quality_type = self.quality_type):
self.intents[intent["id"]] = IntentNode(intent["id"], quality = self)
def getQualityNode(self, quality_type: str) -> Optional["QualityNode"]:
return self.quality_type_map.get(quality_type)
def addQualityChangesMetadata(self, quality_type: str, metadata: Dict[str, Any]):
if quality_type not in self.quality_type_map:
self.quality_type_map[quality_type] = QualityNode()
quality_type_node = self.quality_type_map[quality_type]
name = metadata["name"]
if name not in quality_type_node.children_map:
quality_type_node.children_map[name] = QualityChangesGroup(name, quality_type)
quality_changes_group = quality_type_node.children_map[name]
cast(QualityChangesGroup, quality_changes_group).addNode(QualityNode(metadata))
self.intents["empty_intent"] = IntentNode("empty_intent", quality = self)
# Otherwise, there are no intents for global profiles.

View file

@ -1,16 +1,17 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from collections import OrderedDict
from typing import Optional, TYPE_CHECKING, Dict
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Decorators import deprecated
from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Util import parseBool
from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.VariantType import VariantType, ALL_VARIANT_TYPES
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.GlobalStack import GlobalStack
if TYPE_CHECKING:
@ -35,70 +36,20 @@ if TYPE_CHECKING:
# A container is loaded when getVariant() is called to load a variant InstanceContainer.
#
class VariantManager:
__instance = None
def __init__(self, container_registry: ContainerRegistry) -> None:
self._container_registry = container_registry
@classmethod
@deprecated("Use the ContainerTree structure instead.", since = "4.3")
def getInstance(cls) -> "VariantManager":
if cls.__instance is None:
cls.__instance = VariantManager()
return cls.__instance
def __init__(self) -> None:
self._machine_to_variant_dict_map = dict() # type: Dict[str, Dict["VariantType", Dict[str, ContainerNode]]]
self._machine_to_buildplate_dict_map = dict() # type: Dict[str, Dict[str, ContainerNode]]
self._exclude_variant_id_list = ["empty_variant"]
#
# Initializes the VariantManager including:
# - initializing the variant lookup table based on the metadata in ContainerRegistry.
#
def initialize(self) -> None:
self._machine_to_variant_dict_map = OrderedDict()
self._machine_to_buildplate_dict_map = OrderedDict()
# Cache all variants from the container registry to a variant map for better searching and organization.
variant_metadata_list = self._container_registry.findContainersMetadata(type = "variant")
for variant_metadata in variant_metadata_list:
if variant_metadata["id"] in self._exclude_variant_id_list:
Logger.log("d", "Exclude variant [%s]", variant_metadata["id"])
continue
variant_name = variant_metadata["name"]
variant_definition = variant_metadata["definition"]
if variant_definition not in self._machine_to_variant_dict_map:
self._machine_to_variant_dict_map[variant_definition] = OrderedDict()
for variant_type in ALL_VARIANT_TYPES:
self._machine_to_variant_dict_map[variant_definition][variant_type] = dict()
try:
variant_type = variant_metadata["hardware_type"]
except KeyError:
Logger.log("w", "Variant %s does not specify a hardware_type; assuming 'nozzle'", variant_metadata["id"])
variant_type = VariantType.NOZZLE
variant_type = VariantType(variant_type)
variant_dict = self._machine_to_variant_dict_map[variant_definition][variant_type]
if variant_name in variant_dict:
# ERROR: duplicated variant name.
ConfigurationErrorMessage.getInstance().addFaultyContainers(variant_metadata["id"])
continue #Then ignore this variant. This now chooses one of the two variants arbitrarily and deletes the other one! No guarantees!
variant_dict[variant_name] = ContainerNode(metadata = variant_metadata)
# If the variant is a buildplate then fill also the buildplate map
if variant_type == VariantType.BUILD_PLATE:
if variant_definition not in self._machine_to_buildplate_dict_map:
self._machine_to_buildplate_dict_map[variant_definition] = OrderedDict()
try:
variant_container = self._container_registry.findContainers(type = "variant", id = variant_metadata["id"])[0]
except IndexError as e:
# It still needs to break, but we want to know what variant ID made it break.
msg = "Unable to find build plate variant with the id [%s]" % variant_metadata["id"]
Logger.logException("e", msg)
raise IndexError(msg)
buildplate_type = variant_container.getProperty("machine_buildplate_type", "value")
if buildplate_type not in self._machine_to_buildplate_dict_map[variant_definition]:
self._machine_to_variant_dict_map[variant_definition][buildplate_type] = dict()
self._machine_to_buildplate_dict_map[variant_definition][buildplate_type] = variant_dict[variant_name]
#
# Gets the variant InstanceContainer with the given information.
# Almost the same as getVariantMetadata() except that this returns an InstanceContainer if present.
@ -107,7 +58,7 @@ class VariantManager:
variant_type: Optional["VariantType"] = None) -> Optional["ContainerNode"]:
if variant_type is None:
variant_node = None
variant_type_dict = self._machine_to_variant_dict_map[machine_definition_id]
variant_type_dict = self._machine_to_variant_dict_map.get("machine_definition_id", {})
for variant_dict in variant_type_dict.values():
if variant_name in variant_dict:
variant_node = variant_dict[variant_name]
@ -145,8 +96,3 @@ class VariantManager:
if preferred_variant_name:
node = self.getVariantNode(machine_definition_id, preferred_variant_name, variant_type)
return node
def getBuildplateVariantNode(self, machine_definition_id: str, buildplate_type: str) -> Optional["ContainerNode"]:
if machine_definition_id in self._machine_to_buildplate_dict_map:
return self._machine_to_buildplate_dict_map[machine_definition_id].get(buildplate_type)
return None

View file

@ -0,0 +1,162 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING
from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import ContainerInterface
from UM.Signal import Signal
from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.MaterialNode import MaterialNode
import UM.FlameProfiler
if TYPE_CHECKING:
from typing import Dict
from cura.Machines.MachineNode import MachineNode
## This class represents an extruder variant in the container tree.
#
# The subnodes of these nodes are materials.
#
# This node contains materials with ALL filament diameters underneath it. The
# tree of this variant is not specific to one global stack, so because the
# list of materials can be different per stack depending on the compatible
# material diameter setting, we cannot filter them here. Filtering must be
# done in the model.
class VariantNode(ContainerNode):
def __init__(self, container_id: str, machine: "MachineNode") -> None:
super().__init__(container_id)
self.machine = machine
self.materials = {} # type: Dict[str, MaterialNode] # Mapping material base files to their nodes.
self.materialsChanged = Signal()
container_registry = ContainerRegistry.getInstance()
self.variant_name = container_registry.findContainersMetadata(id = container_id)[0]["name"] # Store our own name so that we can filter more easily.
container_registry.containerAdded.connect(self._materialAdded)
container_registry.containerRemoved.connect(self._materialRemoved)
self._loadAll()
## (Re)loads all materials under this variant.
@UM.FlameProfiler.profile
def _loadAll(self) -> None:
container_registry = ContainerRegistry.getInstance()
if not self.machine.has_materials:
self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
return # There should not be any materials loaded for this printer.
# Find all the materials for this variant's name.
else: # Printer has its own material profiles. Look for material profiles with this printer's definition.
base_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = "fdmprinter")
printer_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id, variant_name = None)
variant_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id, variant_name = self.variant_name) # If empty_variant, this won't return anything.
materials_per_base_file = {material["base_file"]: material for material in base_materials}
materials_per_base_file.update({material["base_file"]: material for material in printer_specific_materials}) # Printer-specific profiles override global ones.
materials_per_base_file.update({material["base_file"]: material for material in variant_specific_materials}) # Variant-specific profiles override all of those.
materials = list(materials_per_base_file.values())
# Filter materials based on the exclude_materials property.
filtered_materials = [material for material in materials if material["id"] not in self.machine.exclude_materials]
for material in filtered_materials:
base_file = material["base_file"]
if base_file not in self.materials:
self.materials[base_file] = MaterialNode(material["id"], variant = self)
self.materials[base_file].materialChanged.connect(self.materialsChanged)
if not self.materials:
self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
## Finds the preferred material for this printer with this nozzle in one of
# the extruders.
#
# If the preferred material is not available, an arbitrary material is
# returned. If there is a configuration mistake (like a typo in the
# preferred material) this returns a random available material. If there
# are no available materials, this will return the empty material node.
# \param approximate_diameter The desired approximate diameter of the
# material.
# \return The node for the preferred material, or any arbitrary material
# if there is no match.
def preferredMaterial(self, approximate_diameter: int) -> MaterialNode:
for base_material, material_node in self.materials.items():
if self.machine.preferred_material in base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
return material_node
# First fallback: Choose any material with matching diameter.
for material_node in self.materials.values():
if approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")):
return material_node
fallback = next(iter(self.materials.values())) # Should only happen with empty material node.
Logger.log("w", "Could not find preferred material {preferred_material} with diameter {diameter} for variant {variant_id}, falling back to {fallback}.".format(
preferred_material = self.machine.preferred_material,
diameter = approximate_diameter,
variant_id = self.container_id,
fallback = fallback.container_id
))
return fallback
## When a material gets added to the set of profiles, we need to update our
# tree here.
@UM.FlameProfiler.profile
def _materialAdded(self, container: ContainerInterface) -> None:
if container.getMetaDataEntry("type") != "material":
return # Not interested.
if not self.machine.has_materials:
return # We won't add any materials.
material_definition = container.getMetaDataEntry("definition")
base_file = container.getMetaDataEntry("base_file")
if base_file in self.machine.exclude_materials:
return # Material is forbidden for this printer.
if base_file not in self.materials: # Completely new base file. Always better than not having a file as long as it matches our set-up.
if material_definition != "fdmprinter" and material_definition != self.machine.container_id:
return
material_variant = container.getMetaDataEntry("variant_name", "empty")
if material_variant != "empty" and material_variant != self.variant_name:
return
else: # We already have this base profile. Replace the base profile if the new one is more specific.
new_definition = container.getMetaDataEntry("definition")
if new_definition == "fdmprinter":
return # Just as unspecific or worse.
if new_definition != self.machine.container_id:
return # Doesn't match this set-up.
original_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = self.materials[base_file].container_id)[0]
original_variant = original_metadata.get("variant_name", "empty")
if original_variant != "empty" or container.getMetaDataEntry("variant_name", "empty") == "empty":
return # Original was already specific or just as unspecific as the new one.
if "empty_material" in self.materials:
del self.materials["empty_material"]
self.materials[base_file] = MaterialNode(container.getId(), variant = self)
self.materials[base_file].materialChanged.connect(self.materialsChanged)
self.materialsChanged.emit(self.materials[base_file])
@UM.FlameProfiler.profile
def _materialRemoved(self, container: ContainerInterface) -> None:
if container.getMetaDataEntry("type") != "material":
return # Only interested in materials.
base_file = container.getMetaDataEntry("base_file")
if base_file not in self.materials:
return # We don't track this material anyway. No need to remove it.
original_node = self.materials[base_file]
del self.materials[base_file]
self.materialsChanged.emit(original_node)
# Now a different material from the same base file may have been hidden because it was not as specific as the one we deleted.
# Search for any submaterials from that base file that are still left.
materials_same_base_file = ContainerRegistry.getInstance().findContainersMetadata(base_file = base_file)
if materials_same_base_file:
most_specific_submaterial = materials_same_base_file[0]
for submaterial in materials_same_base_file:
if submaterial["definition"] == self.machine.container_id:
if most_specific_submaterial["definition"] == "fdmprinter":
most_specific_submaterial = submaterial
if most_specific_submaterial.get("variant_name", "empty") == "empty" and submaterial.get("variant_name", "empty") == self.variant_name:
most_specific_submaterial = submaterial
self.materials[base_file] = MaterialNode(most_specific_submaterial["id"], variant = self)
self.materialsChanged.emit(self.materials[base_file])
if not self.materials: # The last available material just got deleted and there is nothing with the same base file to replace it.
self.materials["empty_material"] = MaterialNode("empty_material", variant = self)
self.materialsChanged.emit(self.materials["empty_material"])

View file

@ -225,7 +225,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded():
all_configurations.add(printer.printerConfiguration)
all_configurations.update(printer.availableConfigurations)
new_configurations = sorted(all_configurations, key = lambda config: config.printerType)
new_configurations = sorted(all_configurations, key = lambda config: config.printerType or "")
if new_configurations != self._unique_configurations:
self._unique_configurations = new_configurations
self.uniqueConfigurationsChanged.emit()
@ -233,7 +233,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
# Returns the unique configurations of the printers within this output device
@pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
def uniquePrinterTypes(self) -> List[str]:
return list(sorted(set([configuration.printerType for configuration in self._unique_configurations])))
return list(sorted(set([configuration.printerType or "" for configuration in self._unique_configurations])))
def _onPrintersChanged(self) -> None:
for printer in self._printers:

View file

@ -1,15 +1,14 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
import urllib.parse
import uuid
from typing import Dict, Union, Any, TYPE_CHECKING, List, cast
from typing import Any, cast, Dict, List, TYPE_CHECKING, Union
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
@ -17,21 +16,19 @@ from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
from UM.Platform import Platform
from UM.SaveFile import SaveFile
from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.InstanceContainer import InstanceContainer
import cura.CuraApplication
from cura.Machines.ContainerTree import ContainerTree
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 cura.Settings.MachineManager import MachineManager
from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
catalog = i18nCatalog("cura")
@ -52,17 +49,11 @@ class ContainerManager(QObject):
except TypeError:
super().__init__()
self._application = application # type: CuraApplication
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
self._container_registry = self._application.getContainerRegistry() # type: CuraContainerRegistry
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:
metadatas = self._container_registry.findContainersMetadata(id = container_id)
metadatas = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().findContainersMetadata(id = container_id)
if not metadatas:
Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
return ""
@ -91,15 +82,19 @@ class ContainerManager(QObject):
# 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: "ContainerNode", entry_name: str, entry_value: str) -> bool:
root_material_id = container_node.getMetaDataEntry("base_file", "")
if self._container_registry.isReadOnly(root_material_id):
if container_node.container is None:
Logger.log("w", "Container node {0} doesn't have a container.".format(container_node.container_id))
return False
root_material_id = container_node.container.getMetaDataEntry("base_file", "")
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
if 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)
root_material_query = container_registry.findContainers(id = root_material_id)
if not root_material_query:
Logger.log("w", "Unable to find root material: {root_material}.".format(root_material = root_material_id))
return False
root_material = root_material_query[0]
entries = entry_name.split("/")
entry_name = entries.pop()
@ -107,7 +102,7 @@ class ContainerManager(QObject):
sub_item_changed = False
if entries:
root_name = entries.pop(0)
root = material_group.root_material_node.getMetaDataEntry(root_name)
root = root_material.getMetaDataEntry(root_name)
item = root
for _ in range(len(entries)):
@ -120,16 +115,14 @@ class ContainerManager(QObject):
entry_name = root_name
entry_value = root
container = material_group.root_material_node.getContainer()
if container is not None:
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)
root_material.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.
root_material.metaDataChanged.emit(root_material)
return True
@pyqtSlot(str, result = str)
def makeUniqueName(self, original_name: str) -> str:
return self._container_registry.uniqueName(original_name)
return cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().uniqueName(original_name)
## Get a list of string that can be used as name filters for a Qt File Dialog
#
@ -184,7 +177,7 @@ class ContainerManager(QObject):
else:
mime_type = self._container_name_filters[file_type]["mime"]
containers = self._container_registry.findContainers(id = container_id)
containers = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().findContainers(id = container_id)
if not containers:
return {"status": "error", "message": "Container not found"}
container = containers[0]
@ -242,12 +235,13 @@ class ContainerManager(QObject):
except MimeTypeNotFoundError:
return {"status": "error", "message": "Could not determine mime type of file"}
container_type = self._container_registry.getContainerForMimeType(mime_type)
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
container_type = container_registry.getContainerForMimeType(mime_type)
if not container_type:
return {"status": "error", "message": "Could not find a container to handle the specified file."}
container_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_url)))
container_id = self._container_registry.uniqueName(container_id)
container_id = container_registry.uniqueName(container_id)
container = container_type(container_id)
@ -263,7 +257,7 @@ class ContainerManager(QObject):
container.setDirty(True)
self._container_registry.addContainer(container)
container_registry.addContainer(container)
return {"status": "success", "message": "Successfully imported container {0}".format(container.getName())}
@ -275,44 +269,55 @@ class ContainerManager(QObject):
# \return \type{bool} True if successful, False if not.
@pyqtSlot(result = bool)
def updateQualityChanges(self) -> bool:
global_stack = self._machine_manager.activeMachine
application = cura.CuraApplication.CuraApplication.getInstance()
global_stack = application.getMachineManager().activeMachine
if not global_stack:
return False
self._machine_manager.blurSettings.emit()
application.getMachineManager().blurSettings.emit()
current_quality_changes_name = global_stack.qualityChanges.getName()
current_quality_type = global_stack.quality.getMetaDataEntry("quality_type")
extruder_stacks = list(global_stack.extruders.values())
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
machine_definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
for stack in [global_stack] + extruder_stacks:
# Find the quality_changes container for this stack and merge the contents of the top container into it.
quality_changes = stack.qualityChanges
if quality_changes.getId() == "empty_quality_changes":
quality_changes = self._quality_manager._createQualityChanges(current_quality_type, current_quality_changes_name,
global_stack, stack)
self._container_registry.addContainer(quality_changes)
quality_changes = InstanceContainer(container_registry.uniqueName((stack.getId() + "_" + current_quality_changes_name).lower().replace(" ", "_")))
quality_changes.setName(current_quality_changes_name)
quality_changes.setMetaDataEntry("type", "quality_changes")
quality_changes.setMetaDataEntry("quality_type", current_quality_type)
if stack.getMetaDataEntry("position") is not None: # Extruder stacks.
quality_changes.setMetaDataEntry("position", stack.getMetaDataEntry("position"))
quality_changes.setMetaDataEntry("intent_category", stack.quality.getMetaDataEntry("intent_category", "default"))
quality_changes.setMetaDataEntry("setting_version", application.SettingVersion)
quality_changes.setDefinition(machine_definition_id)
container_registry.addContainer(quality_changes)
stack.qualityChanges = quality_changes
if not quality_changes or self._container_registry.isReadOnly(quality_changes.getId()):
if not quality_changes or container_registry.isReadOnly(quality_changes.getId()):
Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
continue
self._performMerge(quality_changes, stack.getTop())
self._machine_manager.activeQualityChangesGroupChanged.emit()
cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeQualityChangesGroupChanged.emit()
return True
## Clear the top-most (user) containers of the active stacks.
@pyqtSlot()
def clearUserContainers(self) -> None:
self._machine_manager.blurSettings.emit()
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
machine_manager.blurSettings.emit()
send_emits_containers = []
# Go through global and extruder stacks and clear their topmost container (the user settings).
global_stack = self._machine_manager.activeMachine
global_stack = machine_manager.activeMachine
extruder_stacks = list(global_stack.extruders.values())
for stack in [global_stack] + extruder_stacks:
container = stack.userChanges
@ -320,40 +325,39 @@ class ContainerManager(QObject):
send_emits_containers.append(container)
# user changes are possibly added to make the current setup match the current enabled extruders
self._machine_manager.correctExtruderSettings()
machine_manager.correctExtruderSettings()
for container in send_emits_containers:
container.sendPostponedEmits()
## Get a list of materials that have the same GUID as the reference material
#
# \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
# \param material_node The node representing the material for which to get
# the same GUID.
# \param exclude_self Whether to include the name of the material you
# provided.
# \return A list of names of materials with the same GUID.
@pyqtSlot("QVariant", bool, result = "QStringList")
def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False):
guid = material_node.getMetaDataEntry("GUID", "")
self_root_material_id = material_node.getMetaDataEntry("base_file")
material_group_list = self._material_manager.getMaterialGroupListByGUID(guid)
linked_material_names = []
if material_group_list:
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.getMetaDataEntry("name", ""))
return linked_material_names
def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False) -> List[str]:
same_guid = ContainerRegistry.getInstance().findInstanceContainersMetadata(guid = material_node.guid)
if exclude_self:
return [metadata["name"] for metadata in same_guid if metadata["base_file"] != material_node.base_file]
else:
return [metadata["name"] for metadata in same_guid]
## 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: "MaterialNode") -> None:
# Get the material group
material_group = self._material_manager.getMaterialGroup(material_node.getMetaDataEntry("base_file", ""))
if material_group is None:
if material_node.container is None:
Logger.log("w", "Material node {0} doesn't have a container.".format(material_node.container_id))
return
root_material_query = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().findInstanceContainers(id = material_node.getMetaDataEntry("base_file", ""))
if not root_material_query:
Logger.log("w", "Unable to find material group for %s", material_node)
return
root_material = root_material_query[0]
# Generate a new GUID
new_guid = str(uuid.uuid4())
@ -361,9 +365,7 @@ class ContainerManager(QObject):
# Update the GUID
# NOTE: We only need to set the root material container because XmlMaterialProfile.setMetaDataEntry() will
# take care of the derived containers too
container = material_group.root_material_node.getContainer()
if container is not None:
container.setMetaDataEntry("GUID", new_guid)
root_material.setMetaDataEntry("GUID", new_guid)
def _performMerge(self, merge_into: InstanceContainer, merge: InstanceContainer, clear_settings: bool = True) -> None:
if merge == merge_into:
@ -377,14 +379,16 @@ class ContainerManager(QObject):
def _updateContainerNameFilters(self) -> None:
self._container_name_filters = {}
for plugin_id, container_type in self._container_registry.getContainerTypes():
plugin_registry = cura.CuraApplication.CuraApplication.getInstance().getPluginRegistry()
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
for plugin_id, container_type in container_registry.getContainerTypes():
# Ignore default container types since those are not plugins
if container_type in (InstanceContainer, ContainerStack, DefinitionContainer):
continue
serialize_type = ""
try:
plugin_metadata = self._plugin_registry.getMetaData(plugin_id)
plugin_metadata = plugin_registry.getMetaData(plugin_id)
if plugin_metadata:
serialize_type = plugin_metadata["settings_container"]["type"]
else:
@ -392,7 +396,7 @@ class ContainerManager(QObject):
except KeyError as e:
continue
mime_type = self._container_registry.getMimeTypeForContainer(container_type)
mime_type = container_registry.getMimeTypeForContainer(container_type)
if mime_type is None:
continue
entry = {
@ -428,7 +432,7 @@ class ContainerManager(QObject):
path = file_url.toLocalFile()
if not path:
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
return self._container_registry.importProfile(path)
return cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().importProfile(path)
@pyqtSlot(QObject, QUrl, str)
def exportQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", file_url: QUrl, file_type: str) -> None:
@ -438,8 +442,11 @@ class ContainerManager(QObject):
if not path:
return
container_list = [cast(InstanceContainer, n.getContainer()) for n in quality_changes_group.getAllNodes() if n.getContainer() is not None]
self._container_registry.exportQualityProfile(container_list, path, file_type)
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
container_list = [cast(InstanceContainer, container_registry.findContainers(id = quality_changes_group.metadata_for_global["id"])[0])] # type: List[InstanceContainer]
for metadata in quality_changes_group.metadata_per_extruder.values():
container_list.append(cast(InstanceContainer, container_registry.findContainers(id = metadata["id"])[0]))
cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().exportQualityProfile(container_list, path, file_type)
__instance = None # type: ContainerManager

View file

@ -21,6 +21,7 @@ from UM.Message import Message
from UM.Platform import Platform
from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with.
from UM.Resources import Resources
from UM.Util import parseBool
from cura.ReaderWriters.ProfileWriter import ProfileWriter
from . import ExtruderStack
@ -28,7 +29,7 @@ from . import GlobalStack
import cura.CuraApplication
from cura.Settings.cura_empty_instance_containers import empty_quality_container
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from cura.Machines.ContainerTree import ContainerTree
from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader
from UM.i18n import i18nCatalog
@ -101,7 +102,8 @@ class CuraContainerRegistry(ContainerRegistry):
## Exports an profile to a file
#
# \param container_list \type{list} the containers to export
# \param container_list \type{list} the containers to export. This is not
# necessarily in any order!
# \param file_name \type{str} the full path and filename to export to.
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
# \return True if the export succeeded, false otherwise.
@ -177,6 +179,7 @@ class CuraContainerRegistry(ContainerRegistry):
global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack:
return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Can't import profile from <filename>{0}</filename> before a printer is added.", file_name)}
container_tree = ContainerTree.getInstance()
machine_extruders = []
for position in sorted(global_stack.extruders):
@ -226,7 +229,7 @@ class CuraContainerRegistry(ContainerRegistry):
# Make sure we have a profile_definition in the file:
if profile_definition is None:
break
machine_definitions = self.findDefinitionContainers(id = profile_definition)
machine_definitions = self.findContainers(id = profile_definition)
if not machine_definitions:
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
return {"status": "error",
@ -236,8 +239,9 @@ class CuraContainerRegistry(ContainerRegistry):
# Get the expected machine definition.
# i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode...
profile_definition = getMachineDefinitionIDForQualitySearch(machine_definition)
expected_machine_definition = getMachineDefinitionIDForQualitySearch(global_stack.definition)
has_machine_quality = parseBool(machine_definition.getMetaDataEntry("has_machine_quality", "false"))
profile_definition = machine_definition.getMetaDataEntry("quality_definition", machine_definition.getId()) if has_machine_quality else "fdmprinter"
expected_machine_definition = container_tree.machines[global_stack.definition.getId()].quality_definition
# And check if the profile_definition matches either one (showing error if not):
if profile_definition != expected_machine_definition:
@ -268,7 +272,6 @@ class CuraContainerRegistry(ContainerRegistry):
profile.setMetaDataEntry("type", "quality_changes")
profile.setMetaDataEntry("definition", expected_machine_definition)
profile.setMetaDataEntry("quality_type", quality_type)
profile.setMetaDataEntry("position", "0")
profile.setDirty(True)
if idx == 0:
# Move all per-extruder settings to the first extruder's quality_changes
@ -384,14 +387,13 @@ class CuraContainerRegistry(ContainerRegistry):
global_stack = Application.getInstance().getGlobalContainerStack()
if global_stack is None:
return None
definition_id = getMachineDefinitionIDForQualitySearch(global_stack.definition)
definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
profile.setDefinition(definition_id)
# Check to make sure the imported profile actually makes sense in context of the current configuration.
# This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as
# successfully imported but then fail to show up.
quality_manager = cura.CuraApplication.CuraApplication.getInstance()._quality_manager
quality_group_dict = quality_manager.getQualityGroupsForMachineDefinition(global_stack)
quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups()
# "not_supported" profiles can be imported.
if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict:
return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
@ -614,6 +616,7 @@ class CuraContainerRegistry(ContainerRegistry):
extruder_quality_changes_container.setMetaDataEntry("setting_version", application.SettingVersion)
extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
extruder_quality_changes_container.setMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type"))
extruder_quality_changes_container.setMetaDataEntry("intent_category", "default") # Intent categories weren't a thing back then.
extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId())
self.addContainer(extruder_quality_changes_container)

View file

@ -87,6 +87,19 @@ class CuraContainerStack(ContainerStack):
def qualityChanges(self) -> InstanceContainer:
return cast(InstanceContainer, self._containers[_ContainerIndexes.QualityChanges])
## Set the intent container.
#
# \param new_intent The new intent container. It is expected to have a "type" metadata entry with the value "intent".
def setIntent(self, new_intent: InstanceContainer, postpone_emit: bool = False) -> None:
self.replaceContainer(_ContainerIndexes.Intent, new_intent, postpone_emit = postpone_emit)
## Get the quality container.
#
# \return The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@pyqtProperty(InstanceContainer, fset = setIntent, notify = pyqtContainersChanged)
def intent(self) -> InstanceContainer:
return cast(InstanceContainer, self._containers[_ContainerIndexes.Intent])
## Set the quality container.
#
# \param new_quality The new quality container. It is expected to have a "type" metadata entry with the value "quality".
@ -330,16 +343,18 @@ class CuraContainerStack(ContainerStack):
class _ContainerIndexes:
UserChanges = 0
QualityChanges = 1
Quality = 2
Material = 3
Variant = 4
DefinitionChanges = 5
Definition = 6
Intent = 2
Quality = 3
Material = 4
Variant = 5
DefinitionChanges = 6
Definition = 7
# Simple hash map to map from index to "type" metadata entry
IndexTypeMap = {
UserChanges: "user",
QualityChanges: "quality_changes",
Intent: "intent",
Quality: "quality",
Material: "material",
Variant: "variant",

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
@ -8,7 +8,8 @@ from UM.Logger import Logger
from UM.Settings.Interfaces import DefinitionContainerInterface
from UM.Settings.InstanceContainer import InstanceContainer
from cura.Machines.VariantType import VariantType
from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.MachineNode import MachineNode
from .GlobalStack import GlobalStack
from .ExtruderStack import ExtruderStack
@ -26,9 +27,8 @@ class CuraStackBuilder:
def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]:
from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance()
variant_manager = application.getVariantManager()
quality_manager = application.getQualityManager()
registry = application.getContainerRegistry()
container_tree = ContainerTree.getInstance()
definitions = registry.findDefinitionContainers(id = definition_id)
if not definitions:
@ -37,14 +37,11 @@ class CuraStackBuilder:
return None
machine_definition = definitions[0]
# get variant container for the global stack
global_variant_container = application.empty_variant_container
global_variant_node = variant_manager.getDefaultVariantNode(machine_definition, VariantType.BUILD_PLATE)
if global_variant_node:
global_variant_container = global_variant_node.getContainer()
if not global_variant_container:
global_variant_container = application.empty_variant_container
# The container tree listens to the containerAdded signal to add the definition and build the tree,
# but that signal is emitted with a delay which might not have passed yet.
# Therefore we must make sure that it's manually added here.
container_tree.addMachineNodeByDefinitionId(machine_definition.getId())
machine_node = container_tree.machines[machine_definition.getId()]
generated_name = registry.createUniqueName("machine", "", name, machine_definition.getName())
# Make sure the new name does not collide with any definition or (quality) profile
@ -56,9 +53,9 @@ class CuraStackBuilder:
new_global_stack = cls.createGlobalStack(
new_stack_id = generated_name,
definition = machine_definition,
variant_container = global_variant_container,
variant_container = application.empty_variant_container,
material_container = application.empty_material_container,
quality_container = application.empty_quality_container,
quality_container = machine_node.preferredGlobalQuality().container,
)
new_global_stack.setName(generated_name)
@ -67,33 +64,9 @@ class CuraStackBuilder:
for position in extruder_dict:
cls.createExtruderStackWithDefaultSetup(new_global_stack, position)
for new_extruder in new_global_stack.extruders.values(): #Only register the extruders if we're sure that all of them are correct.
for new_extruder in new_global_stack.extruders.values(): # Only register the extruders if we're sure that all of them are correct.
registry.addContainer(new_extruder)
preferred_quality_type = machine_definition.getMetaDataEntry("preferred_quality_type")
quality_group_dict = quality_manager.getQualityGroups(new_global_stack)
if not quality_group_dict:
# There is no available quality group, set all quality containers to empty.
new_global_stack.quality = application.empty_quality_container
for extruder_stack in new_global_stack.extruders.values():
extruder_stack.quality = application.empty_quality_container
else:
# Set the quality containers to the preferred quality type if available, otherwise use the first quality
# type that's available.
if preferred_quality_type not in quality_group_dict:
Logger.log("w", "The preferred quality {quality_type} doesn't exist for this set-up. Choosing a random one.".format(quality_type = preferred_quality_type))
preferred_quality_type = next(iter(quality_group_dict))
quality_group = quality_group_dict.get(preferred_quality_type)
new_global_stack.quality = quality_group.node_for_global.getContainer()
if not new_global_stack.quality:
new_global_stack.quality = application.empty_quality_container
for position, extruder_stack in new_global_stack.extruders.items():
if position in quality_group.nodes_for_extruders and quality_group.nodes_for_extruders[position].getContainer():
extruder_stack.quality = quality_group.nodes_for_extruders[position].getContainer()
else:
extruder_stack.quality = application.empty_quality_container
# Register the global stack after the extruder stacks are created. This prevents the registry from adding another
# extruder stack because the global stack didn't have one yet (which is enforced since Cura 3.1).
registry.addContainer(new_global_stack)
@ -108,37 +81,32 @@ class CuraStackBuilder:
def createExtruderStackWithDefaultSetup(cls, global_stack: "GlobalStack", extruder_position: int) -> None:
from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance()
variant_manager = application.getVariantManager()
material_manager = application.getMaterialManager()
registry = application.getContainerRegistry()
# get variant container for extruders
extruder_variant_container = application.empty_variant_container
extruder_variant_node = variant_manager.getDefaultVariantNode(global_stack.definition, VariantType.NOZZLE,
global_stack = global_stack)
extruder_variant_name = None
if extruder_variant_node:
extruder_variant_container = extruder_variant_node.getContainer()
if not extruder_variant_container:
extruder_variant_container = application.empty_variant_container
extruder_variant_name = extruder_variant_container.getName()
# Get the extruder definition.
extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains")
extruder_definition_id = extruder_definition_dict[str(extruder_position)]
try:
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
except IndexError as e:
except IndexError:
# It still needs to break, but we want to know what extruder ID made it break.
msg = "Unable to find extruder definition with the id [%s]" % extruder_definition_id
Logger.logException("e", msg)
raise IndexError(msg)
# get material container for extruders
material_container = application.empty_material_container
material_node = material_manager.getDefaultMaterial(global_stack, str(extruder_position), extruder_variant_name,
extruder_definition = extruder_definition)
if material_node and material_node.getContainer():
material_container = material_node.getContainer()
# Find out what filament diameter we need.
approximate_diameter = round(extruder_definition.getProperty("material_diameter", "value")) # Can't be modified by definition changes since we are just initialising the stack here.
# Find the preferred containers.
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
extruder_variant_node = machine_node.variants.get(machine_node.preferred_variant_name)
if not extruder_variant_node:
Logger.log("w", "Could not find preferred nozzle {nozzle_name}. Falling back to {fallback}.".format(nozzle_name = machine_node.preferred_variant_name, fallback = next(iter(machine_node.variants))))
extruder_variant_node = next(iter(machine_node.variants.values()))
extruder_variant_container = extruder_variant_node.container
material_node = extruder_variant_node.preferredMaterial(approximate_diameter)
material_container = material_node.container
quality_node = material_node.preferredQuality()
new_extruder_id = registry.uniqueName(extruder_definition_id)
new_extruder = cls.createExtruderStack(
@ -148,7 +116,7 @@ class CuraStackBuilder:
position = extruder_position,
variant_container = extruder_variant_container,
material_container = material_container,
quality_container = application.empty_quality_container
quality_container = quality_node.container
)
new_extruder.setNextStack(global_stack)
@ -190,6 +158,7 @@ class CuraStackBuilder:
stack.variant = variant_container
stack.material = material_container
stack.quality = quality_container
stack.intent = application.empty_intent_container
stack.qualityChanges = application.empty_quality_changes_container
stack.userChanges = user_container
@ -238,6 +207,7 @@ class CuraStackBuilder:
stack.variant = variant_container
stack.material = material_container
stack.quality = quality_container
stack.intent = application.empty_intent_container
stack.qualityChanges = application.empty_quality_changes_container
stack.userChanges = user_container

View file

@ -0,0 +1,165 @@
#Copyright (c) 2019 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from typing import Any, Dict, List, Optional, Set, Tuple, TYPE_CHECKING
import cura.CuraApplication
from UM.Logger import Logger
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.cura_empty_instance_containers import empty_intent_container
from UM.Settings.InstanceContainer import InstanceContainer
if TYPE_CHECKING:
from UM.Settings.InstanceContainer import InstanceContainer
## Front-end for querying which intents are available for a certain
# configuration.
class IntentManager(QObject):
__instance = None
## This class is a singleton.
@classmethod
def getInstance(cls):
if not cls.__instance:
cls.__instance = IntentManager()
return cls.__instance
intentCategoryChanged = pyqtSignal() #Triggered when we switch categories.
## Gets the metadata dictionaries of all intent profiles for a given
# configuration.
#
# \param definition_id ID of the printer.
# \param nozzle_name Name of the nozzle.
# \param material_base_file The base_file of the material.
# \return A list of metadata dictionaries matching the search criteria, or
# an empty list if nothing was found.
def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]:
material_node = ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials[material_base_file]
intent_metadatas = []
for quality_node in material_node.qualities.values():
for intent_node in quality_node.intents.values():
intent_metadatas.append(intent_node.getMetadata())
return intent_metadatas
## Collects and returns all intent categories available for the given
# parameters. Note that the 'default' category is always available.
#
# \param definition_id ID of the printer.
# \param nozzle_name Name of the nozzle.
# \param material_id ID of the material.
# \return A set of intent category names.
def intentCategories(self, definition_id: str, nozzle_id: str, material_id: str) -> List[str]:
categories = set()
for intent in self.intentMetadatas(definition_id, nozzle_id, material_id):
categories.add(intent["intent_category"])
categories.add("default") #The "empty" intent is not an actual profile specific to the configuration but we do want it to appear in the categories list.
return list(categories)
## List of intents to be displayed in the interface.
#
# For the interface this will have to be broken up into the different
# intent categories. That is up to the model there.
#
# \return A list of tuples of intent_category and quality_type. The actual
# instance may vary per extruder.
def getCurrentAvailableIntents(self) -> List[Tuple[str, str]]:
application = cura.CuraApplication.CuraApplication.getInstance()
global_stack = application.getGlobalContainerStack()
if global_stack is None:
return [("default", "normal")]
# TODO: We now do this (return a default) if the global stack is missing, but not in the code below,
# even though there should always be defaults. The problem then is what to do with the quality_types.
# Currently _also_ inconsistent with 'currentAvailableIntentCategories', which _does_ return default.
quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
available_quality_types = {quality_group.quality_type for quality_group in quality_groups.values() if quality_group.node_for_global is not None}
final_intent_ids = set() # type: Set[str]
current_definition_id = global_stack.definition.getId()
for extruder_stack in global_stack.extruderList:
nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
material_id = extruder_stack.material.getMetaDataEntry("base_file")
final_intent_ids |= {metadata["id"] for metadata in self.intentMetadatas(current_definition_id, nozzle_name, material_id) if metadata.get("quality_type") in available_quality_types}
result = set() # type: Set[Tuple[str, str]]
for intent_id in final_intent_ids:
intent_metadata = application.getContainerRegistry().findContainersMetadata(id = intent_id)[0]
result.add((intent_metadata["intent_category"], intent_metadata["quality_type"]))
return list(result)
## List of intent categories available in either of the extruders.
#
# This is purposefully inconsistent with the way that the quality types
# are listed. The quality types will show all quality types available in
# the printer using any configuration. This will only list the intent
# categories that are available using the current configuration (but the
# union over the extruders).
# \return List of all categories in the current configurations of all
# extruders.
def currentAvailableIntentCategories(self) -> List[str]:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return ["default"]
current_definition_id = global_stack.definition.getId()
final_intent_categories = set() # type: Set[str]
for extruder_stack in global_stack.extruderList:
nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
material_id = extruder_stack.material.getMetaDataEntry("base_file")
final_intent_categories.update(self.intentCategories(current_definition_id, nozzle_name, material_id))
return list(final_intent_categories)
## The intent that gets selected by default when no intent is available for
# the configuration, an extruder can't match the intent that the user
# selects, or just when creating a new printer.
def getDefaultIntent(self) -> InstanceContainer:
return empty_intent_container
@pyqtProperty(str, notify = intentCategoryChanged)
def currentIntentCategory(self) -> str:
application = cura.CuraApplication.CuraApplication.getInstance()
active_extruder_stack = application.getMachineManager().activeStack
if active_extruder_stack is None:
return ""
return active_extruder_stack.intent.getMetaDataEntry("intent_category", "")
## Apply intent on the stacks.
@pyqtSlot(str, str)
def selectIntent(self, intent_category: str, quality_type: str) -> None:
Logger.log("i", "Attempting to set intent_category to [%s] and quality type to [%s]", intent_category, quality_type)
old_intent_category = self.currentIntentCategory
application = cura.CuraApplication.CuraApplication.getInstance()
global_stack = application.getGlobalContainerStack()
if global_stack is None:
return
current_definition_id = global_stack.definition.getId()
machine_node = ContainerTree.getInstance().machines[current_definition_id]
for extruder_stack in global_stack.extruderList:
nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
material_id = extruder_stack.material.getMetaDataEntry("base_file")
material_node = machine_node.variants[nozzle_name].materials[material_id]
# Since we want to switch to a certain quality type, check the tree if we have one.
quality_node = None
for q_node in material_node.qualities.values():
if q_node.quality_type == quality_type:
quality_node = q_node
if quality_node is None:
Logger.log("w", "Unable to find quality_type [%s] for extruder [%s]", quality_type, extruder_stack.getId())
continue
# Check that quality node if we can find a matching intent.
intent_id = None
for id, intent_node in quality_node.intents.items():
if intent_node.intent_category == intent_category:
intent_id = id
intent = application.getContainerRegistry().findContainers(id = intent_id)
if intent:
extruder_stack.intent = intent[0]
else:
extruder_stack.intent = self.getDefaultIntent()
application.getMachineManager().setQualityGroupByQualityType(quality_type)
if old_intent_category != intent_category:
self.intentCategoryChanged.emit()

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import time
@ -22,7 +22,12 @@ from UM.Message import Message
from UM.Settings.SettingFunction import SettingFunction
from UM.Signal import postponeSignals, CompressTechnique
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
import cura.CuraApplication # Imported like this to prevent circular references.
from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionType
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
@ -32,7 +37,7 @@ from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.cura_empty_instance_containers import (empty_definition_changes_container, empty_variant_container,
empty_material_container, empty_quality_container,
empty_quality_changes_container)
empty_quality_changes_container, empty_intent_container)
from .CuraStackBuilder import CuraStackBuilder
@ -41,13 +46,10 @@ catalog = i18nCatalog("cura")
from cura.Settings.GlobalStack import GlobalStack
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
from cura.Settings.CuraContainerStack import CuraContainerStack
from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager
from cura.Machines.VariantManager import VariantManager
from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.MaterialNode import MaterialNode
from cura.Machines.QualityChangesGroup import QualityChangesGroup
from cura.Machines.QualityGroup import QualityGroup
from cura.Machines.VariantNode import VariantNode
class MachineManager(QObject):
@ -58,8 +60,6 @@ class MachineManager(QObject):
self._global_container_stack = None # type: Optional[GlobalStack]
self._current_root_material_id = {} # type: Dict[str, str]
self._current_quality_group = None # type: Optional[QualityGroup]
self._current_quality_changes_group = None # type: Optional[QualityChangesGroup]
self._default_extruder_position = "0" # to be updated when extruders are switched on and off
@ -118,22 +118,20 @@ class MachineManager(QObject):
if containers:
containers[0].nameChanged.connect(self._onMaterialNameChanged)
self._material_manager = self._application.getMaterialManager() # type: MaterialManager
self._variant_manager = self._application.getVariantManager() # type: VariantManager
self._quality_manager = self._application.getQualityManager() # type: QualityManager
# When the materials lookup table gets updated, it can mean that a material has its name changed, which should
# be reflected on the GUI. This signal emission makes sure that it happens.
self._material_manager.materialsUpdated.connect(self.rootMaterialChanged)
# When the materials get updated, it can be that an activated material's diameter gets changed. In that case,
# a material update should be triggered to make sure that the machine still has compatible materials activated.
self._material_manager.materialsUpdated.connect(self._updateUponMaterialMetadataChange)
self.rootMaterialChanged.connect(self._onRootMaterialChanged)
# Emit the printerConnectedStatusChanged when either globalContainerChanged or outputDevicesChanged are emitted
self.globalContainerChanged.connect(self.printerConnectedStatusChanged)
self.outputDevicesChanged.connect(self.printerConnectedStatusChanged)
# For updating active quality display name
self.activeQualityChanged.connect(self.activeQualityDisplayNameChanged)
self.activeIntentChanged.connect(self.activeQualityDisplayNameChanged)
self.activeQualityGroupChanged.connect(self.activeQualityDisplayNameChanged)
self.activeQualityChangesGroupChanged.connect(self.activeQualityDisplayNameChanged)
activeQualityDisplayNameChanged = pyqtSignal()
activeQualityGroupChanged = pyqtSignal()
activeQualityChangesGroupChanged = pyqtSignal()
@ -141,6 +139,7 @@ class MachineManager(QObject):
activeMaterialChanged = pyqtSignal()
activeVariantChanged = pyqtSignal()
activeQualityChanged = pyqtSignal()
activeIntentChanged = pyqtSignal()
activeStackChanged = pyqtSignal() # Emitted whenever the active stack is changed (ie: when changing between extruders, changing a profile, but not when changing a value)
extruderChanged = pyqtSignal()
@ -222,10 +221,6 @@ class MachineManager(QObject):
def _onGlobalContainerChanged(self) -> None:
if self._global_container_stack:
try:
self._global_container_stack.nameChanged.disconnect(self._onMachineNameChanged)
except TypeError: # pyQtSignal gives a TypeError when disconnecting from something that was already disconnected.
pass
try:
self._global_container_stack.containersChanged.disconnect(self._onContainersChanged)
except TypeError:
@ -250,7 +245,6 @@ class MachineManager(QObject):
if self._global_container_stack:
self._application.getPreferences().setValue("cura/active_machine", self._global_container_stack.getId())
self._global_container_stack.nameChanged.connect(self._onMachineNameChanged)
self._global_container_stack.containersChanged.connect(self._onContainersChanged)
self._global_container_stack.propertyChanged.connect(self._onPropertyChanged)
@ -270,16 +264,23 @@ class MachineManager(QObject):
extruder_stack.propertyChanged.connect(self._onPropertyChanged)
extruder_stack.containersChanged.connect(self._onContainersChanged)
self._onRootMaterialChanged()
self.activeQualityGroupChanged.emit()
def _onActiveExtruderStackChanged(self) -> None:
self.blurSettings.emit() # Ensure no-one has focus.
if self._active_container_stack is not None:
self._active_container_stack.pyqtContainersChanged.disconnect(self.activeStackChanged) # Unplug from the old one.
self._active_container_stack = ExtruderManager.getInstance().getActiveExtruderStack()
if self._active_container_stack is not None:
self._active_container_stack.pyqtContainersChanged.connect(self.activeStackChanged) # Plug into the new one.
def __emitChangedSignals(self) -> None:
self.activeQualityChanged.emit()
self.activeVariantChanged.emit()
self.activeMaterialChanged.emit()
self.activeIntentChanged.emit()
self.rootMaterialChanged.emit()
self.numberExtrudersEnabledChanged.emit()
@ -292,57 +293,6 @@ class MachineManager(QObject):
# Notify UI items, such as the "changed" star in profile pull down menu.
self.activeStackValueChanged.emit()
## Given a global_stack, make sure that it's all valid by searching for this quality group and applying it again
def _initMachineState(self, global_stack: "CuraContainerStack") -> None:
material_dict = {}
for position, extruder in enumerate(global_stack.extruderList):
material_dict[str(position)] = extruder.material.getMetaDataEntry("base_file")
self._current_root_material_id = material_dict
# Update materials to make sure that the diameters match with the machine's
for position, _ in enumerate(global_stack.extruderList):
self.updateMaterialWithVariant(str(position))
global_quality = global_stack.quality
quality_type = global_quality.getMetaDataEntry("quality_type")
global_quality_changes = global_stack.qualityChanges
global_quality_changes_name = global_quality_changes.getName()
# Try to set the same quality/quality_changes as the machine specified.
# If the quality/quality_changes is not available, switch to the default or the first quality that's available.
same_quality_found = False
quality_groups = self._application.getQualityManager().getQualityGroups(global_stack)
if global_quality_changes.getId() != "empty_quality_changes":
quality_changes_groups = self._application.getQualityManager().getQualityChangesGroups(global_stack)
new_quality_changes_group = quality_changes_groups.get(global_quality_changes_name)
if new_quality_changes_group is not None:
self._setQualityChangesGroup(new_quality_changes_group)
same_quality_found = True
Logger.log("i", "Machine '%s' quality changes set to '%s'",
global_stack.getName(), new_quality_changes_group.name)
else:
new_quality_group = quality_groups.get(quality_type)
if new_quality_group is not None:
self._setQualityGroup(new_quality_group, empty_quality_changes = True)
same_quality_found = True
Logger.log("i", "Machine '%s' quality set to '%s'",
global_stack.getName(), new_quality_group.quality_type)
# Could not find the specified quality/quality_changes, switch to the preferred quality if available,
# otherwise the first quality that's available, otherwise empty (not supported).
if not same_quality_found:
Logger.log("i", "Machine '%s' could not find quality_type '%s' and quality_changes '%s'. "
"Available quality types are [%s]. Switching to default quality.",
global_stack.getName(), quality_type, global_quality_changes_name,
", ".join(quality_groups.keys()))
preferred_quality_type = global_stack.getMetaDataEntry("preferred_quality_type")
quality_group = quality_groups.get(preferred_quality_type)
if quality_group is None:
if quality_groups:
quality_group = list(quality_groups.values())[0]
self._setQualityGroup(quality_group, empty_quality_changes = True)
@pyqtSlot(str)
def setActiveMachine(self, stack_id: str) -> None:
self.blurSettings.emit() # Ensure no-one has focus.
@ -353,7 +303,7 @@ class MachineManager(QObject):
if not containers:
return
global_stack = containers[0]
global_stack = cast(GlobalStack, containers[0])
# Make sure that the default machine actions for this machine have been added
self._application.getMachineActionManager().addDefaultMachineActions(global_stack)
@ -367,7 +317,6 @@ class MachineManager(QObject):
self._global_container_stack = global_stack
self._application.setGlobalContainerStack(global_stack)
ExtruderManager.getInstance()._globalContainerStackChanged()
self._initMachineState(global_stack)
self._onGlobalContainerChanged()
# Switch to the first enabled extruder
@ -612,6 +561,7 @@ class MachineManager(QObject):
#
# \return The material ids in all stacks
@pyqtProperty("QVariantMap", notify = activeMaterialChanged)
@deprecated("use Cura.MachineManager.activeStack.extruders instead.", "4.3")
def allActiveMaterialIds(self) -> Dict[str, str]:
result = {}
@ -629,22 +579,15 @@ class MachineManager(QObject):
# This is indicated together with the name of the active quality profile.
#
# \return The layer height of the currently active quality profile. If
# there is no quality profile, this returns 0.
# there is no quality profile, this returns the default layer height.
@pyqtProperty(float, notify = activeQualityGroupChanged)
def activeQualityLayerHeight(self) -> float:
if not self._global_container_stack:
return 0
if self._current_quality_changes_group:
value = self._global_container_stack.getRawProperty("layer_height", "value", skip_until_container = self._global_container_stack.qualityChanges.getId())
if isinstance(value, SettingFunction):
value = value(self._global_container_stack)
return value
elif self._current_quality_group:
value = self._global_container_stack.getRawProperty("layer_height", "value", skip_until_container = self._global_container_stack.quality.getId())
if isinstance(value, SettingFunction):
value = value(self._global_container_stack)
return value
return 0
value = self._global_container_stack.getRawProperty("layer_height", "value", skip_until_container = self._global_container_stack.qualityChanges.getId())
if isinstance(value, SettingFunction):
value = value(self._global_container_stack)
return value
@pyqtProperty(str, notify = activeVariantChanged)
def globalVariantName(self) -> str:
@ -656,27 +599,60 @@ class MachineManager(QObject):
@pyqtProperty(str, notify = activeQualityGroupChanged)
def activeQualityType(self) -> str:
quality_type = ""
if self._active_container_stack:
if self._current_quality_group:
quality_type = self._current_quality_group.quality_type
return quality_type
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack:
return ""
return global_stack.quality.getMetaDataEntry("quality_type")
@pyqtProperty(bool, notify = activeQualityGroupChanged)
def isActiveQualitySupported(self) -> bool:
is_supported = False
if self._global_container_stack:
if self._current_quality_group:
is_supported = self._current_quality_group.is_available
return is_supported
global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_container_stack:
return False
active_quality_group = self.activeQualityGroup()
if active_quality_group is None:
return False
return active_quality_group.is_available
@pyqtProperty(bool, notify = activeQualityGroupChanged)
def isActiveQualityExperimental(self) -> bool:
is_experimental = False
if self._global_container_stack:
if self._current_quality_group:
is_experimental = self._current_quality_group.is_experimental
return is_experimental
global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_container_stack:
return False
return Util.parseBool(global_container_stack.quality.getMetaDataEntry("is_experimental", False))
@pyqtProperty(str, notify = activeIntentChanged)
def activeIntentCategory(self) -> str:
global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_container_stack:
return ""
intent_category = "default"
for extruder in global_container_stack.extruderList:
category = extruder.intent.getMetaDataEntry("intent_category", "default")
if category != "default" and category != intent_category:
intent_category = category
return intent_category
# Provies a list of extruder positions that have a different intent from the active one.
@pyqtProperty("QStringList", notify=activeIntentChanged)
def extruderPositionsWithNonActiveIntent(self):
global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_container_stack:
return []
active_intent_category = self.activeIntentCategory
result = []
for extruder in global_container_stack.extruderList:
if not extruder.isEnabled:
continue
category = extruder.intent.getMetaDataEntry("intent_category", "default")
if category != active_intent_category:
result.append(str(int(extruder.getMetaDataEntry("position")) + 1))
return result
## Returns whether there is anything unsupported in the current set-up.
#
@ -764,9 +740,10 @@ class MachineManager(QObject):
# \returns DefinitionID (string) if found, empty string otherwise
@pyqtProperty(str, notify = globalContainerChanged)
def activeQualityDefinitionId(self) -> str:
if self._global_container_stack:
return getMachineDefinitionIDForQualitySearch(self._global_container_stack.definition)
return ""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack:
return ""
return ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
## Gets how the active definition calls variants
# Caveat: per-definition-variant-title is currently not translated (though the fallback is)
@ -1074,19 +1051,19 @@ class MachineManager(QObject):
self.forceUpdateAllSettings()
# Also trigger the build plate compatibility to update
self.activeMaterialChanged.emit()
def _onMachineNameChanged(self) -> None:
self.globalContainerChanged.emit()
self.activeIntentChanged.emit()
def _onMaterialNameChanged(self) -> None:
self.activeMaterialChanged.emit()
## Get the signals that signal that the containers changed for all stacks.
#
# This includes the global stack and all extruder stacks. So if any
# container changed anywhere.
def _getContainerChangedSignals(self) -> List[Signal]:
if self._global_container_stack is None:
return []
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
stacks.append(self._global_container_stack)
return [ s.containersChanged for s in stacks ]
return [s.containersChanged for s in ExtruderManager.getInstance().getActiveExtruderStacks() + [self._global_container_stack]]
@pyqtSlot(str, str, str)
def setSettingForAllExtruders(self, setting_name: str, property_name: str, property_value: str) -> None:
@ -1155,8 +1132,6 @@ class MachineManager(QObject):
def _setEmptyQuality(self) -> None:
if self._global_container_stack is None:
return
self._current_quality_group = None
self._current_quality_changes_group = None
self._global_container_stack.quality = empty_quality_container
self._global_container_stack.qualityChanges = empty_quality_changes_container
for extruder in self._global_container_stack.extruderList:
@ -1165,6 +1140,7 @@ class MachineManager(QObject):
self.activeQualityGroupChanged.emit()
self.activeQualityChangesGroupChanged.emit()
self._updateIntentWithQuality()
def _setQualityGroup(self, quality_group: Optional["QualityGroup"], empty_quality_changes: bool = True) -> None:
if self._global_container_stack is None:
@ -1173,40 +1149,33 @@ class MachineManager(QObject):
self._setEmptyQuality()
return
if quality_group.node_for_global is None or quality_group.node_for_global.getContainer() is None:
if quality_group.node_for_global is None or quality_group.node_for_global.container is None:
return
for node in quality_group.nodes_for_extruders.values():
if node.getContainer() is None:
if node.container is None:
return
self._current_quality_group = quality_group
if empty_quality_changes:
self._current_quality_changes_group = None
# Set quality and quality_changes for the GlobalStack
self._global_container_stack.quality = quality_group.node_for_global.getContainer()
self._global_container_stack.quality = quality_group.node_for_global.container
if empty_quality_changes:
self._global_container_stack.qualityChanges = empty_quality_changes_container
# Set quality and quality_changes for each ExtruderStack
for position, node in quality_group.nodes_for_extruders.items():
try:
self._global_container_stack.extruderList[int(position)].quality = node.getContainer()
if empty_quality_changes:
self._global_container_stack.extruderList[int(position)].qualityChanges = empty_quality_changes_container
except IndexError:
continue # This can be ignored as in some cases the quality group gets set for an extruder that's not active (eg custom fff printer)
self._global_container_stack.extruders[str(position)].quality = node.container
if empty_quality_changes:
self._global_container_stack.extruders[str(position)].qualityChanges = empty_quality_changes_container
self.activeQualityGroupChanged.emit()
self.activeQualityChangesGroupChanged.emit()
self._updateIntentWithQuality()
def _fixQualityChangesGroupToNotSupported(self, quality_changes_group: "QualityChangesGroup") -> None:
nodes = [quality_changes_group.node_for_global] + list(quality_changes_group.nodes_for_extruders.values())
containers = [n.getContainer() for n in nodes if n is not None]
for container in containers:
if container:
container.setMetaDataEntry("quality_type", "not_supported")
metadatas = [quality_changes_group.metadata_for_global] + list(quality_changes_group.metadata_per_extruder.values())
for metadata in metadatas:
metadata["quality_type"] = "not_supported" # This actually changes the metadata of the container since they are stored by reference!
quality_changes_group.quality_type = "not_supported"
quality_changes_group.intent_category = "default"
def _setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
if self._global_container_stack is None:
@ -1215,68 +1184,76 @@ class MachineManager(QObject):
# A custom quality can be created based on "not supported".
# In that case, do not set quality containers to empty.
quality_group = None
if quality_type != "not_supported":
quality_group_dict = self._quality_manager.getQualityGroups(self._global_container_stack)
quality_group = quality_group_dict.get(quality_type)
if quality_type != "not_supported": # Find the quality group that the quality changes was based on.
quality_group = ContainerTree.getInstance().getCurrentQualityGroups().get(quality_type)
if quality_group is None:
self._fixQualityChangesGroupToNotSupported(quality_changes_group)
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
quality_changes_container = empty_quality_changes_container
quality_container = empty_quality_container # type: Optional[InstanceContainer]
if quality_changes_group.node_for_global and quality_changes_group.node_for_global.getContainer():
quality_changes_container = cast(InstanceContainer, quality_changes_group.node_for_global.getContainer())
if quality_group is not None and quality_group.node_for_global and quality_group.node_for_global.getContainer():
quality_container = quality_group.node_for_global.getContainer()
quality_container = empty_quality_container # type: InstanceContainer
if quality_changes_group.metadata_for_global:
global_containers = container_registry.findContainers(id = quality_changes_group.metadata_for_global["id"])
if global_containers:
quality_changes_container = global_containers[0]
if quality_changes_group.metadata_for_global:
containers = container_registry.findContainers(id = quality_changes_group.metadata_for_global["id"])
if containers:
quality_changes_container = cast(InstanceContainer, containers[0])
if quality_group is not None and quality_group.node_for_global and quality_group.node_for_global.container:
quality_container = quality_group.node_for_global.container
self._global_container_stack.quality = quality_container
self._global_container_stack.qualityChanges = quality_changes_container
for extruder in self._global_container_stack.extruderList:
position = int(extruder.getMetaDataEntry("position", "0"))
quality_changes_node = quality_changes_group.nodes_for_extruders.get(position)
for position, extruder in self._global_container_stack.extruders.items():
quality_node = None
if quality_group is not None:
quality_node = quality_group.nodes_for_extruders.get(position)
quality_node = quality_group.nodes_for_extruders.get(int(position))
quality_changes_container = empty_quality_changes_container
quality_container = empty_quality_container
if quality_changes_node and quality_changes_node.getContainer():
quality_changes_container = cast(InstanceContainer, quality_changes_node.getContainer())
if quality_node and quality_node.getContainer():
quality_container = quality_node.getContainer()
quality_changes_metadata = quality_changes_group.metadata_per_extruder.get(int(position))
if quality_changes_metadata:
containers = container_registry.findContainers(id = quality_changes_metadata["id"])
if containers:
quality_changes_container = cast(InstanceContainer, containers[0])
if quality_node and quality_node.container:
quality_container = quality_node.container
extruder.quality = quality_container
extruder.qualityChanges = quality_changes_container
self._current_quality_group = quality_group
self._current_quality_changes_group = quality_changes_group
self.setIntentByCategory(quality_changes_group.intent_category)
self.activeQualityGroupChanged.emit()
self.activeQualityChangesGroupChanged.emit()
def _setVariantNode(self, position: str, container_node: "ContainerNode") -> None:
if container_node.getContainer() is None or self._global_container_stack is None:
def _setVariantNode(self, position: str, variant_node: "VariantNode") -> None:
if self._global_container_stack is None:
return
self._global_container_stack.extruderList[int(position)].variant = container_node.getContainer()
self._global_container_stack.extruders[position].variant = variant_node.container
self.activeVariantChanged.emit()
def _setGlobalVariant(self, container_node: "ContainerNode") -> None:
if self._global_container_stack is None:
return
self._global_container_stack.variant = container_node.getContainer()
self._global_container_stack.variant = container_node.container
if not self._global_container_stack.variant:
self._global_container_stack.variant = self._application.empty_variant_container
def _setMaterial(self, position: str, container_node: Optional["ContainerNode"] = None) -> None:
def _setMaterial(self, position: str, material_node: Optional["MaterialNode"] = None) -> None:
if self._global_container_stack is None:
return
if container_node and container_node.getContainer():
self._global_container_stack.extruderList[int(position)].material = container_node.getContainer()
root_material_id = container_node.getMetaDataEntry("base_file", None)
if material_node and material_node.container:
material_container = material_node.container
self._global_container_stack.extruders[position].material = material_container
root_material_id = material_container.getMetaDataEntry("base_file", None)
else:
self._global_container_stack.extruderList[int(position)].material = empty_material_container
root_material_id = None
# The _current_root_material_id is used in the MaterialMenu to see which material is selected
if root_material_id != self._current_root_material_id[position]:
if position not in self._current_root_material_id or root_material_id != self._current_root_material_id[position]:
self._current_root_material_id[position] = root_material_id
self.rootMaterialChanged.emit()
@ -1293,13 +1270,12 @@ class MachineManager(QObject):
## Update current quality type and machine after setting material
def _updateQualityWithMaterial(self, *args: Any) -> None:
if self._global_container_stack is None:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return
Logger.log("d", "Updating quality/quality_changes due to material change")
current_quality_type = None
if self._current_quality_group:
current_quality_type = self._current_quality_group.quality_type
candidate_quality_groups = self._quality_manager.getQualityGroups(self._global_container_stack)
current_quality_type = global_stack.quality.getMetaDataEntry("quality_type")
candidate_quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
available_quality_types = {qt for qt, g in candidate_quality_groups.items() if g.is_available}
Logger.log("d", "Current quality type = [%s]", current_quality_type)
@ -1310,7 +1286,7 @@ class MachineManager(QObject):
return
if not available_quality_types:
if self._current_quality_changes_group is None:
if global_stack.qualityChanges == empty_quality_changes_container:
Logger.log("i", "No available quality types found, setting all qualities to empty (Not Supported).")
self._setEmptyQuality()
return
@ -1323,6 +1299,9 @@ class MachineManager(QObject):
# The current quality type is not available so we use the preferred quality type if it's available,
# otherwise use one of the available quality types.
quality_type = sorted(list(available_quality_types))[0]
if self._global_container_stack is None:
Logger.log("e", "Global stack not present!")
return
preferred_quality_type = self._global_container_stack.getMetaDataEntry("preferred_quality_type")
if preferred_quality_type in available_quality_types:
quality_type = preferred_quality_type
@ -1331,6 +1310,38 @@ class MachineManager(QObject):
current_quality_type, quality_type)
self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True)
## Update the current intent after the quality changed
def _updateIntentWithQuality(self):
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return
Logger.log("d", "Updating intent due to quality change")
category = "default"
for extruder in global_stack.extruderList:
if not extruder.isEnabled:
continue
current_category = extruder.intent.getMetaDataEntry("intent_category", "default")
if current_category != "default" and current_category != category:
category = current_category
continue
# It's also possible that the qualityChanges has an opinion about the intent_category.
# This is in the case that a QC was made on an intent, but none of the materials have that intent.
# If the user switches back, we do want the intent to be selected again.
#
# Do not ask empty quality changes for intent category.
if extruder.qualityChanges.getId() == empty_quality_changes_container.getId():
continue
current_category = extruder.qualityChanges.getMetaDataEntry("intent_category", "default")
if current_category != "default" and current_category != category:
category = current_category
self.setIntentByCategory(category)
## Update the material profile in the current stacks when the variant is
# changed.
# \param position The extruder stack to update. If provided with None, all
# extruder stacks will be updated.
def updateMaterialWithVariant(self, position: Optional[str]) -> None:
if self._global_container_stack is None:
return
@ -1339,10 +1350,6 @@ class MachineManager(QObject):
else:
position_list = [position]
buildplate_name = None
if self._global_container_stack.variant.getId() != "empty_variant":
buildplate_name = self._global_container_stack.variant.getName()
for position_item in position_list:
try:
extruder = self._global_container_stack.extruderList[int(position_item)]
@ -1350,39 +1357,35 @@ class MachineManager(QObject):
continue
current_material_base_name = extruder.material.getMetaDataEntry("base_file")
current_nozzle_name = None
if extruder.variant.getId() != empty_variant_container.getId():
current_nozzle_name = extruder.variant.getMetaDataEntry("name")
current_nozzle_name = extruder.variant.getMetaDataEntry("name")
material_diameter = extruder.getCompatibleMaterialDiameter()
candidate_materials = self._material_manager.getAvailableMaterials(
self._global_container_stack.definition,
current_nozzle_name,
buildplate_name,
material_diameter)
# If we can keep the current material after the switch, try to do so.
nozzle_node = ContainerTree.getInstance().machines[self._global_container_stack.definition.getId()].variants[current_nozzle_name]
candidate_materials = nozzle_node.materials
old_approximate_material_diameter = int(extruder.material.getMetaDataEntry("approximate_diameter", default = 3))
new_approximate_material_diameter = int(self._global_container_stack.extruderList[int(position_item)].getApproximateMaterialDiameter())
if not candidate_materials:
self._setMaterial(position_item, container_node = None)
continue
if current_material_base_name in candidate_materials:
# Only switch to the old candidate material if the approximate material diameter of the extruder stays the
# same.
if new_approximate_material_diameter == old_approximate_material_diameter and \
current_material_base_name in candidate_materials: # The current material is also available after the switch. Retain it.
new_material = candidate_materials[current_material_base_name]
self._setMaterial(position_item, new_material)
continue
# The current material is not available, find the preferred one
material_node = self._material_manager.getDefaultMaterial(self._global_container_stack, position_item, current_nozzle_name)
if material_node is not None:
self._setMaterial(position_item, material_node)
else:
# The current material is not available, find the preferred one.
if position is not None:
approximate_material_diameter = int(self._global_container_stack.extruderList[int(position_item)].getApproximateMaterialDiameter())
material_node = nozzle_node.preferredMaterial(approximate_material_diameter)
self._setMaterial(position_item, material_node)
## Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new
# instance with the same network key.
@pyqtSlot(str)
def switchPrinterType(self, machine_name: str) -> None:
Logger.log("i", "Attempting to switch the printer type to [%s]", machine_name)
# Don't switch if the user tries to change to the same type of printer
if self._global_container_stack is None or self.activeMachineDefinitionName == machine_name:
return
Logger.log("i", "Attempting to switch the printer type to [%s]", machine_name)
# Get the definition id corresponding to this machine name
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
# Try to find a machine with the same network key
@ -1412,6 +1415,7 @@ class MachineManager(QObject):
if self._global_container_stack is None:
return
self.blurSettings.emit()
container_registry = CuraContainerRegistry.getInstance()
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self.switchPrinterType(configuration.printerType)
@ -1446,36 +1450,25 @@ class MachineManager(QObject):
disabled_used_extruder_position_set.add(int(position))
else:
variant_container_node = self._variant_manager.getVariantNode(self._global_container_stack.definition.getId(),
extruder_configuration.hotendID)
material_container_node = self._material_manager.getMaterialNodeByType(self._global_container_stack,
position,
extruder_configuration.hotendID,
configuration.buildplateConfiguration,
extruder_configuration.material.guid)
if variant_container_node:
self._setVariantNode(position, variant_container_node)
else:
self._global_container_stack.extruderList[int(position)].variant = empty_variant_container
machine_node = ContainerTree.getInstance().machines.get(self._global_container_stack.definition.getId())
variant_node = machine_node.variants.get(extruder_configuration.hotendID)
self._setVariantNode(position, variant_node)
if material_container_node:
self._setMaterial(position, material_container_node)
else:
self._global_container_stack.extruderList[int(position)].material = empty_material_container
self._global_container_stack.extruderList[int(position)].setEnabled(True)
# Find the material profile that the printer has stored.
# This might find one of the duplicates if the user duplicated the material to sync with. But that's okay; both have this GUID so both are correct.
approximate_diameter = int(self._global_container_stack.extruderList[int(position)].getApproximateMaterialDiameter())
materials_with_guid = container_registry.findInstanceContainersMetadata(GUID = extruder_configuration.material.guid, approximate_diameter = str(approximate_diameter), ignore_case = True)
material_container_node = variant_node.preferredMaterial(approximate_diameter)
if materials_with_guid: # We also have the material profile that the printer wants to share.
base_file = materials_with_guid[0]["base_file"]
material_container_node = variant_node.materials.get(base_file, material_container_node)
self._setMaterial(position, material_container_node)
self._global_container_stack.extruders[position].setEnabled(True)
self.updateMaterialWithVariant(position)
self.updateDefaultExtruder()
self.updateNumberExtrudersEnabled()
if configuration.buildplateConfiguration is not None:
global_variant_container_node = self._variant_manager.getBuildplateVariantNode(self._global_container_stack.definition.getId(), configuration.buildplateConfiguration)
if global_variant_container_node:
self._setGlobalVariant(global_variant_container_node)
else:
self._global_container_stack.variant = empty_variant_container
else:
self._global_container_stack.variant = empty_variant_container
self._updateQualityWithMaterial()
if need_to_show_message:
@ -1509,17 +1502,12 @@ class MachineManager(QObject):
def setMaterialById(self, position: str, root_material_id: str) -> None:
if self._global_container_stack is None:
return
buildplate_name = None
if self._global_container_stack.variant.getId() != "empty_variant":
buildplate_name = self._global_container_stack.variant.getName()
machine_definition_id = self._global_container_stack.definition.id
position = str(position)
extruder_stack = self._global_container_stack.extruderList[int(position)]
nozzle_name = extruder_stack.variant.getName()
material_diameter = extruder_stack.getApproximateMaterialDiameter()
material_node = self._material_manager.getMaterialNode(machine_definition_id, nozzle_name, buildplate_name,
material_diameter, root_material_id)
material_node = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials[root_material_id]
self.setMaterial(position, material_node)
## Global_stack: if you want to provide your own global_stack instead of the current active one
@ -1527,7 +1515,7 @@ class MachineManager(QObject):
@pyqtSlot(str, "QVariant")
def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None:
if global_stack is not None and global_stack != self._global_container_stack:
global_stack.extruderList[int(position)].material = container_node.getContainer()
global_stack.extruders[position].material = container_node.container
return
position = str(position)
self.blurSettings.emit()
@ -1548,11 +1536,11 @@ class MachineManager(QObject):
self.setVariant(position, variant_node)
@pyqtSlot(str, "QVariant")
def setVariant(self, position: str, container_node: "ContainerNode") -> None:
def setVariant(self, position: str, variant_node: "VariantNode") -> None:
position = str(position)
self.blurSettings.emit()
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self._setVariantNode(position, container_node)
self._setVariantNode(position, variant_node)
self.updateMaterialWithVariant(position)
self._updateQualityWithMaterial()
@ -1565,9 +1553,7 @@ class MachineManager(QObject):
if self._global_container_stack is None:
return
# Get all the quality groups for this global stack and filter out by quality_type
quality_group_dict = self._quality_manager.getQualityGroups(self._global_container_stack)
quality_group = quality_group_dict[quality_type]
self.setQualityGroup(quality_group)
self.setQualityGroup(ContainerTree.getInstance().getCurrentQualityGroups()[quality_type])
## Optionally provide global_stack if you want to use your own
# The active global_stack is treated differently.
@ -1581,11 +1567,11 @@ class MachineManager(QObject):
Logger.log("e", "Could not set quality group [%s] because it has no node_for_global", str(quality_group))
return
# This is not changing the quality for the active machine !!!!!!!!
global_stack.quality = quality_group.node_for_global.getContainer()
for extruder_nr, extruder_stack in global_stack.extruders.items():
global_stack.quality = quality_group.node_for_global.container
for extruder_nr, extruder_stack in enumerate(global_stack.extruderList):
quality_container = empty_quality_container
if extruder_nr in quality_group.nodes_for_extruders:
container = quality_group.nodes_for_extruders[extruder_nr].getContainer()
container = quality_group.nodes_for_extruders[extruder_nr].container
quality_container = container if container is not None else quality_container
extruder_stack.quality = quality_container
return
@ -1598,9 +1584,90 @@ class MachineManager(QObject):
if not no_dialog and self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1:
self._application.discardOrKeepProfileChanges()
@pyqtProperty(QObject, fset = setQualityGroup, notify = activeQualityGroupChanged)
# The display name map of currently active quality.
# The display name has 2 parts, a main part and a suffix part.
# This display name is:
# - For built-in qualities (quality/intent): the quality type name, such as "Fine", "Normal", etc.
# - For custom qualities: <custom_quality_name> - <intent_name> - <quality_type_name>
# Examples:
# - "my_profile - Fine" (only based on a default quality, no intent involved)
# - "my_profile - Engineering - Fine" (based on an intent)
@pyqtProperty("QVariantMap", notify = activeQualityDisplayNameChanged)
def activeQualityDisplayNameMap(self) -> Dict[str, str]:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return {"main": "",
"suffix": ""}
display_name = global_stack.quality.getName()
intent_category = self.activeIntentCategory
if intent_category != "default":
intent_display_name = IntentCategoryModel.name_translation.get(intent_category,
catalog.i18nc("@label", "Unknown"))
display_name = "{intent_name} - {the_rest}".format(intent_name = intent_display_name,
the_rest = display_name)
main_part = display_name
suffix_part = ""
# Not a custom quality
if global_stack.qualityChanges != empty_quality_changes_container:
main_part = self.activeQualityOrQualityChangesName
suffix_part = display_name
return {"main": main_part,
"suffix": suffix_part}
## Change the intent category of the current printer.
#
# All extruders can change their profiles. If an intent profile is
# available with the desired intent category, that one will get chosen.
# Otherwise the intent profile will be left to the empty profile, which
# represents the "default" intent category.
# \param intent_category The intent category to change to.
@pyqtSlot(str)
def setIntentByCategory(self, intent_category: str) -> None:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return
container_tree = ContainerTree.getInstance()
for extruder in global_stack.extruderList:
definition_id = global_stack.definition.getId()
variant_name = extruder.variant.getName()
material_base_file = extruder.material.getMetaDataEntry("base_file")
quality_id = extruder.quality.getId()
if quality_id == empty_quality_container.getId():
extruder.intent = empty_intent_container
continue
quality_node = container_tree.machines[definition_id].variants[variant_name].materials[material_base_file].qualities[quality_id]
for intent_node in quality_node.intents.values():
if intent_node.intent_category == intent_category: # Found an intent with the correct category.
extruder.intent = intent_node.container
break
else: # No intent had the correct category.
extruder.intent = empty_intent_container
## Get the currently activated quality group.
#
# If no printer is added yet or the printer doesn't have quality profiles,
# this returns ``None``.
# \return The currently active quality group.
def activeQualityGroup(self) -> Optional["QualityGroup"]:
return self._current_quality_group
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack or global_stack.quality == empty_quality_container:
return None
return ContainerTree.getInstance().getCurrentQualityGroups().get(self.activeQualityType)
## Get the name of the active quality group.
# \return The name of the active quality group.
@pyqtProperty(str, notify = activeQualityGroupChanged)
def activeQualityGroupName(self) -> str:
quality_group = self.activeQualityGroup()
if quality_group is None:
return ""
return quality_group.getName()
@pyqtSlot(QObject)
def setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", no_dialog: bool = False) -> None:
@ -1617,30 +1684,50 @@ class MachineManager(QObject):
if self._global_container_stack is None:
return
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self._setQualityGroup(self._current_quality_group)
self._setQualityGroup(self.activeQualityGroup())
for stack in [self._global_container_stack] + list(self._global_container_stack.extruders.values()):
stack.userChanges.clear()
@pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged)
def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]:
return self._current_quality_changes_group
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None or global_stack.qualityChanges == empty_quality_changes_container:
return None
all_group_list = ContainerTree.getInstance().getCurrentQualityChangesGroups()
the_group = None
for group in all_group_list: # Match on the container ID of the global stack to find the quality changes group belonging to the active configuration.
if group.metadata_for_global and group.metadata_for_global["id"] == global_stack.qualityChanges.getId():
the_group = group
break
return the_group
@pyqtProperty(bool, notify = activeQualityChangesGroupChanged)
def hasCustomQuality(self) -> bool:
return self._current_quality_changes_group is not None
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
return global_stack is None or global_stack.qualityChanges != empty_quality_changes_container
@pyqtProperty(str, notify = activeQualityGroupChanged)
def activeQualityOrQualityChangesName(self) -> str:
name = empty_quality_container.getName()
if self._current_quality_changes_group:
name = self._current_quality_changes_group.name
elif self._current_quality_group:
name = self._current_quality_group.name
return name
global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_container_stack:
return empty_quality_container.getName()
if global_container_stack.qualityChanges != empty_quality_changes_container:
return global_container_stack.qualityChanges.getName()
return global_container_stack.quality.getName()
@pyqtProperty(bool, notify = activeQualityGroupChanged)
def hasNotSupportedQuality(self) -> bool:
return self._current_quality_group is None and self._current_quality_changes_group is None
global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
return (not global_container_stack is None) and global_container_stack.quality == empty_quality_container and global_container_stack.qualityChanges == empty_quality_changes_container
@pyqtProperty(bool, notify = activeQualityGroupChanged)
def isActiveQualityCustom(self) -> bool:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return False
return global_stack.qualityChanges != empty_quality_changes_container
def _updateUponMaterialMetadataChange(self) -> None:
if self._global_container_stack is None:
@ -1667,7 +1754,3 @@ class MachineManager(QObject):
abbr_machine += stripped_word
return abbr_machine
# Gets all machines that belong to the given group_id.
def getMachinesInGroup(self, group_id: str) -> List["GlobalStack"]:
return self._container_registry.findContainerStacks(type = "machine", group_id = group_id)

View file

@ -25,6 +25,9 @@ EMPTY_MATERIAL_CONTAINER_ID = "empty_material"
empty_material_container = copy.deepcopy(empty_container)
empty_material_container.setMetaDataEntry("id", EMPTY_MATERIAL_CONTAINER_ID)
empty_material_container.setMetaDataEntry("type", "material")
empty_material_container.setMetaDataEntry("base_file", "empty_material")
empty_material_container.setMetaDataEntry("GUID", "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")
empty_material_container.setMetaDataEntry("material", "empty")
# Empty quality
EMPTY_QUALITY_CONTAINER_ID = "empty_quality"
@ -41,6 +44,15 @@ empty_quality_changes_container = copy.deepcopy(empty_container)
empty_quality_changes_container.setMetaDataEntry("id", EMPTY_QUALITY_CHANGES_CONTAINER_ID)
empty_quality_changes_container.setMetaDataEntry("type", "quality_changes")
empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported")
empty_quality_changes_container.setMetaDataEntry("intent_category", "not_supported")
# Empty intent
EMPTY_INTENT_CONTAINER_ID = "empty_intent"
empty_intent_container = copy.deepcopy(empty_container)
empty_intent_container.setMetaDataEntry("id", EMPTY_INTENT_CONTAINER_ID)
empty_intent_container.setMetaDataEntry("type", "intent")
empty_intent_container.setMetaDataEntry("intent_category", "default")
empty_intent_container.setName(catalog.i18nc("@info:No intent profile selected", "Default"))
# All empty container IDs set
@ -51,6 +63,7 @@ ALL_EMPTY_CONTAINER_ID_SET = {
EMPTY_MATERIAL_CONTAINER_ID,
EMPTY_QUALITY_CONTAINER_ID,
EMPTY_QUALITY_CHANGES_CONTAINER_ID,
EMPTY_INTENT_CONTAINER_ID
}
@ -73,4 +86,6 @@ __all__ = ["EMPTY_CONTAINER_ID",
"empty_quality_container",
"ALL_EMPTY_CONTAINER_ID_SET",
"isEmptyContainer",
"EMPTY_INTENT_CONTAINER_ID",
"empty_intent_container"
]

View file

@ -2,11 +2,12 @@
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtSlot
from UM.i18n import i18nCatalog
from cura.Machines.ContainerTree import ContainerTree
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
@ -42,7 +43,7 @@ class MachineSettingsManager(QObject):
# it was moved to the machine manager instead. Now this method just calls the machine manager.
self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count)
# Function for the Machine Settings panel (QML) to update after the usre changes "Number of Extruders".
# Function for the Machine Settings panel (QML) to update after the user changes "Number of Extruders".
#
# fieldOfView: The Ultimaker 2 family (not 2+) does not have materials in Cura by default, because the material is
# to be set on the printer. But when switching to Marlin flavor, the printer firmware can not change/insert material
@ -51,8 +52,6 @@ class MachineSettingsManager(QObject):
@pyqtSlot()
def updateHasMaterialsMetadata(self):
machine_manager = self._application.getMachineManager()
material_manager = self._application.getMaterialManager()
global_stack = machine_manager.activeMachine
definition = global_stack.definition
@ -76,7 +75,10 @@ class MachineSettingsManager(QObject):
# set materials
for position in extruder_positions:
if has_materials:
material_node = material_manager.getDefaultMaterial(global_stack, position, None)
extruder = global_stack.extruderList[int(position)]
approximate_diameter = extruder.getApproximateMaterialDiameter()
variant_node = ContainerTree.getInstance().machines[global_stack.definition.getId()].variants[extruder.variant.getName()]
material_node = variant_node.preferredMaterial(approximate_diameter)
machine_manager.setMaterial(position, material_node)
self.forceUpdate()

View file

@ -19,12 +19,12 @@ from UM.Scene.SceneNode import SceneNode #For typing.
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
try:
@ -131,7 +131,7 @@ class ThreeMFReader(MeshReader):
um_node.callDecoration("setActiveExtruder", default_stack.getId())
# Get the definition & set it
definition_id = getMachineDefinitionIDForQualitySearch(global_container_stack.definition)
definition_id = ContainerTree.getInstance().machines[global_container_stack.definition.getId()].quality_definition
um_node.callDecoration("getStack").getTop().setDefinition(definition_id)
setting_container = um_node.callDecoration("getStack").getTop()

View file

@ -1,10 +1,10 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from configparser import ConfigParser
import zipfile
import os
from typing import Dict, List, Tuple, cast
from typing import cast, Dict, List, Optional, Tuple
import xml.etree.ElementTree as ET
@ -14,7 +14,6 @@ from UM.Application import Application
from UM.Logger import Logger
from UM.Message import Message
from UM.i18n import i18nCatalog
from UM.Signal import postponeSignals, CompressTechnique
from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.DefinitionContainer import DefinitionContainer
@ -24,11 +23,12 @@ from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from UM.Job import Job
from UM.Preferences import Preferences
from cura.Machines.VariantType import VariantType
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.CuraStackBuilder import CuraStackBuilder
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.IntentManager import IntentManager
from cura.Settings.CuraContainerStack import _ContainerIndexes
from cura.CuraApplication import CuraApplication
from cura.Utils.Threading import call_on_qt_thread
@ -41,7 +41,7 @@ i18n_catalog = i18nCatalog("cura")
class ContainerInfo:
def __init__(self, file_name: str, serialized: str, parser: ConfigParser) -> None:
def __init__(self, file_name: Optional[str], serialized: Optional[str], parser: Optional[ConfigParser]) -> None:
self.file_name = file_name
self.serialized = serialized
self.parser = parser
@ -65,6 +65,7 @@ class MachineInfo:
self.metadata_dict = {} # type: Dict[str, str]
self.quality_type = None
self.intent_category = None
self.custom_quality_name = None
self.quality_changes_info = None
self.variant_info = None
@ -84,6 +85,7 @@ class ExtruderInfo:
self.definition_changes_info = None
self.user_changes_info = None
self.intent_info = None
## Base implementation for reading 3MF workspace files.
@ -266,6 +268,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
quality_name = ""
custom_quality_name = ""
intent_name = ""
intent_category = ""
num_settings_overridden_by_quality_changes = 0 # How many settings are changed by the quality changes
num_user_settings = 0
quality_changes_conflict = False
@ -323,6 +327,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
elif container_type == "quality":
if not quality_name:
quality_name = parser["general"]["name"]
elif container_type == "intent":
if not intent_name:
intent_name = parser["general"]["name"]
intent_category = parser["metadata"]["intent_category"]
elif container_type == "user":
num_user_settings += len(parser["values"])
elif container_type in self._ignored_instance_container_types:
@ -348,6 +356,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# To simplify this, only check if the global stack exists or not
global_stack_id = self._stripFileToId(global_stack_file)
serialized = archive.open(global_stack_file).read().decode("utf-8")
serialized = GlobalStack._updateSerialized(serialized, global_stack_file)
machine_name = self._getMachineNameFromSerializedStack(serialized)
self._machine_info.metadata_dict = self._getMetaDataDictFromSerializedStack(serialized)
@ -444,6 +453,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_info.user_changes_info = instance_container_info_dict[user_changes_id]
self._machine_info.extruder_info_dict[position] = extruder_info
intent_id = parser["containers"][str(_ContainerIndexes.Intent)]
if intent_id not in ("empty", "empty_intent"):
extruder_info.intent_info = instance_container_info_dict[intent_id]
if not machine_conflict and containers_found_dict["machine"]:
if position not in global_stack.extruders:
continue
@ -508,6 +521,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._machine_info.definition_id = machine_definition_id
self._machine_info.quality_type = quality_type
self._machine_info.custom_quality_name = quality_name
self._machine_info.intent_category = intent_category
if machine_conflict and not self._is_same_machine_type:
machine_conflict = False
@ -528,6 +542,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._dialog.setNumVisibleSettings(num_visible_settings)
self._dialog.setQualityName(quality_name)
self._dialog.setQualityType(quality_type)
self._dialog.setIntentName(intent_name)
self._dialog.setNumSettingsOverriddenByQualityChanges(num_settings_overridden_by_quality_changes)
self._dialog.setNumUserSettings(num_user_settings)
self._dialog.setActiveMode(active_mode)
@ -571,26 +586,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# \param file_name
@call_on_qt_thread
def read(self, file_name):
container_registry = ContainerRegistry.getInstance()
signals = [container_registry.containerAdded,
container_registry.containerRemoved,
container_registry.containerMetaDataChanged]
#
# We now have different managers updating their lookup tables upon container changes. It is critical to make
# sure that the managers have a complete set of data when they update.
#
# In project loading, lots of the container-related signals are loosely emitted, which can create timing gaps
# for incomplete data update or other kinds of issues to happen.
#
# To avoid this, we postpone all signals so they don't get emitted immediately. But, please also be aware that,
# because of this, do not expect to have the latest data in the lookup tables in project loading.
#
with postponeSignals(*signals, compress = CompressTechnique.NoCompression):
return self._read(file_name)
def _read(self, file_name):
application = CuraApplication.getInstance()
material_manager = application.getMaterialManager()
archive = zipfile.ZipFile(file_name, "r")
@ -688,7 +684,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if self._resolve_strategies["material"] == "override":
# Remove the old materials and then deserialize the one from the project
root_material_id = material_container.getMetaDataEntry("base_file")
material_manager.removeMaterialByRootId(root_material_id)
application.getContainerRegistry().removeContainer(root_material_id)
elif self._resolve_strategies["material"] == "new":
# Note that we *must* deserialize it with a new ID, as multiple containers will be
# auto created & added.
@ -742,9 +738,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if self._machine_info.quality_changes_info is None:
return
application = CuraApplication.getInstance()
quality_manager = application.getQualityManager()
# If we have custom profiles, load them
quality_changes_name = self._machine_info.quality_changes_info.name
if self._machine_info.quality_changes_info is not None:
@ -752,12 +745,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._machine_info.quality_changes_info.name)
# Get the correct extruder definition IDs for quality changes
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
machine_definition_id_for_quality = getMachineDefinitionIDForQualitySearch(global_stack.definition)
machine_definition_id_for_quality = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
machine_definition_for_quality = self._container_registry.findDefinitionContainers(id = machine_definition_id_for_quality)[0]
quality_changes_info = self._machine_info.quality_changes_info
quality_changes_quality_type = quality_changes_info.global_info.parser["metadata"]["quality_type"]
quality_changes_intent_category_per_extruder = {position: info.parser["metadata"].get("intent_category", "default") for position, info in quality_changes_info.extruder_info_dict.items()}
quality_changes_name = quality_changes_info.name
create_new = self._resolve_strategies.get("quality_changes") != "override"
@ -768,13 +761,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
quality_changes_name = self._container_registry.uniqueName(quality_changes_name)
for position, container_info in container_info_dict.items():
extruder_stack = None
intent_category = None # type: Optional[str]
if position is not None:
extruder_stack = global_stack.extruders[position]
container = quality_manager._createQualityChanges(quality_changes_quality_type,
quality_changes_name,
global_stack, extruder_stack)
intent_category = quality_changes_intent_category_per_extruder[position]
container = self._createNewQualityChanges(quality_changes_quality_type, intent_category, quality_changes_name, global_stack, extruder_stack)
container_info.container = container
container.setDirty(True)
self._container_registry.addContainer(container)
Logger.log("d", "Created new quality changes container [%s]", container.getId())
@ -802,11 +794,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if not global_stack.extruders:
ExtruderManager.getInstance().fixSingleExtrusionMachineExtruderDefinition(global_stack)
extruder_stack = global_stack.extruders["0"]
intent_category = quality_changes_intent_category_per_extruder["0"]
container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name,
global_stack, extruder_stack)
container = self._createNewQualityChanges(quality_changes_quality_type, intent_category, quality_changes_name, global_stack, extruder_stack)
container_info.container = container
container.setDirty(True)
self._container_registry.addContainer(container)
Logger.log("d", "Created new quality changes container [%s]", container.getId())
@ -832,10 +823,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if container_info.container is None:
extruder_stack = global_stack.extruders[position]
container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name,
global_stack, extruder_stack)
intent_category = quality_changes_intent_category_per_extruder[position]
container = self._createNewQualityChanges(quality_changes_quality_type, intent_category, quality_changes_name, global_stack, extruder_stack)
container_info.container = container
container.setDirty(True)
self._container_registry.addContainer(container)
for key, value in container_info.parser["values"].items():
@ -843,6 +833,45 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._machine_info.quality_changes_info.name = quality_changes_name
## Helper class to create a new quality changes profile.
#
# This will then later be filled with the appropriate data.
# \param quality_type The quality type of the new profile.
# \param intent_category The intent category of the new profile.
# \param name The name for the profile. This will later be made unique so
# it doesn't need to be unique yet.
# \param global_stack The global stack showing the configuration that the
# profile should be created for.
# \param extruder_stack The extruder stack showing the configuration that
# the profile should be created for. If this is None, it will be created
# for the global stack.
def _createNewQualityChanges(self, quality_type: str, intent_category: Optional[str], name: str, global_stack: GlobalStack, extruder_stack: Optional[ExtruderStack]) -> InstanceContainer:
container_registry = CuraApplication.getInstance().getContainerRegistry()
base_id = global_stack.definition.getId() if extruder_stack is None else extruder_stack.getId()
new_id = base_id + "_" + 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(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[global_stack.definition.getId()].quality_definition
quality_changes.setDefinition(machine_definition_id)
quality_changes.setMetaDataEntry("setting_version", CuraApplication.getInstance().SettingVersion)
quality_changes.setDirty(True)
return quality_changes
@staticmethod
def _clearStack(stack):
application = CuraApplication.getInstance()
@ -903,45 +932,30 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_stack.userChanges.setProperty(key, "value", value)
def _applyVariants(self, global_stack, extruder_stack_dict):
application = CuraApplication.getInstance()
variant_manager = application.getVariantManager()
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
# Take the global variant from the machine info if available.
if self._machine_info.variant_info is not None:
parser = self._machine_info.variant_info.parser
variant_name = parser["general"]["name"]
variant_type = VariantType.BUILD_PLATE
node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type)
if node is not None and node.getContainer() is not None:
global_stack.variant = node.getContainer()
variant_name = self._machine_info.variant_info.parser["general"]["name"]
if variant_name in machine_node.variants:
global_stack.variant = machine_node.variants[variant_name].container
else:
Logger.log("w", "Could not find global variant '{0}'.".format(variant_name))
for position, extruder_stack in extruder_stack_dict.items():
if position not in self._machine_info.extruder_info_dict:
continue
extruder_info = self._machine_info.extruder_info_dict[position]
if extruder_info.variant_info is None:
# If there is no variant_info, try to use the default variant. Otherwise, leave it be.
node = variant_manager.getDefaultVariantNode(global_stack.definition, VariantType.NOZZLE, global_stack)
if node is not None and node.getContainer() is not None:
extruder_stack.variant = node.getContainer()
continue
parser = extruder_info.variant_info.parser
variant_name = parser["general"]["name"]
variant_type = VariantType.NOZZLE
node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type)
if node is not None and node.getContainer() is not None:
extruder_stack.variant = node.getContainer()
# If there is no variant_info, try to use the default variant. Otherwise, any available variant.
node = machine_node.variants.get(machine_node.preferred_variant_name, next(iter(machine_node.variants.values())))
else:
variant_name = extruder_info.variant_info.parser["general"]["name"]
node = ContainerTree.getInstance().machines[global_stack.definition.getId()].variants[variant_name]
extruder_stack.variant = node.container
def _applyMaterials(self, global_stack, extruder_stack_dict):
application = CuraApplication.getInstance()
material_manager = application.getMaterialManager()
# Force update lookup tables first
material_manager.initialize()
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
for position, extruder_stack in extruder_stack_dict.items():
if position not in self._machine_info.extruder_info_dict:
continue
@ -952,18 +966,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
root_material_id = extruder_info.root_material_id
root_material_id = self._old_new_materials.get(root_material_id, root_material_id)
build_plate_id = global_stack.variant.getId()
# get material diameter of this extruder
machine_material_diameter = extruder_stack.getCompatibleMaterialDiameter()
material_node = material_manager.getMaterialNode(global_stack.definition.getId(),
extruder_stack.variant.getName(),
build_plate_id,
machine_material_diameter,
root_material_id)
if material_node is not None and material_node.getContainer() is not None:
extruder_stack.material = material_node.getContainer() # type: InstanceContainer
material_node = machine_node.variants[extruder_stack.variant.getName()].materials[root_material_id]
extruder_stack.material = material_node.container # type: InstanceContainer
def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
# Clear all first
@ -979,10 +983,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# prepare the quality to select
self._quality_changes_to_apply = None
self._quality_type_to_apply = None
self._intent_category_to_apply = None
if self._machine_info.quality_changes_info is not None:
self._quality_changes_to_apply = self._machine_info.quality_changes_info.name
else:
self._quality_type_to_apply = self._machine_info.quality_type
self._intent_category_to_apply = self._machine_info.intent_category
# Set enabled/disabled for extruders
for position, extruder_stack in extruder_stack_dict.items():
@ -1001,12 +1007,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
def _updateActiveMachine(self, global_stack):
# Actually change the active machine.
machine_manager = Application.getInstance().getMachineManager()
material_manager = Application.getInstance().getMaterialManager()
quality_manager = Application.getInstance().getQualityManager()
# Force update the lookup maps first
material_manager.initialize()
quality_manager.initialize()
container_tree = ContainerTree.getInstance()
machine_manager.setActiveMachine(global_stack.getId())
@ -1016,21 +1017,20 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
global_stack.setMetaDataEntry(key, value)
if self._quality_changes_to_apply:
quality_changes_group_dict = quality_manager.getQualityChangesGroups(global_stack)
if self._quality_changes_to_apply not in quality_changes_group_dict:
quality_changes_group_list = container_tree.getCurrentQualityChangesGroups()
quality_changes_group = next((qcg for qcg in quality_changes_group_list if qcg.name == self._quality_changes_to_apply), None)
if not quality_changes_group:
Logger.log("e", "Could not find quality_changes [%s]", self._quality_changes_to_apply)
return
quality_changes_group = quality_changes_group_dict[self._quality_changes_to_apply]
machine_manager.setQualityChangesGroup(quality_changes_group, no_dialog = True)
else:
self._quality_type_to_apply = self._quality_type_to_apply.lower()
quality_group_dict = quality_manager.getQualityGroups(global_stack)
quality_group_dict = container_tree.getCurrentQualityGroups()
if self._quality_type_to_apply in quality_group_dict:
quality_group = quality_group_dict[self._quality_type_to_apply]
else:
Logger.log("i", "Could not find quality type [%s], switch to default", self._quality_type_to_apply)
preferred_quality_type = global_stack.getMetaDataEntry("preferred_quality_type")
quality_group_dict = quality_manager.getQualityGroups(global_stack)
quality_group = quality_group_dict.get(preferred_quality_type)
if quality_group is None:
Logger.log("e", "Could not get preferred quality type [%s]", preferred_quality_type)
@ -1038,6 +1038,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if quality_group is not None:
machine_manager.setQualityGroup(quality_group, no_dialog = True)
# Also apply intent if available
available_intent_category_list = IntentManager.getInstance().currentAvailableIntentCategories()
if self._intent_category_to_apply is not None and self._intent_category_to_apply in available_intent_category_list:
machine_manager.setIntentByCategory(self._intent_category_to_apply)
# Notify everything/one that is to notify about changes.
global_stack.containersChanged.emit(global_stack.getTop())

View file

@ -43,6 +43,7 @@ class WorkspaceDialog(QObject):
self._quality_name = ""
self._num_settings_overridden_by_quality_changes = 0
self._quality_type = ""
self._intent_name = ""
self._machine_name = ""
self._machine_type = ""
self._variant_type = ""
@ -60,6 +61,7 @@ class WorkspaceDialog(QObject):
hasVisibleSettingsFieldChanged = pyqtSignal()
numSettingsOverridenByQualityChangesChanged = pyqtSignal()
qualityTypeChanged = pyqtSignal()
intentNameChanged = pyqtSignal()
machineNameChanged = pyqtSignal()
materialLabelsChanged = pyqtSignal()
objectsOnPlateChanged = pyqtSignal()
@ -166,6 +168,15 @@ class WorkspaceDialog(QObject):
self._quality_name = quality_name
self.qualityNameChanged.emit()
@pyqtProperty(str, notify = intentNameChanged)
def intentName(self) -> str:
return self._intent_name
def setIntentName(self, intent_name: str) -> None:
if self._intent_name != intent_name:
self._intent_name = intent_name
self.intentNameChanged.emit()
@pyqtProperty(str, notify=activeModeChanged)
def activeMode(self):
return self._active_mode

View file

@ -1,10 +1,10 @@
// Copyright (c) 2016 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.1
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import UM 1.1 as UM
@ -24,7 +24,7 @@ UM.Dialog
onClosing: manager.notifyClosed()
onVisibleChanged:
{
if(visible)
if (visible)
{
machineResolveComboBox.currentIndex = 0
qualityChangesResolveComboBox.currentIndex = 0
@ -223,6 +223,21 @@ UM.Dialog
}
}
Row
{
width: parent.width
height: childrenRect.height
Label
{
text: catalog.i18nc("@action:label", "Intent")
width: (parent.width / 3) | 0
}
Label
{
text: manager.intentName
width: (parent.width / 3) | 0
}
}
Row
{
width: parent.width
height: manager.numUserSettings != 0 ? childrenRect.height : 0

View file

@ -8,7 +8,7 @@ from UM.Logger import Logger
from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
from cura.CuraApplication import CuraApplication
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from cura.Machines.ContainerTree import ContainerTree
from cura.ReaderWriters.ProfileReader import ProfileReader
import zipfile
@ -97,7 +97,7 @@ class CuraProfileReader(ProfileReader):
if global_stack is None:
return None
active_quality_definition = getMachineDefinitionIDForQualitySearch(global_stack.definition)
active_quality_definition = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
if profile.getMetaDataEntry("definition") != active_quality_definition:
profile.setMetaDataEntry("definition", active_quality_definition)
return profile

View file

@ -1,4 +1,4 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import re # For escaping characters in the settings.
@ -9,8 +9,7 @@ from UM.Mesh.MeshWriter import MeshWriter
from UM.Logger import Logger
from UM.Application import Application
from UM.Settings.InstanceContainer import InstanceContainer
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from cura.Machines.ContainerTree import ContainerTree
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
@ -117,17 +116,24 @@ class GCodeWriter(MeshWriter):
# \return A serialised string of the settings.
def _serialiseSettings(self, stack):
container_registry = self._application.getContainerRegistry()
quality_manager = self._application.getQualityManager()
prefix = self._setting_keyword + str(GCodeWriter.version) + " " # The prefix to put before each line.
prefix_length = len(prefix)
quality_type = stack.quality.getMetaDataEntry("quality_type")
container_with_profile = stack.qualityChanges
machine_definition_id_for_quality = ContainerTree.getInstance().machines[stack.definition.getId()].quality_definition
if container_with_profile.getId() == "empty_quality_changes":
# If the global quality changes is empty, create a new one
quality_name = container_registry.uniqueName(stack.quality.getName())
container_with_profile = quality_manager._createQualityChanges(quality_type, quality_name, stack, None)
quality_id = container_registry.uniqueName((stack.definition.getId() + "_" + quality_name).lower().replace(" ", "_"))
container_with_profile = InstanceContainer(quality_id)
container_with_profile.setName(quality_name)
container_with_profile.setMetaDataEntry("type", "quality_changes")
container_with_profile.setMetaDataEntry("quality_type", quality_type)
if stack.getMetaDataEntry("position") is not None: # For extruder stacks, the quality changes should include an intent category.
container_with_profile.setMetaDataEntry("intent_category", stack.intent.getMetaDataEntry("intent_category", "default"))
container_with_profile.setDefinition(machine_definition_id_for_quality)
flat_global_container = self._createFlattenedContainerInstance(stack.userChanges, container_with_profile)
# If the quality changes is not set, we need to set type manually
@ -139,7 +145,6 @@ class GCodeWriter(MeshWriter):
flat_global_container.setMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal"))
# Get the machine definition ID for quality profiles
machine_definition_id_for_quality = getMachineDefinitionIDForQualitySearch(stack.definition)
flat_global_container.setMetaDataEntry("definition", machine_definition_id_for_quality)
serialized = flat_global_container.serialize()
@ -151,7 +156,12 @@ class GCodeWriter(MeshWriter):
if extruder_quality.getId() == "empty_quality_changes":
# Same story, if quality changes is empty, create a new one
quality_name = container_registry.uniqueName(stack.quality.getName())
extruder_quality = quality_manager._createQualityChanges(quality_type, quality_name, stack, None)
quality_id = container_registry.uniqueName((stack.definition.getId() + "_" + quality_name).lower().replace(" ", "_"))
extruder_quality = InstanceContainer(quality_id)
extruder_quality.setName(quality_name)
extruder_quality.setMetaDataEntry("type", "quality_changes")
extruder_quality.setMetaDataEntry("quality_type", quality_type)
extruder_quality.setDefinition(machine_definition_id_for_quality)
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.userChanges, extruder_quality)
# If the quality changes is not set, we need to set type manually

View file

@ -11,7 +11,9 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Util import parseBool
import cura.CuraApplication # Imported like this to prevent circular dependencies.
from cura.MachineAction import MachineAction
from cura.Machines.ContainerTree import ContainerTree # To re-build the machine node when hasMaterials changes.
from cura.Settings.CuraStackBuilder import CuraStackBuilder
from cura.Settings.cura_empty_instance_containers import isEmptyContainer
@ -41,6 +43,9 @@ class MachineSettingsAction(MachineAction):
self._backend = self._application.getBackend()
self.onFinished.connect(self._onFinished)
# If the g-code flavour changes between UltiGCode and another flavour, we need to update the container tree.
self._application.globalContainerStackChanged.connect(self._updateHasMaterialsInContainerTree)
# Which container index in a stack to store machine setting changes.
@pyqtProperty(int, constant = True)
def storeContainerIndex(self) -> int:
@ -51,6 +56,18 @@ class MachineSettingsAction(MachineAction):
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
## Triggered when the global container stack changes or when the g-code
# flavour setting is changed.
def _updateHasMaterialsInContainerTree(self) -> None:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
if machine_node.has_materials != parseBool(global_stack.getMetaDataEntry("has_materials")): # May have changed due to the g-code flavour.
machine_node.has_materials = parseBool(global_stack.getMetaDataEntry("has_materials"))
machine_node._loadAll()
def _reset(self):
global_stack = self._application.getMachineManager().activeMachine
if not global_stack:
@ -98,11 +115,8 @@ class MachineSettingsAction(MachineAction):
return
machine_manager = self._application.getMachineManager()
material_manager = self._application.getMaterialManager()
extruder_positions = list(global_stack.extruders.keys())
has_materials = global_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
material_node = None
if has_materials:
global_stack.setMetaDataEntry("has_materials", True)
else:
@ -111,11 +125,15 @@ class MachineSettingsAction(MachineAction):
if "has_materials" in global_stack.getMetaData():
global_stack.removeMetaDataEntry("has_materials")
self._updateHasMaterialsInContainerTree()
# set materials
for position in extruder_positions:
if has_materials:
material_node = material_manager.getDefaultMaterial(global_stack, position, None)
machine_manager.setMaterial(position, material_node)
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
for position, extruder in enumerate(global_stack.extruderList):
#Find out what material we need to default to.
approximate_diameter = round(extruder.getProperty("material_diameter", "value"))
material_node = machine_node.variants[extruder.variant.getName()].preferredMaterial(approximate_diameter)
machine_manager.setMaterial(str(position), material_node)
self._application.globalContainerStackChanged.emit()

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Toolbox is released under the terms of the LGPLv3 or higher.
import json
@ -19,6 +19,7 @@ from UM.Version import Version
from cura import ApplicationMetadata
from cura import UltimakerCloudAuthentication
from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerTree import ContainerTree
from .AuthorsModel import AuthorsModel
from .PackagesModel import PackagesModel
@ -359,15 +360,22 @@ class Toolbox(QObject, Extension):
@pyqtSlot()
def resetMaterialsQualitiesAndUninstall(self) -> None:
application = CuraApplication.getInstance()
material_manager = application.getMaterialManager()
quality_manager = application.getQualityManager()
machine_manager = application.getMachineManager()
container_tree = ContainerTree.getInstance()
for global_stack, extruder_nr, container_id in self._package_used_materials:
default_material_node = material_manager.getDefaultMaterial(global_stack, extruder_nr, global_stack.extruders[extruder_nr].variant.getName())
extruder = global_stack.extruderList[int(extruder_nr)]
approximate_diameter = extruder.getApproximateMaterialDiameter()
variant_node = container_tree.machines[global_stack.definition.getId()].variants[extruder.variant.getName()]
default_material_node = variant_node.preferredMaterial(approximate_diameter)
machine_manager.setMaterial(extruder_nr, default_material_node, global_stack = global_stack)
for global_stack, extruder_nr, container_id in self._package_used_qualities:
default_quality_group = quality_manager.getDefaultQualityType(global_stack)
variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList]
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList]
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
definition_id = global_stack.definition.getId()
machine_node = container_tree.machines[definition_id]
default_quality_group = machine_node.getQualityGroups(variant_names, material_bases, extruder_enabled)[machine_node.preferred_quality_type]
machine_manager.setQualityGroup(default_quality_group, global_stack = global_stack)
if self._package_id_to_uninstall is not None:

View file

@ -1,4 +1,4 @@
#Copyright (c) 2018 Ultimaker B.V.
#Copyright (c) 2019 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher.
from typing import cast
@ -7,13 +7,13 @@ from Charon.VirtualFile import VirtualFile #To open UFP files.
from Charon.OpenMode import OpenMode #To indicate that we want to write to UFP files.
from io import StringIO #For converting g-code to bytes.
from UM.Application import Application
from UM.Logger import Logger
from UM.Mesh.MeshWriter import MeshWriter #The writer we need to implement.
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from UM.PluginRegistry import PluginRegistry #To get the g-code writer.
from PyQt5.QtCore import QBuffer
from cura.CuraApplication import CuraApplication
from cura.Snapshot import Snapshot
from cura.Utils.Threading import call_on_qt_thread
@ -83,9 +83,9 @@ class UFPWriter(MeshWriter):
Logger.log("d", "Thumbnail not created, cannot save it")
# Store the material.
application = Application.getInstance()
application = CuraApplication.getInstance()
machine_manager = application.getMachineManager()
material_manager = application.getMaterialManager()
container_registry = application.getContainerRegistry()
global_stack = machine_manager.activeMachine
material_extension = "xml.fdm_material"
@ -111,12 +111,12 @@ class UFPWriter(MeshWriter):
continue
material_root_id = material.getMetaDataEntry("base_file")
material_group = material_manager.getMaterialGroup(material_root_id)
if material_group is None:
Logger.log("e", "Cannot find material container with root id [%s]", material_root_id)
material_root_query = container_registry.findContainers(id = material_root_id)
if not material_root_query:
Logger.log("e", "Cannot find material container with root id {root_id}".format(root_id = material_root_id))
return False
material_container = material_root_query[0]
material_container = material_group.root_material_node.getContainer()
try:
serialized_material = material_container.serialize()
except NotImplementedError:

View file

@ -291,8 +291,8 @@ Cura.MachineAction
MessageDialog
{
id: invalidIPAddressMessageDialog
x: (parent.x + (parent.width) / 2) | 0
y: (parent.y + (parent.height) / 2) | 0
x: parent ? (parent.x + (parent.width) / 2) : 0
y: parent ? (parent.y + (parent.height) / 2) : 0
title: catalog.i18nc("@title:window", "Invalid IP address")
text: catalog.i18nc("@text", "Please enter a valid IP address.")
icon: StandardIcon.Warning

View file

@ -1,8 +1,9 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from cura.CuraApplication import CuraApplication
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
from ..BaseModel import BaseModel
@ -24,32 +25,27 @@ class ClusterPrinterConfigurationMaterial(BaseModel):
self.material = material
super().__init__(**kwargs)
## Creates a material output model based on this cloud printer material.
## Creates a material output model based on this cloud printer material.
#
# A material is chosen that matches the current GUID. If multiple such
# materials are available, read-only materials are preferred and the
# material with the earliest alphabetical name will be selected.
# \return A material output model that matches the current GUID.
def createOutputModel(self) -> MaterialOutputModel:
material_manager = CuraApplication.getInstance().getMaterialManager()
material_group_list = material_manager.getMaterialGroupListByGUID(self.guid) or []
# Sort the material groups by "is_read_only = True" first, and then the name alphabetically.
read_only_material_group_list = list(filter(lambda x: x.is_read_only, material_group_list))
non_read_only_material_group_list = list(filter(lambda x: not x.is_read_only, material_group_list))
material_group = None
if read_only_material_group_list:
read_only_material_group_list = sorted(read_only_material_group_list, key = lambda x: x.name)
material_group = read_only_material_group_list[0]
elif non_read_only_material_group_list:
non_read_only_material_group_list = sorted(non_read_only_material_group_list, key = lambda x: x.name)
material_group = non_read_only_material_group_list[0]
if material_group:
container = material_group.root_material_node.getContainer()
color = container.getMetaDataEntry("color_code")
brand = container.getMetaDataEntry("brand")
material_type = container.getMetaDataEntry("material")
name = container.getName()
container_registry = ContainerRegistry.getInstance()
same_guid = container_registry.findInstanceContainersMetadata(GUID = self.guid)
if same_guid:
read_only = sorted(filter(lambda metadata: container_registry.isReadOnly(metadata["id"]), same_guid), key = lambda metadata: metadata["name"])
if read_only:
material_metadata = read_only[0]
else:
material_metadata = min(same_guid, key = lambda metadata: metadata["name"])
else:
color = self.color
brand = self.brand
material_type = self.material
name = "Empty" if self.material == "empty" else "Unknown"
material_metadata = {
"color_code": self.color,
"brand": self.brand,
"material": self.material,
"name": "Empty" if self.material == "empty" else "Unknown"
}
return MaterialOutputModel(guid=self.guid, type=material_type, brand=brand, color=color, name=name)
return MaterialOutputModel(guid = self.guid, type = material_metadata["material"], brand = material_metadata["brand"], color = material_metadata["color_code"], name = material_metadata["name"])

View file

@ -6,6 +6,7 @@ from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from UM.Job import Job
from UM.Logger import Logger
from UM.Settings import ContainerRegistry
from cura.CuraApplication import CuraApplication
from ..Models.Http.ClusterMaterial import ClusterMaterial
@ -67,10 +68,10 @@ class SendMaterialJob(Job):
# \param materials_to_send A set with id's of materials that must be sent.
def _sendMaterials(self, materials_to_send: Set[str]) -> None:
container_registry = CuraApplication.getInstance().getContainerRegistry()
material_manager = CuraApplication.getInstance().getMaterialManager()
material_group_dict = material_manager.getAllMaterialGroups()
all_materials = container_registry.findInstanceContainersMetadata(type = "material")
all_base_files = {material["base_file"] for material in all_materials if "base_file" in material} # Filters out uniques by making it a set. Don't include files without base file (i.e. empty material).
for root_material_id in material_group_dict:
for root_material_id in all_base_files:
if root_material_id not in materials_to_send:
# If the material does not have to be sent we skip it.
continue
@ -127,20 +128,18 @@ class SendMaterialJob(Job):
@staticmethod
def _getLocalMaterials() -> Dict[str, LocalMaterial]:
result = {} # type: Dict[str, LocalMaterial]
material_manager = CuraApplication.getInstance().getMaterialManager()
material_group_dict = material_manager.getAllMaterialGroups()
all_materials = CuraApplication.getInstance().getContainerRegistry().findInstanceContainersMetadata(type = "material")
all_base_files = [material for material in all_materials if material["id"] == material.get("base_file")] # Don't send materials without base_file: The empty material doesn't need to be sent.
# Find the latest version of all material containers in the registry.
for root_material_id, material_group in material_group_dict.items():
material_metadata = material_group.root_material_node.getMetadata()
for material_metadata in all_base_files:
try:
# material version must be an int
material_metadata["version"] = int(material_metadata["version"])
# Create a new local material
local_material = LocalMaterial(**material_metadata)
local_material.id = root_material_id
local_material.id = material_metadata["id"]
if local_material.GUID not in result or \
local_material.GUID not in result or \

View file

@ -1,76 +0,0 @@
# Copyright (c) 2018 Ultimaker B.V.
# Uranium is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog
from UM.Application import Application
from UM.Util import parseBool
from cura.MachineAction import MachineAction
catalog = i18nCatalog("cura")
## The Ultimaker 2 can have a few revisions & upgrades.
class UM2UpgradeSelection(MachineAction):
def __init__(self):
super().__init__("UM2UpgradeSelection", catalog.i18nc("@action", "Select upgrades"))
self._qml_url = "UM2UpgradeSelectionMachineAction.qml"
self._container_registry = ContainerRegistry.getInstance()
self._current_global_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._reset()
def _reset(self):
self.hasVariantsChanged.emit()
def _onGlobalStackChanged(self):
if self._current_global_stack:
self._current_global_stack.metaDataChanged.disconnect(self._onGlobalStackMetaDataChanged)
self._current_global_stack = Application.getInstance().getGlobalContainerStack()
if self._current_global_stack:
self._current_global_stack.metaDataChanged.connect(self._onGlobalStackMetaDataChanged)
self._reset()
def _onGlobalStackMetaDataChanged(self):
self._reset()
hasVariantsChanged = pyqtSignal()
def setHasVariants(self, has_variants = True):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
variant_container = global_container_stack.extruders["0"].variant
if has_variants:
global_container_stack.setMetaDataEntry("has_variants", True)
# Set the variant container to a sane default
empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer()
if type(variant_container) == type(empty_container):
search_criteria = { "type": "variant", "definition": "ultimaker2", "id": "*0.4*" }
containers = self._container_registry.findInstanceContainers(**search_criteria)
if containers:
global_container_stack.extruders["0"].variant = containers[0]
else:
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
if "has_variants" in global_container_stack.getMetaData():
global_container_stack.removeMetaDataEntry("has_variants")
# Set the variant container to an empty variant
global_container_stack.extruders["0"].variant = ContainerRegistry.getInstance().getEmptyInstanceContainer()
Application.getInstance().globalContainerStackChanged.emit()
self._reset()
@pyqtProperty(bool, fset = setHasVariants, notify = hasVariantsChanged)
def hasVariants(self):
if self._current_global_stack:
return parseBool(self._current_global_stack.getMetaDataEntry("has_variants", "false"))

View file

@ -1,55 +0,0 @@
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.3 as UM
import Cura 1.1 as Cura
Cura.MachineAction
{
UM.I18nCatalog { id: catalog; name: "cura"; }
anchors.fill: parent
Item
{
id: upgradeSelectionMachineAction
anchors.fill: parent
anchors.topMargin: UM.Theme.getSize("default_margin").width * 5
anchors.leftMargin: UM.Theme.getSize("default_margin").width * 4
Label
{
id: pageDescription
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Please select any upgrades made to this Ultimaker 2.")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
Cura.CheckBox
{
id: olssonBlockCheckBox
anchors.top: pageDescription.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
height: UM.Theme.getSize("setting_control").height
text: catalog.i18nc("@label", "Olsson Block")
checked: manager.hasVariants
onClicked: manager.hasVariants = checked
Connections
{
target: manager
onHasVariantsChanged: olssonBlockCheckBox.checked = manager.hasVariants
}
}
}
}

View file

@ -1,9 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import BedLevelMachineAction
from . import UMOUpgradeSelection
from . import UM2UpgradeSelection
def getMetaData():
return {}
@ -11,6 +10,5 @@ def getMetaData():
def register(app):
return { "machine_action": [
BedLevelMachineAction.BedLevelMachineAction(),
UMOUpgradeSelection.UMOUpgradeSelection(),
UM2UpgradeSelection.UM2UpgradeSelection()
UMOUpgradeSelection.UMOUpgradeSelection()
]}

View file

@ -56,4 +56,4 @@ def getMetaData() -> Dict[str, Any]:
def register(app: "Application") -> Dict[str, Any]:
return { "version_upgrade": upgrade }
return { "version_upgrade": upgrade }

View file

@ -4,6 +4,14 @@ import io
from UM.VersionUpgrade import VersionUpgrade
_renamed_container_id_map = {
"ultimaker2_0.25": "ultimaker2_olsson_0.25",
"ultimaker2_0.4": "ultimaker2_olsson_0.4",
"ultimaker2_0.6": "ultimaker2_olsson_0.6",
"ultimaker2_0.8": "ultimaker2_olsson_0.8",
"ultimaker2_extended_0.25": "ultimaker2_extended_olsson_0.25",
"ultimaker2_extended_0.4": "ultimaker2_extended_olsson_0.4",
"ultimaker2_extended_0.6": "ultimaker2_extended_olsson_0.6",
"ultimaker2_extended_0.8": "ultimaker2_extended_olsson_0.8",
# HMS434 "extra coarse", "super coarse", and "ultra coarse" are removed.
"hms434_global_Extra_Coarse_Quality": "hms434_global_Normal_Quality",
"hms434_global_Super_Coarse_Quality": "hms434_global_Normal_Quality",
@ -49,6 +57,10 @@ class VersionUpgrade43to44(VersionUpgrade):
# Update version number.
parser["metadata"]["setting_version"] = "10"
# Intent profiles were added, so the quality changes should match with no intent (so "default")
if parser["metadata"].get("type", "") == "quality_changes":
parser["metadata"]["intent_category"] = "default"
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
@ -64,6 +76,29 @@ class VersionUpgrade43to44(VersionUpgrade):
parser["metadata"]["setting_version"] = "10"
if "containers" in parser:
# With the ContainerTree refactor, UM2 with Olsson block got moved to a separate definition.
if "6" in parser["containers"]:
if parser["containers"]["6"] == "ultimaker2":
if "metadata" in parser and "has_variants" in parser["metadata"] and parser["metadata"]["has_variants"] == "True": # This is an Olsson block upgraded UM2!
parser["containers"]["6"] = "ultimaker2_olsson"
del parser["metadata"]["has_variants"]
elif parser["containers"]["6"] == "ultimaker2_extended":
if "metadata" in parser and "has_variants" in parser["metadata"] and parser["metadata"]["has_variants"] == "True": # This is an Olsson block upgraded UM2E!
parser["containers"]["6"] = "ultimaker2_extended_olsson"
del parser["metadata"]["has_variants"]
# We should only have 6 levels when we start.
if "7" in parser["containers"]:
return ([], [])
# We added the intent container in Cura 4.4. This means that all other containers move one step down.
parser["containers"]["7"] = parser["containers"]["6"]
parser["containers"]["6"] = parser["containers"]["5"]
parser["containers"]["5"] = parser["containers"]["4"]
parser["containers"]["4"] = parser["containers"]["3"]
parser["containers"]["3"] = parser["containers"]["2"]
parser["containers"]["2"] = "empty_intent"
# Update renamed containers
for key, value in parser["containers"].items():
if value in _renamed_container_id_map:

View file

@ -0,0 +1,36 @@
import configparser
import VersionUpgrade43to44
before_update = """[general]
version = 4
name = Ultimaker 3
id = Ultimaker 3
[metadata]
type = machine
[containers]
0 = user_profile
1 = quality_changes
2 = quality
3 = material
4 = variant
5 = definition_changes
6 = definition
"""
def test_upgrade():
upgrader = VersionUpgrade43to44.VersionUpgrade43to44()
file_name, new_data = upgrader.upgradeStack(before_update, "whatever")
parser = configparser.ConfigParser(interpolation=None)
parser.read_string(new_data[0])
assert parser["containers"]["0"] == "user_profile"
assert parser["containers"]["1"] == "quality_changes"
assert parser["containers"]["2"] == "empty_intent"
assert parser["containers"]["3"] == "quality"
assert parser["containers"]["4"] == "material"
assert parser["containers"]["5"] == "variant"
assert parser["containers"]["6"] == "definition_changes"
assert parser["containers"]["7"] == "definition"

View file

@ -17,6 +17,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.VariantType import VariantType
try:
@ -74,37 +75,20 @@ class XmlMaterialProfile(InstanceContainer):
if k in self.__material_properties_setting_map:
new_setting_values_dict[self.__material_properties_setting_map[k]] = v
# Prevent recursion
if not apply_to_all:
super().setMetaDataEntry(key, value)
if not apply_to_all: # Historical: If you only want to modify THIS container. We only used that to prevent recursion but with the below code that's no longer necessary.
container_query = registry.findContainers(id = self.getId())
else:
container_query = registry.findContainers(base_file = self.getMetaDataEntry("base_file"))
for container in container_query:
if key not in container.getMetaData() or container.getMetaData()[key] != value:
container.getMetaData()[key] = value
container.setDirty(True)
container.metaDataChanged.emit(container)
for k, v in new_setting_values_dict.items():
self.setProperty(k, "value", v)
return
# Get the MaterialGroup
material_manager = CuraApplication.getInstance().getMaterialManager()
root_material_id = self.getMetaDataEntry("base_file") #if basefile is self.getId, this is a basefile.
material_group = material_manager.getMaterialGroup(root_material_id)
if not material_group: #If the profile is not registered in the registry but loose/temporary, it will not have a base file tree.
super().setMetaDataEntry(key, value)
for k, v in new_setting_values_dict.items():
self.setProperty(k, "value", v)
return
# Update the root material container
root_material_container = material_group.root_material_node.getContainer()
if root_material_container is not None:
root_material_container.setMetaDataEntry(key, value, apply_to_all = False)
for k, v in new_setting_values_dict.items():
root_material_container.setProperty(k, "value", v)
# Update all containers derived from it
for node in material_group.derived_material_node_list:
container = node.getContainer()
if container is not None:
container.setMetaDataEntry(key, value, apply_to_all = False)
for k, v in new_setting_values_dict.items():
container.setProperty(k, "value", v)
## Overridden from InstanceContainer, similar to setMetaDataEntry.
# without this function the setName would only set the name of the specific nozzle / material / machine combination container
# The function is a bit tricky. It will not set the name of all containers if it has the correct name itself.
@ -225,10 +209,8 @@ class XmlMaterialProfile(InstanceContainer):
for instance in self.findInstances():
self._addSettingElement(builder, instance)
machine_container_map = {} # type: Dict[str, InstanceContainer]
machine_variant_map = {} # type: Dict[str, Dict[str, Any]]
variant_manager = CuraApplication.getInstance().getVariantManager()
machine_container_map = {} # type: Dict[str, InstanceContainer]
machine_variant_map = {} # type: Dict[str, Dict[str, Any]]
root_material_id = self.getMetaDataEntry("base_file") # if basefile is self.getId, this is a basefile.
all_containers = registry.findInstanceContainers(base_file = root_material_id)
@ -245,13 +227,13 @@ class XmlMaterialProfile(InstanceContainer):
machine_variant_map[definition_id] = {}
variant_name = container.getMetaDataEntry("variant_name")
if variant_name:
variant_dict = {"variant_node": variant_manager.getVariantNode(definition_id, variant_name),
"material_container": container}
machine_variant_map[definition_id][variant_name] = variant_dict
if not variant_name:
machine_container_map[definition_id] = container
continue
machine_container_map[definition_id] = container
variant_dict = {"variant_type": container.getMetaDataEntry("hardware_type", "nozzle"),
"material_container": container}
machine_variant_map[definition_id][variant_name] = variant_dict
# Map machine human-readable names to IDs
product_id_map = self.getProductIdMap()
@ -284,8 +266,7 @@ class XmlMaterialProfile(InstanceContainer):
# Find all hotend sub-profiles corresponding to this material and machine and add them to this profile.
buildplate_dict = {} # type: Dict[str, Any]
for variant_name, variant_dict in machine_variant_map[definition_id].items():
variant_type = variant_dict["variant_node"].getMetaDataEntry("hardware_type", str(VariantType.NOZZLE))
variant_type = VariantType(variant_type)
variant_type = VariantType(variant_dict["variant_type"])
if variant_type == VariantType.NOZZLE:
# The hotend identifier is not the containers name, but its "name".
builder.start("hotend", {"id": variant_name})
@ -348,7 +329,7 @@ class XmlMaterialProfile(InstanceContainer):
stream = io.BytesIO()
tree = ET.ElementTree(root)
# this makes sure that the XML header states encoding="utf-8"
tree.write(stream, encoding = "utf-8", xml_declaration=True)
tree.write(stream, encoding = "utf-8", xml_declaration = True)
return stream.getvalue().decode("utf-8")
@ -699,32 +680,6 @@ class XmlMaterialProfile(InstanceContainer):
if is_new_material:
containers_to_add.append(new_material)
# Find the buildplates compatibility
buildplates = machine.iterfind("./um:buildplate", self.__namespaces)
buildplate_map = {}
buildplate_map["buildplate_compatible"] = {}
buildplate_map["buildplate_recommended"] = {}
for buildplate in buildplates:
buildplate_id = buildplate.get("id")
if buildplate_id is None:
continue
variant_manager = CuraApplication.getInstance().getVariantManager()
variant_node = variant_manager.getVariantNode(machine_id, buildplate_id,
variant_type = VariantType.BUILD_PLATE)
if not variant_node:
continue
_, buildplate_unmapped_settings_dict = self._getSettingsDictForNode(buildplate)
buildplate_compatibility = buildplate_unmapped_settings_dict.get("hardware compatible",
machine_compatibility)
buildplate_recommended = buildplate_unmapped_settings_dict.get("hardware recommended",
machine_compatibility)
buildplate_map["buildplate_compatible"][buildplate_id] = buildplate_compatibility
buildplate_map["buildplate_recommended"][buildplate_id] = buildplate_recommended
hotends = machine.iterfind("./um:hotend", self.__namespaces)
for hotend in hotends:
# The "id" field for hotends in material profiles is actually name
@ -732,11 +687,6 @@ class XmlMaterialProfile(InstanceContainer):
if hotend_name is None:
continue
variant_manager = CuraApplication.getInstance().getVariantManager()
variant_node = variant_manager.getVariantNode(machine_id, hotend_name, VariantType.NOZZLE)
if not variant_node:
continue
hotend_mapped_settings, hotend_unmapped_settings = self._getSettingsDictForNode(hotend)
hotend_compatibility = hotend_unmapped_settings.get("hardware compatible", machine_compatibility)
@ -760,9 +710,6 @@ class XmlMaterialProfile(InstanceContainer):
new_hotend_material.getMetaData()["compatible"] = hotend_compatibility
new_hotend_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
new_hotend_material.getMetaData()["definition"] = machine_id
if buildplate_map["buildplate_compatible"]:
new_hotend_material.getMetaData()["buildplate_compatible"] = buildplate_map["buildplate_compatible"]
new_hotend_material.getMetaData()["buildplate_recommended"] = buildplate_map["buildplate_recommended"]
cached_hotend_setting_properties = cached_machine_setting_properties.copy()
cached_hotend_setting_properties.update(hotend_mapped_settings)
@ -774,61 +721,6 @@ class XmlMaterialProfile(InstanceContainer):
if is_new_material:
containers_to_add.append(new_hotend_material)
#
# Build plates in hotend
#
buildplates = hotend.iterfind("./um:buildplate", self.__namespaces)
for buildplate in buildplates:
# The "id" field for buildplate in material profiles is actually name
buildplate_name = buildplate.get("id")
if buildplate_name is None:
continue
variant_manager = CuraApplication.getInstance().getVariantManager()
variant_node = variant_manager.getVariantNode(machine_id, buildplate_name, VariantType.BUILD_PLATE)
if not variant_node:
continue
buildplate_mapped_settings, buildplate_unmapped_settings = self._getSettingsDictForNode(buildplate)
buildplate_compatibility = buildplate_unmapped_settings.get("hardware compatible",
buildplate_map["buildplate_compatible"])
buildplate_recommended = buildplate_unmapped_settings.get("hardware recommended",
buildplate_map["buildplate_recommended"])
# Generate container ID for the hotend-and-buildplate-specific material container
new_hotend_and_buildplate_specific_material_id = new_hotend_specific_material_id + "_" + buildplate_name.replace(" ", "_")
# Same as machine compatibility, keep the derived material containers consistent with the parent material
if ContainerRegistry.getInstance().isLoaded(new_hotend_and_buildplate_specific_material_id):
new_hotend_and_buildplate_material = ContainerRegistry.getInstance().findContainers(id = new_hotend_and_buildplate_specific_material_id)[0]
is_new_material = False
else:
new_hotend_and_buildplate_material = XmlMaterialProfile(new_hotend_and_buildplate_specific_material_id)
is_new_material = True
new_hotend_and_buildplate_material.setMetaData(copy.deepcopy(new_hotend_material.getMetaData()))
new_hotend_and_buildplate_material.getMetaData()["id"] = new_hotend_and_buildplate_specific_material_id
new_hotend_and_buildplate_material.getMetaData()["name"] = self.getName()
new_hotend_and_buildplate_material.getMetaData()["variant_name"] = hotend_name
new_hotend_and_buildplate_material.getMetaData()["buildplate_name"] = buildplate_name
new_hotend_and_buildplate_material.setDefinition(machine_id)
# Don't use setMetadata, as that overrides it for all materials with same base file
new_hotend_and_buildplate_material.getMetaData()["compatible"] = buildplate_compatibility
new_hotend_and_buildplate_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
new_hotend_and_buildplate_material.getMetaData()["definition"] = machine_id
new_hotend_and_buildplate_material.getMetaData()["buildplate_compatible"] = buildplate_compatibility
new_hotend_and_buildplate_material.getMetaData()["buildplate_recommended"] = buildplate_recommended
cached_hotend_and_buildplate_setting_properties = cached_hotend_setting_properties.copy()
cached_hotend_and_buildplate_setting_properties.update(buildplate_mapped_settings)
new_hotend_and_buildplate_material.setCachedValues(cached_hotend_and_buildplate_setting_properties)
new_hotend_and_buildplate_material._dirty = False
if is_new_material:
containers_to_add.append(new_hotend_and_buildplate_material)
# there is only one ID for a machine. Once we have reached here, it means we have already found
# a workable ID for that machine, so there is no need to continue
break

View file

@ -815,6 +815,23 @@
}
}
},
"VersionUpgrade43to44": {
"package_info": {
"package_id": "VersionUpgrade43to44",
"package_type": "plugin",
"display_name": "Version Upgrade 4.3 to 4.4",
"description": "Upgrades configurations from Cura 4.3 to Cura 4.4.",
"package_version": "1.0.0",
"sdk_version": "6.0.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
"display_name": "Ultimaker B.V.",
"email": "plugins@ultimaker.com",
"website": "https://ultimaker.com"
}
}
},
"X3DReader": {
"package_info": {
"package_id": "X3DReader",

View file

@ -11,6 +11,8 @@
"file_formats": "text/x-gcode;application/x-stl-ascii;application/x-stl-binary;application/x-wavefront-obj;application/x3g",
"visible": false,
"has_materials": true,
"has_variants": false,
"has_machine_quality": false,
"preferred_material": "generic_pla",
"preferred_quality_type": "normal",
"machine_extruder_trains":

View file

@ -9,7 +9,6 @@
"file_formats": "text/x-gcode",
"has_materials": true,
"has_machine_materials": true,
"preferred_material": "generic_pla",
"exclude_materials": [
"chromatik_pla",

View file

@ -14,8 +14,6 @@
"has_materials": false,
"has_machine_quality": true,
"preferred_variant_name": "0.4 mm",
"first_start_actions": ["UM2UpgradeSelection"],
"supported_actions":["UM2UpgradeSelection"],
"machine_extruder_trains":
{
"0": "ultimaker2_extruder_0"

View file

@ -0,0 +1,12 @@
{
"version": 2,
"name": "Ultimaker 2 Extended with Olsson",
"inherits": "ultimaker2_extended",
"metadata": {
"has_variants": true
},
"overrides": {
"machine_name": { "default_value": "Ultimaker 2 Extended with Olsson" }
}
}

View file

@ -11,7 +11,6 @@
"platform": "ultimaker2go_platform.obj",
"platform_texture": "Ultimaker2Gobackplate.png",
"platform_offset": [0, 0, 0],
"first_start_actions": [],
"machine_extruder_trains":
{
"0": "ultimaker2_go_extruder_0"

View file

@ -0,0 +1,13 @@
{
"version": 2,
"name": "Ultimaker 2 with Olsson Block",
"inherits": "ultimaker2",
"metadata": {
"has_variants": true,
"quality_definition": "ultimaker2"
},
"overrides": {
"machine_name": { "default_value": "Ultimaker 2 with Olsson Block" }
}
}

View file

@ -1,10 +1,10 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.1
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import UM 1.2 as UM
import Cura 1.0 as Cura
@ -139,22 +139,6 @@ UM.Dialog
}
}
}
Row
{
visible: Cura.MachineManager.hasVariantBuildplates
width: parent.width
height: childrenRect.height
Label
{
text: catalog.i18nc("@action:label", "Build plate")
width: Math.floor(scroll.width / 3) | 0
}
Label
{
text: Cura.activeStack != null ? Cura.MachineManager.activeStack.variant.name : ""
width: Math.floor(scroll.width / 3) | 0
}
}
Repeater
{
width: parent.width
@ -256,6 +240,23 @@ UM.Dialog
width: Math.floor(scroll.width / 3) | 0
}
}
// Intent
Row
{
width: parent.width
height: childrenRect.height
Label
{
text: catalog.i18nc("@action:label", "Intent")
width: Math.floor(scroll.width / 3) | 0
}
Label
{
text: Cura.MachineManager.activeIntentCategory
width: Math.floor(scroll.width / 3) | 0
}
}
}
}
}

View file

@ -0,0 +1,65 @@
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import UM 1.2 as UM
// The labelBar shows a set of labels that are evenly spaced from one another.
// The first item is aligned to the left, the last is aligned to the right.
// It's intended to be used together with RadioCheckBar. As such, it needs
// to know what the used itemSize is, so it can ensure the labels are aligned correctly.
Item
{
id: base
property var model: null
property string modelKey: ""
property int itemSize: 14
height: childrenRect.height
RowLayout
{
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
Repeater
{
id: repeater
model: base.model
Item
{
Layout.fillWidth: true
Layout.maximumWidth: Math.round(index + 1 === repeater.count || repeater.count <= 1 ? itemSize : base.width / (repeater.count - 1))
height: label.height
Label
{
id: label
text: model[modelKey]
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
height: contentHeight
anchors
{
// Some magic to ensure that the items are aligned properly.
// We want the following:
// First item should be aligned to the left, no margin.
// Last item should be aligned to the right, no margin.
// The middle item(s) should be aligned to the center of the "item" it's showing (hence half the itemsize as offset).
// We want the center of the label to align with the center of the item, so we negatively offset by half the contentWidth
right: index + 1 === repeater.count ? parent.right: undefined
left: index + 1 === repeater.count || index === 0 ? undefined: parent.left
leftMargin: Math.round((itemSize - contentWidth) * 0.5)
// For some reason, the last label in the row gets misaligned with Qt 5.10. This lines seems to
// fix it.
verticalCenter: parent.verticalCenter
}
}
}
}
}
}

View file

@ -1,36 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 1.4
import UM 1.2 as UM
import Cura 1.0 as Cura
Menu
{
id: menu
title: "Build plate"
property var buildPlateModel: CuraApplication.getBuildPlateModel()
Instantiator
{
model: menu.buildPlateModel
MenuItem {
text: model.name
checkable: true
checked: model.name == Cura.MachineManager.globalVariantName
exclusiveGroup: group
onTriggered: {
Cura.MachineManager.setGlobalVariant(model.container_node);
}
}
onObjectAdded: menu.insertItem(index, object)
onObjectRemoved: menu.removeItem(object)
}
ExclusiveGroup { id: group }
}

View file

@ -1,8 +1,8 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Controls.Styles 1.4
import UM 1.2 as UM
@ -99,12 +99,14 @@ Cura.ExpandablePopup
left: extruderIcon.right
leftMargin: UM.Theme.getSize("default_margin").width
top: typeAndBrandNameLabel.bottom
right: parent.right
rightMargin: UM.Theme.getSize("default_margin").width
}
}
}
}
//Placeholder text if there is a configuration to select but no materials (so we can't show the materials per extruder).
// Placeholder text if there is a configuration to select but no materials (so we can't show the materials per extruder).
Label
{
text: catalog.i18nc("@label", "Select configuration")

View file

@ -14,7 +14,7 @@ Menu
property int extruderIndex: 0
property string currentRootMaterialId: Cura.MachineManager.currentRootMaterialId[extruderIndex]
property string activeMaterialId: Cura.MachineManager.allActiveMaterialIds[Cura.ExtruderManager.extruderIds[extruderIndex]]
property string activeMaterialId: Cura.MachineManager.activeMachine.extruderList[extruderIndex].material.id
property bool updateModels: true
Cura.FavoriteMaterialsModel
{
@ -52,10 +52,10 @@ Menu
checkable: true
checked: model.root_material_id === menu.currentRootMaterialId
onTriggered: Cura.MachineManager.setMaterial(extruderIndex, model.container_node)
exclusiveGroup: group
exclusiveGroup: favoriteGroup // One favorite and one item from the others can be active at the same time.
}
onObjectAdded: menu.insertItem(index, object)
onObjectRemoved: menu.removeItem(object) // TODO: This ain't gonna work, removeItem() takes an index, not object
onObjectRemoved: menu.removeItem(index)
}
MenuSeparator {}
@ -77,7 +77,7 @@ Menu
onTriggered: Cura.MachineManager.setMaterial(extruderIndex, model.container_node)
}
onObjectAdded: genericMenu.insertItem(index, object)
onObjectRemoved: genericMenu.removeItem(object) // TODO: This ain't gonna work, removeItem() takes an index, not object
onObjectRemoved: genericMenu.removeItem(index)
}
}
@ -126,10 +126,16 @@ Menu
onObjectRemoved: menu.removeItem(object)
}
ExclusiveGroup {
ExclusiveGroup
{
id: group
}
ExclusiveGroup
{
id: favoriteGroup
}
MenuSeparator {}
MenuItem

View file

@ -1,84 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 1.4
import UM 1.2 as UM
import Cura 1.0 as Cura
Menu
{
id: menu
Instantiator
{
model: Cura.QualityProfilesDropDownMenuModel
MenuItem
{
text:
{
var full_text = (model.layer_height != "") ? model.name + " - " + model.layer_height + model.layer_height_unit : model.name
full_text += model.is_experimental ? " - " + catalog.i18nc("@label", "Experimental") : ""
return full_text
}
checkable: true
checked: Cura.MachineManager.activeQualityOrQualityChangesName == model.name
exclusiveGroup: group
onTriggered: Cura.MachineManager.setQualityGroup(model.quality_group)
visible: model.available
}
onObjectAdded: menu.insertItem(index, object)
onObjectRemoved: menu.removeItem(object)
}
MenuSeparator
{
id: customSeparator
visible: Cura.CustomQualityProfilesDropDownMenuModel.count > 0
}
Instantiator
{
id: customProfileInstantiator
model: Cura.CustomQualityProfilesDropDownMenuModel
Connections
{
target: Cura.CustomQualityProfilesDropDownMenuModel
onModelReset: customSeparator.visible = Cura.CustomQualityProfilesDropDownMenuModel.count > 0
}
MenuItem
{
text: model.name
checkable: true
checked: Cura.MachineManager.activeQualityOrQualityChangesName == model.name
exclusiveGroup: group
onTriggered: Cura.MachineManager.setQualityChangesGroup(model.quality_changes_group)
}
onObjectAdded:
{
customSeparator.visible = model.count > 0;
menu.insertItem(index, object);
}
onObjectRemoved:
{
customSeparator.visible = model.count > 0;
menu.removeItem(object);
}
}
ExclusiveGroup { id: group; }
MenuSeparator { id: profileMenuSeparator }
MenuItem { action: Cura.Actions.addProfile }
MenuItem { action: Cura.Actions.updateProfile }
MenuItem { action: Cura.Actions.resetProfile }
MenuSeparator { }
MenuItem { action: Cura.Actions.manageProfiles }
}

View file

@ -22,7 +22,7 @@ Menu
Menu
{
title: modelData.name
property var extruder: Cura.MachineManager.getExtruder(model.index)
NozzleMenu { title: Cura.MachineManager.activeDefinitionVariantsName; visible: Cura.MachineManager.hasVariants; extruderIndex: index }
MaterialMenu { title: catalog.i18nc("@title:menu", "&Material"); visible: Cura.MachineManager.hasMaterials; extruderIndex: index }
@ -41,14 +41,14 @@ Menu
{
text: catalog.i18nc("@action:inmenu", "Enable Extruder")
onTriggered: Cura.MachineManager.setExtruderEnabled(model.index, true)
visible: !Cura.MachineManager.getExtruder(model.index).isEnabled
visible: extruder === null ? false : !extruder.isEnabled
}
MenuItem
{
text: catalog.i18nc("@action:inmenu", "Disable Extruder")
onTriggered: Cura.MachineManager.setExtruderEnabled(index, false)
visible: Cura.MachineManager.getExtruder(model.index).isEnabled
visible: extruder === null ? false : extruder.isEnabled
enabled: Cura.MachineManager.numberExtrudersEnabled > 1
}
@ -57,14 +57,6 @@ Menu
onObjectRemoved: base.removeItem(object)
}
// TODO Only show in dev mode. Remove check when feature ready
BuildplateMenu
{
title: catalog.i18nc("@title:menu", "&Build plate")
visible: CuraSDKVersion == "dev" && Cura.MachineManager.hasVariantBuildplates
}
ProfileMenu { title: catalog.i18nc("@title:settings", "&Profile") }
MenuSeparator { }
MenuItem { action: Cura.Actions.configureSettingVisibility }

View file

@ -7,7 +7,7 @@ import QtQuick.Layouts 1.3
import QtQuick.Dialogs 1.2
import UM 1.2 as UM
import Cura 1.0 as Cura
import Cura 1.5 as Cura
Item
{
@ -17,6 +17,8 @@ Item
property var resetEnabled: false
property var currentItem: null
property var materialManagementModel: CuraApplication.getMaterialManagementModel()
property var hasCurrentItem: base.currentItem != null
property var isCurrentItemActivated:
{
@ -119,7 +121,7 @@ Item
onClicked:
{
forceActiveFocus();
base.newRootMaterialIdToSwitchTo = CuraApplication.getMaterialManager().createMaterial();
base.newRootMaterialIdToSwitchTo = base.materialManagementModel.createMaterial();
base.toActivateNewMaterial = true;
}
}
@ -134,7 +136,7 @@ Item
onClicked:
{
forceActiveFocus();
base.newRootMaterialIdToSwitchTo = CuraApplication.getMaterialManager().duplicateMaterial(base.currentItem.container_node);
base.newRootMaterialIdToSwitchTo = base.materialManagementModel.duplicateMaterial(base.currentItem.container_node);
base.toActivateNewMaterial = true;
}
}
@ -145,7 +147,8 @@ Item
id: removeMenuButton
text: catalog.i18nc("@action:button", "Remove")
iconName: "list-remove"
enabled: base.hasCurrentItem && !base.currentItem.is_read_only && !base.isCurrentItemActivated && CuraApplication.getMaterialManager().canMaterialBeRemoved(base.currentItem.container_node)
enabled: base.hasCurrentItem && !base.currentItem.is_read_only && !base.isCurrentItemActivated && base.materialManagementModel.canMaterialBeRemoved(base.currentItem.container_node)
onClicked:
{
forceActiveFocus();
@ -294,7 +297,7 @@ Item
{
// Set the active material as the fallback. It will be selected when the current material is deleted
base.newRootMaterialIdToSwitchTo = base.active_root_material_id
CuraApplication.getMaterialManager().removeMaterial(base.currentItem.container_node);
base.materialManagementModel.removeMaterial(base.currentItem.container_node);
}
}

View file

@ -82,11 +82,12 @@ Rectangle
{
if (materialSlot.is_favorite)
{
CuraApplication.getMaterialManager().removeFavorite(material.root_material_id)
return
CuraApplication.getMaterialManagementModel().removeFavorite(material.root_material_id)
}
else
{
CuraApplication.getMaterialManagementModel().addFavorite(material.root_material_id)
}
CuraApplication.getMaterialManager().addFavorite(material.root_material_id)
return
}
style: ButtonStyle
{

View file

@ -23,6 +23,7 @@ TabView
property real secondColumnWidth: (width * 0.40) | 0
property string containerId: ""
property var materialPreferenceValues: UM.Preferences.getValue("cura/material_settings") ? JSON.parse(UM.Preferences.getValue("cura/material_settings")) : {}
property var materialManagementModel: CuraApplication.getMaterialManagementModel()
property double spoolLength: calculateSpoolLength()
property real costPerMeter: calculateCostPerMeter()
@ -565,7 +566,7 @@ TabView
}
// update the values
CuraApplication.getMaterialManager().setMaterialName(base.currentMaterialNode, new_name)
base.materialManagementModel.setMaterialName(base.currentMaterialNode, new_name)
properties.name = new_name
}

View file

@ -38,14 +38,28 @@ Tab
property var setting: qualitySettings.getItem(styleData.row)
height: childrenRect.height
width: (parent != null) ? parent.width : 0
text: (styleData.value.substr(0,1) == "=") ? styleData.value : ""
text:
{
if (styleData.value === undefined)
{
return ""
}
return (styleData.value.substr(0,1) == "=") ? styleData.value : ""
}
Label
{
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
text: (styleData.value.substr(0,1) == "=") ? catalog.i18nc("@info:status", "Calculated") : styleData.value
text:
{
if (styleData.value === undefined)
{
return ""
}
return (styleData.value.substr(0,1) == "=") ? catalog.i18nc("@info:status", "Calculated") : styleData.value
}
font.strikeout: styleData.column == 1 && setting.user_value != "" && base.isQualityItemCurrentlyActivated
font.italic: setting.profile_value_source == "quality_changes" || (setting.user_value != "" && base.isQualityItemCurrentlyActivated)
opacity: font.strikeout ? 0.5 : 1

View file

@ -1,4 +1,4 @@
// Copyright (c) 2018 Ultimaker B.V.
// Copyright (c) 2019 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
@ -7,26 +7,24 @@ import QtQuick.Layouts 1.3
import QtQuick.Dialogs 1.2
import UM 1.2 as UM
import Cura 1.0 as Cura
import Cura 1.6 as Cura
Item
{
id: base
property QtObject qualityManager: CuraApplication.getQualityManager()
property var resetEnabled: false // Keep PreferencesDialog happy
property var extrudersModel: CuraApplication.getExtrudersModel()
property var qualityManagementModel: CuraApplication.getQualityManagementModel()
UM.I18nCatalog { id: catalog; name: "cura"; }
Cura.QualityManagementModel {
id: qualitiesModel
}
Label {
Label
{
id: titleLabel
anchors {
anchors
{
top: parent.top
left: parent.left
right: parent.right
@ -38,28 +36,41 @@ Item
property var hasCurrentItem: base.currentItem != null
property var currentItem: {
property var currentItem:
{
var current_index = qualityListView.currentIndex;
return (current_index == -1) ? null : qualitiesModel.getItem(current_index);
return (current_index == -1) ? null : base.qualityManagementModel.getItem(current_index);
}
property var currentItemName: hasCurrentItem ? base.currentItem.name : ""
property var currentItemDisplayName: hasCurrentItem ? base.qualityManagementModel.getQualityItemDisplayName(base.currentItem) : ""
property var isCurrentItemActivated: {
if (!base.currentItem) {
property var isCurrentItemActivated:
{
if (!base.currentItem)
{
return false;
}
return base.currentItem.name == Cura.MachineManager.activeQualityOrQualityChangesName;
if (base.currentItem.is_read_only)
{
return (base.currentItem.name == Cura.MachineManager.activeQualityOrQualityChangesName) && (base.currentItem.intent_category == Cura.MachineManager.activeIntentCategory);
}
else
{
return base.currentItem.name == Cura.MachineManager.activeQualityOrQualityChangesName;
}
}
property var canCreateProfile: {
property var canCreateProfile:
{
return isCurrentItemActivated && Cura.MachineManager.hasUserSettings;
}
Row // Button Row
{
id: buttonRow
anchors {
anchors
{
left: parent.left
right: parent.right
top: titleLabel.bottom
@ -73,10 +84,14 @@ Item
text: catalog.i18nc("@action:button", "Activate")
iconName: "list-activate"
enabled: !isCurrentItemActivated
onClicked: {
if (base.currentItem.is_read_only) {
Cura.MachineManager.setQualityGroup(base.currentItem.quality_group);
} else {
onClicked:
{
if (base.currentItem.is_read_only)
{
Cura.IntentManager.selectIntent(base.currentItem.intent_category, base.currentItem.quality_type);
}
else
{
Cura.MachineManager.setQualityChangesGroup(base.currentItem.quality_changes_group);
}
}
@ -91,7 +106,8 @@ Item
enabled: base.canCreateProfile && !Cura.MachineManager.stacksHaveErrors
visible: base.canCreateProfile
onClicked: {
onClicked:
{
createQualityDialog.object = Cura.ContainerManager.makeUniqueName(base.currentItem.name);
createQualityDialog.open();
createQualityDialog.selectText();
@ -107,7 +123,8 @@ Item
enabled: !base.canCreateProfile
visible: !base.canCreateProfile
onClicked: {
onClicked:
{
duplicateQualityDialog.object = Cura.ContainerManager.makeUniqueName(base.currentItem.name);
duplicateQualityDialog.open();
duplicateQualityDialog.selectText();
@ -121,7 +138,8 @@ Item
text: catalog.i18nc("@action:button", "Remove")
iconName: "list-remove"
enabled: base.hasCurrentItem && !base.currentItem.is_read_only && !base.isCurrentItemActivated
onClicked: {
onClicked:
{
forceActiveFocus();
confirmRemoveQualityDialog.open();
}
@ -134,7 +152,8 @@ Item
text: catalog.i18nc("@action:button", "Rename")
iconName: "edit-rename"
enabled: base.hasCurrentItem && !base.currentItem.is_read_only
onClicked: {
onClicked:
{
renameQualityDialog.object = base.currentItem.name;
renameQualityDialog.open();
renameQualityDialog.selectText();
@ -147,7 +166,8 @@ Item
id: importMenuButton
text: catalog.i18nc("@action:button", "Import")
iconName: "document-import"
onClicked: {
onClicked:
{
importDialog.open();
}
}
@ -159,7 +179,8 @@ Item
text: catalog.i18nc("@action:button", "Export")
iconName: "document-export"
enabled: base.hasCurrentItem && !base.currentItem.is_read_only
onClicked: {
onClicked:
{
exportDialog.open();
}
}
@ -185,7 +206,7 @@ Item
{
base.newQualityNameToSelect = newName; // We want to switch to the new profile once it's created
base.toActivateNewQuality = true;
base.qualityManager.createQualityChanges(newName);
base.qualityManagementModel.createQualityChanges(newName);
}
}
@ -195,7 +216,7 @@ Item
// This connection makes sure that we will switch to the correct quality after the model gets updated
Connections
{
target: qualitiesModel
target: base.qualityManagementModel
onItemsChanged:
{
var toSelectItemName = base.currentItem == null ? "" : base.currentItem.name;
@ -208,9 +229,9 @@ Item
if (toSelectItemName != "")
{
// Select the required quality name if given
for (var idx = 0; idx < qualitiesModel.count; ++idx)
for (var idx = 0; idx < base.qualityManagementModel.count; ++idx)
{
var item = qualitiesModel.getItem(idx);
var item = base.qualityManagementModel.getItem(idx);
if (item.name == toSelectItemName)
{
// Switch to the newly created profile if needed
@ -240,7 +261,7 @@ Item
object: "<new name>"
onAccepted:
{
base.qualityManager.duplicateQualityChanges(newName, base.currentItem);
base.qualityManagementModel.duplicateQualityChanges(newName, base.currentItem);
}
}
@ -257,7 +278,7 @@ Item
onYes:
{
base.qualityManager.removeQualityChangesGroup(base.currentItem.quality_changes_group);
base.qualityManagementModel.removeQualityChangesGroup(base.currentItem.quality_changes_group);
// reset current item to the first if available
qualityListView.currentIndex = -1; // Reset selection.
}
@ -271,7 +292,7 @@ Item
object: "<new name>"
onAccepted:
{
var actualNewName = base.qualityManager.renameQualityChangesGroup(base.currentItem.quality_changes_group, newName);
var actualNewName = base.qualityManagementModel.renameQualityChangesGroup(base.currentItem.quality_changes_group, newName);
base.newQualityNameToSelect = actualNewName; // Select the new name after the model gets updated
}
}
@ -282,19 +303,22 @@ Item
id: importDialog
title: catalog.i18nc("@title:window", "Import Profile")
selectExisting: true
nameFilters: qualitiesModel.getFileNameFilters("profile_reader")
nameFilters: base.qualityManagementModel.getFileNameFilters("profile_reader")
folder: CuraApplication.getDefaultPath("dialog_profile_path")
onAccepted:
{
var result = Cura.ContainerManager.importProfile(fileUrl);
messageDialog.text = result.message;
if (result.status == "ok") {
if (result.status == "ok")
{
messageDialog.icon = StandardIcon.Information;
}
else if (result.status == "duplicate") {
else if (result.status == "duplicate")
{
messageDialog.icon = StandardIcon.Warning;
}
else {
else
{
messageDialog.icon = StandardIcon.Critical;
}
messageDialog.open();
@ -308,14 +332,15 @@ Item
id: exportDialog
title: catalog.i18nc("@title:window", "Export Profile")
selectExisting: false
nameFilters: qualitiesModel.getFileNameFilters("profile_writer")
nameFilters: base.qualityManagementModel.getFileNameFilters("profile_writer")
folder: CuraApplication.getDefaultPath("dialog_profile_path")
onAccepted:
{
var result = Cura.ContainerManager.exportQualityChangesGroup(base.currentItem.quality_changes_group,
fileUrl, selectedNameFilter);
if (result && result.status == "error") {
if (result && result.status == "error")
{
messageDialog.icon = StandardIcon.Critical;
messageDialog.text = result.message;
messageDialog.open();
@ -326,10 +351,12 @@ Item
}
}
Item {
Item
{
id: contentsItem
anchors {
anchors
{
top: titleLabel.bottom
left: parent.left
right: parent.right
@ -343,7 +370,8 @@ Item
Item
{
anchors {
anchors
{
top: buttonRow.bottom
topMargin: UM.Theme.getSize("default_margin").height
left: parent.left
@ -351,12 +379,16 @@ Item
bottom: parent.bottom
}
SystemPalette { id: palette }
SystemPalette
{
id: palette
}
Label
{
id: captionLabel
anchors {
anchors
{
top: parent.top
left: parent.left
}
@ -369,14 +401,16 @@ Item
ScrollView
{
id: profileScrollView
anchors {
anchors
{
top: captionLabel.visible ? captionLabel.bottom : parent.top
topMargin: captionLabel.visible ? UM.Theme.getSize("default_margin").height : 0
bottom: parent.bottom
left: parent.left
}
Rectangle {
Rectangle
{
parent: viewport
anchors.fill: parent
color: palette.light
@ -390,16 +424,16 @@ Item
{
id: qualityListView
model: qualitiesModel
model: base.qualityManagementModel
Component.onCompleted:
{
var selectedItemName = Cura.MachineManager.activeQualityOrQualityChangesName;
// Select the required quality name if given
for (var idx = 0; idx < qualitiesModel.count; idx++)
for (var idx = 0; idx < base.qualityManagementModel.count; idx++)
{
var item = qualitiesModel.getItem(idx);
var item = base.qualityManagementModel.getItem(idx);
if (item.name == selectedItemName)
{
currentIndex = idx;
@ -408,7 +442,7 @@ Item
}
}
section.property: "is_read_only"
section.property: "section_name"
section.delegate: Rectangle
{
height: childrenRect.height
@ -417,7 +451,7 @@ Item
{
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_lining").width
text: section == "true" ? catalog.i18nc("@label", "Default profiles") : catalog.i18nc("@label", "Custom profiles")
text: section
font.bold: true
}
}
@ -441,14 +475,27 @@ Item
width: Math.floor((parent.width * 0.8))
text: model.name
elide: Text.ElideRight
font.italic: model.name == Cura.MachineManager.activeQualityOrQualityChangesName
font.italic:
{
if (model.is_read_only)
{
// For built-in qualities, it needs to match both the intent category and the quality name
return model.name == Cura.MachineManager.activeQualityOrQualityChangesName && model.intent_category == Cura.MachineManager.activeIntentCategory
}
else
{
// For custom qualities, it only needs to match the name
return model.name == Cura.MachineManager.activeQualityOrQualityChangesName
}
}
color: parent.isCurrentItem ? palette.highlightedText : palette.text
}
MouseArea
{
anchors.fill: parent
onClicked: {
onClicked:
{
parent.ListView.view.currentIndex = model.index;
}
}
@ -461,7 +508,8 @@ Item
{
id: detailsPanel
anchors {
anchors
{
left: profileScrollView.right
leftMargin: UM.Theme.getSize("default_margin").width
top: parent.top
@ -481,15 +529,17 @@ Item
width: parent.width
height: childrenRect.height
Label {
text: base.currentItemName
Label
{
text: base.currentItemDisplayName
font: UM.Theme.getFont("large_bold")
}
}
Flow {
Flow
{
id: currentSettingsActions
visible: base.hasCurrentItem && base.currentItem.name == Cura.MachineManager.activeQualityOrQualityChangesName
visible: base.hasCurrentItem && base.currentItem.name == Cura.MachineManager.activeQualityOrQualityChangesName && base.currentItem.intent_category == Cura.MachineManager.activeIntentCategory
anchors.left: parent.left
anchors.right: parent.right
anchors.top: profileName.bottom
@ -510,7 +560,8 @@ Item
}
}
Column {
Column
{
id: profileNotices
anchors.top: currentSettingsActions.visible ? currentSettingsActions.bottom : currentSettingsActions.anchors.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
@ -518,14 +569,16 @@ Item
anchors.right: parent.right
spacing: UM.Theme.getSize("default_margin").height
Label {
Label
{
id: defaultsMessage
visible: false
text: catalog.i18nc("@action:label", "This profile uses the defaults specified by the printer, so it has no settings/overrides in the list below.")
wrapMode: Text.WordWrap
width: parent.width
}
Label {
Label
{
id: noCurrentSettingsMessage
visible: base.isCurrentItemActivated && !Cura.MachineManager.hasUserSettings
text: catalog.i18nc("@action:label", "Your current settings match the selected profile.")
@ -534,7 +587,6 @@ Item
}
}
TabView
{
anchors.left: parent.left

View file

@ -1,12 +1,13 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Controls 1.4 as OldControls
import UM 1.3 as UM
import Cura 1.0 as Cura
import Cura 1.6 as Cura
import ".."
Item
{
@ -17,19 +18,175 @@ Item
property var extrudersModel: CuraApplication.getExtrudersModel()
// Profile selector row
GlobalProfileSelector
Item
{
id: globalProfileRow
id: intent
height: childrenRect.height
anchors
{
top: parent.top
topMargin: parent.padding
topMargin: UM.Theme.getSize("default_margin").height
left: parent.left
leftMargin: parent.padding
right: parent.right
rightMargin: parent.padding
}
Label
{
id: profileLabel
anchors
{
top: parent.top
bottom: parent.bottom
left: parent.left
right: intentSelection.left
}
text: catalog.i18nc("@label", "Profile")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
}
NoIntentIcon
{
affected_extruders: Cura.MachineManager.extruderPositionsWithNonActiveIntent
intent_type: Cura.MachineManager.activeIntentCategory
anchors.right: intentSelection.left
anchors.rightMargin: UM.Theme.getSize("narrow_margin").width
width: Math.round(profileLabel.height * 0.5)
anchors.verticalCenter: parent.verticalCenter
height: width
visible: affected_extruders.length
}
Button
{
id: intentSelection
onClicked: menu.opened ? menu.close() : menu.open()
text: generateActiveQualityText()
anchors.right: parent.right
width: UM.Theme.getSize("print_setup_big_item").width
height: textLabel.contentHeight + 2 * UM.Theme.getSize("narrow_margin").height
hoverEnabled: true
baselineOffset: null // If we don't do this, there is a binding loop. WHich is a bit weird, since we override the contentItem anyway...
contentItem: Label
{
id: textLabel
text: intentSelection.text
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: intentSelection.verticalCenter
height: contentHeight
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
}
background: Rectangle
{
id: backgroundItem
border.color: intentSelection.hovered ? UM.Theme.getColor("setting_control_border_highlight") : UM.Theme.getColor("setting_control_border")
border.width: UM.Theme.getSize("default_lining").width
radius: UM.Theme.getSize("default_radius").width
color: UM.Theme.getColor("main_background")
}
function generateActiveQualityText()
{
var resultMap = Cura.MachineManager.activeQualityDisplayNameMap
var resultMain = resultMap["main"]
var resultSuffix = resultMap["suffix"]
var result = ""
if (Cura.MachineManager.isActiveQualityExperimental)
{
resultSuffix += " (Experimental)"
}
if (Cura.MachineManager.isActiveQualitySupported)
{
if (Cura.MachineManager.activeQualityLayerHeight > 0)
{
result = resultMain
if (resultSuffix)
{
result += " - "
}
result += "<font color=\"" + UM.Theme.getColor("text_detail") + "\">"
if (resultSuffix)
{
result += resultSuffix
}
result += " - "
result += Cura.MachineManager.activeQualityLayerHeight + "mm"
result += "</font>"
}
}
return result
}
UM.SimpleButton
{
id: customisedSettings
visible: Cura.MachineManager.hasUserSettings
width: UM.Theme.getSize("print_setup_icon").width
height: UM.Theme.getSize("print_setup_icon").height
anchors.verticalCenter: parent.verticalCenter
anchors.right: downArrow.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
color: hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button");
iconSource: UM.Theme.getIcon("star")
onClicked:
{
forceActiveFocus();
Cura.Actions.manageProfiles.trigger()
}
onEntered:
{
var content = catalog.i18nc("@tooltip", "Some setting/override values are different from the values stored in the profile.\n\nClick to open the profile manager.")
base.showTooltip(intent, Qt.point(-UM.Theme.getSize("default_margin").width, 0), content)
}
onExited: base.hideTooltip()
}
UM.RecolorImage
{
id: downArrow
source: UM.Theme.getIcon("arrow_bottom")
width: UM.Theme.getSize("standard_arrow").width
height: UM.Theme.getSize("standard_arrow").height
anchors
{
right: parent.right
verticalCenter: parent.verticalCenter
rightMargin: UM.Theme.getSize("default_margin").width
}
color: UM.Theme.getColor("setting_control_button")
}
}
QualitiesWithIntentMenu
{
id: menu
y: intentSelection.y + intentSelection.height
x: intentSelection.x
width: intentSelection.width
}
}
UM.TabRow
@ -40,7 +197,7 @@ Item
anchors
{
top: globalProfileRow.bottom
top: intent.bottom
topMargin: UM.Theme.getSize("default_margin").height
left: parent.left
leftMargin: parent.padding
@ -99,7 +256,7 @@ Item
{
anchors
{
top: tabBar.visible ? tabBar.bottom : globalProfileRow.bottom
top: tabBar.visible ? tabBar.bottom : intent.bottom
topMargin: -UM.Theme.getSize("default_lining").width
left: parent.left
leftMargin: parent.padding

View file

@ -1,100 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.2
import UM 1.2 as UM
import Cura 1.0 as Cura
Item
{
id: globalProfileRow
height: childrenRect.height
Label
{
id: globalProfileLabel
anchors
{
top: parent.top
bottom: parent.bottom
left: parent.left
right: globalProfileSelection.left
}
text: catalog.i18nc("@label", "Profile")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
}
ToolButton
{
id: globalProfileSelection
text: generateActiveQualityText()
width: UM.Theme.getSize("print_setup_big_item").width
height: UM.Theme.getSize("print_setup_big_item").height
anchors
{
top: parent.top
right: parent.right
}
tooltip: Cura.MachineManager.activeQualityOrQualityChangesName
style: UM.Theme.styles.print_setup_header_button
activeFocusOnPress: true
menu: Cura.ProfileMenu { }
function generateActiveQualityText()
{
var result = Cura.MachineManager.activeQualityOrQualityChangesName
if (Cura.MachineManager.isActiveQualityExperimental)
{
result += " (Experimental)"
}
if (Cura.MachineManager.isActiveQualitySupported)
{
if (Cura.MachineManager.activeQualityLayerHeight > 0)
{
result += " <font color=\"" + UM.Theme.getColor("text_detail") + "\">"
result += " - "
result += Cura.MachineManager.activeQualityLayerHeight + "mm"
result += "</font>"
}
}
return result
}
UM.SimpleButton
{
id: customisedSettings
visible: Cura.MachineManager.hasUserSettings
width: UM.Theme.getSize("print_setup_icon").width
height: UM.Theme.getSize("print_setup_icon").height
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Math.round(UM.Theme.getSize("setting_preferences_button_margin").width - UM.Theme.getSize("thick_margin").width)
color: hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button");
iconSource: UM.Theme.getIcon("star")
onClicked:
{
forceActiveFocus();
Cura.Actions.manageProfiles.trigger()
}
onEntered:
{
var content = catalog.i18nc("@tooltip","Some setting/override values are different from the values stored in the profile.\n\nClick to open the profile manager.")
base.showTooltip(globalProfileRow, Qt.point(-UM.Theme.getSize("default_margin").width, 0), content)
}
onExited: base.hideTooltip()
}
}
}

View file

@ -0,0 +1,54 @@
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.2 as UM
import Cura 1.6 as Cura
Button
{
// This is a work around for a qml issue. Since the default button uses a private implementation for contentItem
// (the so called IconText), which handles the mnemonic conversion (aka; ensuring that &Button) text property
// is rendered with the B underlined. Since we're also forced to mix controls 1.0 and 2.0 actions together,
// we need a special property for the text of the label if we do want it to be rendered correclty, but don't want
// another shortcut to be added (which will cause for "QQuickAction::event: Ambiguous shortcut overload: " to
// happen.
property string labelText: ""
id: button
hoverEnabled: true
background: Rectangle
{
id: backgroundRectangle
border.width: UM.Theme.getSize("default_lining").width
border.color: button.checked ? UM.Theme.getColor("setting_control_border_highlight") : "transparent"
color: button.hovered ? UM.Theme.getColor("action_button_hovered") : "transparent"
radius: UM.Theme.getSize("action_button_radius").width
}
// Workarround to ensure that the mnemonic highlighting happens correctly
function replaceText(txt)
{
var index = txt.indexOf("&")
if(index >= 0)
{
txt = txt.replace(txt.substr(index, 2), ("<u>" + txt.substr(index + 1, 1) + "</u>"))
}
return txt
}
contentItem: Label
{
id: textLabel
text: button.text != "" ? replaceText(button.text) : replaceText(button.labelText)
height: contentHeight
verticalAlignment: Text.AlignVCenter
anchors.left: button.left
anchors.leftMargin: UM.Theme.getSize("wide_margin").width
renderType: Text.NativeRendering
font: UM.Theme.getFont("default")
color: button.enabled ? UM.Theme.getColor("text") :UM.Theme.getColor("text_inactive")
}
}

View file

@ -0,0 +1,315 @@
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.2 as UM
import Cura 1.6 as Cura
Popup
{
id: popup
implicitWidth: 400
property var dataModel: Cura.IntentCategoryModel {}
property int defaultMargin: UM.Theme.getSize("default_margin").width
property color backgroundColor: UM.Theme.getColor("main_background")
property color borderColor: UM.Theme.getColor("lining")
topPadding: UM.Theme.getSize("narrow_margin").height
rightPadding: UM.Theme.getSize("default_lining").width
leftPadding: UM.Theme.getSize("default_lining").width
padding: 0
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
background: Cura.RoundedRectangle
{
color: backgroundColor
border.width: UM.Theme.getSize("default_lining").width
border.color: borderColor
cornerSide: Cura.RoundedRectangle.Direction.Down
}
ButtonGroup
{
id: buttonGroup
exclusive: true
onClicked: popup.visible = false
}
contentItem: Column
{
// This repeater adds the intent labels
ScrollView
{
property real maximumHeight: screenScaleFactor * 400
contentHeight: dataColumn.height
height: Math.min(contentHeight, maximumHeight)
clip: true
ScrollBar.vertical.policy: height == maximumHeight ? ScrollBar.AlwaysOn: ScrollBar.AlwaysOff
Column
{
id: dataColumn
width: parent.width
Repeater
{
model: dataModel
delegate: Item
{
// We need to set it like that, otherwise we'd have to set the sub model with model: model.qualities
// Which obviously won't work due to naming conflicts.
property variant subItemModel: model.qualities
height: childrenRect.height
width: popup.contentWidth
Label
{
id: headerLabel
text: model.name
renderType: Text.NativeRendering
height: visible ? contentHeight: 0
enabled: false
visible: qualitiesList.visibleChildren.length > 0
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
}
Column
{
id: qualitiesList
anchors.top: headerLabel.bottom
anchors.left: parent.left
anchors.right: parent.right
// We set it by means of a binding, since then we can use the when condition, which we need to
// prevent a binding loop.
Binding
{
target: parent
property: "height"
value: parent.childrenRect.height
when: parent.visibleChildren.length > 0
}
// Add the qualities that belong to the intent
Repeater
{
visible: false
model: subItemModel
MenuButton
{
id: button
onClicked: Cura.IntentManager.selectIntent(model.intent_category, model.quality_type)
width: parent.width
checkable: true
visible: model.available
text: model.name + " - " + model.layer_height + " mm"
checked:
{
if (Cura.MachineManager.hasCustomQuality)
{
// When user created profile is active, no quality tickbox should be active.
return false;
}
return Cura.MachineManager.activeQualityType == model.quality_type && Cura.MachineManager.activeIntentCategory == model.intent_category;
}
ButtonGroup.group: buttonGroup
}
}
}
}
}
//Another "intent category" for custom profiles.
Item
{
height: childrenRect.height
anchors
{
left: parent.left
right: parent.right
}
Label
{
id: customProfileHeader
text: catalog.i18nc("@label:header", "Custom profiles")
renderType: Text.NativeRendering
height: visible ? contentHeight: 0
enabled: false
visible: profilesList.visibleChildren.length > 1
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
}
Column
{
id: profilesList
anchors
{
top: customProfileHeader.bottom
left: parent.left
right: parent.right
}
//We set it by means of a binding, since then we can use the
//"when" condition, which we need to prevent a binding loop.
Binding
{
target: parent
property: "height"
value: parent.childrenRect.height
when: parent.visibleChildren.length > 1
}
//Add all the custom profiles.
Repeater
{
model: Cura.CustomQualityProfilesDropDownMenuModel
MenuButton
{
onClicked: Cura.MachineManager.setQualityChangesGroup(model.quality_changes_group)
width: parent.width
checkable: true
visible: model.available
text: model.name
checked:
{
var active_quality_group = Cura.MachineManager.activeQualityChangesGroup
if (active_quality_group != null)
{
return active_quality_group.name == model.quality_changes_group.name
}
return false
}
ButtonGroup.group: buttonGroup
}
}
}
}
}
}
Rectangle
{
height: UM.Theme.getSize("default_lining").height
anchors.left: parent.left
anchors.right: parent.right
color: borderColor
}
MenuButton
{
labelText: Cura.Actions.addProfile.text
anchors.left: parent.left
anchors.right: parent.right
enabled: Cura.Actions.addProfile.enabled
onClicked:
{
Cura.Actions.addProfile.trigger()
popup.visible = false
}
}
MenuButton
{
labelText: Cura.Actions.updateProfile.text
anchors.left: parent.left
anchors.right: parent.right
enabled: Cura.Actions.updateProfile.enabled
onClicked:
{
popup.visible = false
Cura.Actions.updateProfile.trigger()
}
}
MenuButton
{
text: catalog.i18nc("@action:button", "Discard current changes")
anchors.left: parent.left
anchors.right: parent.right
enabled: Cura.MachineManager.hasUserSettings
onClicked:
{
popup.visible = false
Cura.ContainerManager.clearUserContainers()
}
}
Rectangle
{
height: UM.Theme.getSize("default_lining").width
anchors.left: parent.left
anchors.right: parent.right
color: borderColor
}
MenuButton
{
id: manageProfilesButton
text: Cura.Actions.manageProfiles.text
anchors
{
left: parent.left
right: parent.right
}
height: textLabel.contentHeight + 2 * UM.Theme.getSize("narrow_margin").height
contentItem: Item
{
width: parent.width
height: childrenRect.height
Label
{
id: textLabel
text: manageProfilesButton.text
height: contentHeight
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
}
Label
{
id: shortcutLabel
text: Cura.Actions.manageProfiles.shortcut
height: contentHeight
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
}
}
onClicked:
{
popup.visible = false
Cura.Actions.manageProfiles.trigger()
}
}
// spacer
Item
{
width: 2
height: UM.Theme.getSize("default_radius").width
}
}
}

View file

@ -0,0 +1,36 @@
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.2 as UM
import Cura 1.6 as Cura
Item
{
id: icon
property var affected_extruders
property var intent_type: ""
implicitWidth: UM.Theme.getSize("section_icon").width
implicitHeight: UM.Theme.getSize("section_icon").height
UM.RecolorImage
{
source: UM.Theme.getIcon("info")
color: UM.Theme.getColor("icon")
anchors.fill: parent
}
MouseArea
{
anchors.fill: parent
hoverEnabled: parent.visible
onEntered:
{
var tooltipContent = catalog.i18ncp("@label %1 is filled in with the type of a profile. %2 is filled with a list of numbers (eg '1' or '1, 2')", "There is no %1 profile for the configuration in extruder %2. The default intent will be used instead", "There is no %1 profile for the configurations in extruders %2. The default intent will be used instead", affected_extruders.length).arg(intent_type).arg(affected_extruders)
base.showTooltip(icon.parent, Qt.point(-UM.Theme.getSize("thick_margin").width, 0), tooltipContent)
}
onExited: base.hideTooltip()
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
@ -20,10 +20,16 @@ RowLayout
{
if (Cura.MachineManager.activeStack)
{
var text = Cura.MachineManager.activeQualityOrQualityChangesName
var resultMap = Cura.MachineManager.activeQualityDisplayNameMap
var text = resultMap["main"]
if (resultMap["suffix"])
{
text += " - " + resultMap["suffix"]
}
if (!Cura.MachineManager.hasNotSupportedQuality)
{
text += " " + layerHeight.properties.value + "mm"
text += " - " + layerHeight.properties.value + "mm"
text += Cura.MachineManager.isActiveQualityExperimental ? " - " + catalog.i18nc("@label", "Experimental") : ""
}
return text

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
@ -27,7 +27,6 @@ Item
Column
{
width: parent.width - 2 * parent.padding
spacing: UM.Theme.getSize("wide_margin").height
anchors

View file

@ -1,17 +1,15 @@
// Copyright (c) 2018 Ultimaker B.V.
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls 2.3 as Controls2
import QtQuick.Controls.Styles 1.4
import UM 1.2 as UM
import Cura 1.0 as Cura
import Cura 1.6 as Cura
import ".."
//
// Quality profile
//
Item
{
id: qualityRow
@ -20,436 +18,153 @@ Item
property real labelColumnWidth: Math.round(width / 3)
property real settingsColumnWidth: width - labelColumnWidth
Timer
{
id: qualitySliderChangeTimer
interval: 50
running: false
repeat: false
onTriggered:
{
var item = Cura.QualityProfilesDropDownMenuModel.getItem(qualitySlider.value);
Cura.MachineManager.activeQualityGroup = item.quality_group;
}
}
Component.onCompleted: qualityModel.update()
Connections
{
target: Cura.QualityProfilesDropDownMenuModel
onItemsChanged: qualityModel.update()
}
Connections {
target: base
onVisibleChanged:
{
// update needs to be called when the widgets are visible, otherwise the step width calculation
// will fail because the width of an invisible item is 0.
if (visible)
{
qualityModel.update();
}
}
}
ListModel
{
id: qualityModel
property var totalTicks: 0
property var availableTotalTicks: 0
property var existingQualityProfile: 0
property var qualitySliderActiveIndex: 0
property var qualitySliderStepWidth: 0
property var qualitySliderAvailableMin: 0
property var qualitySliderAvailableMax: 0
property var qualitySliderMarginRight: 0
function update ()
{
reset()
var availableMin = -1
var availableMax = -1
for (var i = 0; i < Cura.QualityProfilesDropDownMenuModel.rowCount(); i++)
{
var qualityItem = Cura.QualityProfilesDropDownMenuModel.getItem(i)
// Add each quality item to the UI quality model
qualityModel.append(qualityItem)
// Set selected value
if (Cura.MachineManager.activeQualityType == qualityItem.quality_type)
{
// set to -1 when switching to user created profile so all ticks are clickable
if (Cura.MachineManager.hasCustomQuality)
{
qualityModel.qualitySliderActiveIndex = -1
}
else
{
qualityModel.qualitySliderActiveIndex = i
}
qualityModel.existingQualityProfile = 1
}
// Set min available
if (qualityItem.available && availableMin == -1)
{
availableMin = i
}
// Set max available
if (qualityItem.available)
{
availableMax = i
}
}
// Set total available ticks for active slider part
if (availableMin != -1)
{
qualityModel.availableTotalTicks = availableMax - availableMin + 1
}
// Calculate slider values
calculateSliderStepWidth(qualityModel.totalTicks)
calculateSliderMargins(availableMin, availableMax, qualityModel.totalTicks)
qualityModel.qualitySliderAvailableMin = availableMin
qualityModel.qualitySliderAvailableMax = availableMax
}
function calculateSliderStepWidth (totalTicks)
{
// Do not use Math.round otherwise the tickmarks won't be aligned
qualityModel.qualitySliderStepWidth = totalTicks != 0 ?
((settingsColumnWidth - UM.Theme.getSize("print_setup_slider_handle").width) / (totalTicks)) : 0
}
function calculateSliderMargins (availableMin, availableMax, totalTicks)
{
if (availableMin == -1 || (availableMin == 0 && availableMax == 0))
{
// Do not use Math.round otherwise the tickmarks won't be aligned
qualityModel.qualitySliderMarginRight = settingsColumnWidth / 2
}
else if (availableMin == availableMax)
{
// Do not use Math.round otherwise the tickmarks won't be aligned
qualityModel.qualitySliderMarginRight = (totalTicks - availableMin) * qualitySliderStepWidth
}
else
{
// Do not use Math.round otherwise the tickmarks won't be aligned
qualityModel.qualitySliderMarginRight = (totalTicks - availableMax) * qualitySliderStepWidth
}
}
function reset () {
qualityModel.clear()
qualityModel.availableTotalTicks = 0
qualityModel.existingQualityProfile = 0
// check, the ticks count cannot be less than zero
qualityModel.totalTicks = Math.max(0, Cura.QualityProfilesDropDownMenuModel.rowCount() - 1)
}
}
// Here are the elements that are shown in the left column
Item
Column
{
id: titleRow
width: labelColumnWidth
height: childrenRect.height
Cura.IconWithText
{
id: qualityRowTitle
source: UM.Theme.getIcon("category_layer_height")
text: catalog.i18nc("@label", "Layer Height")
font: UM.Theme.getFont("medium")
anchors.left: parent.left
anchors.right: customisedSettings.left
}
UM.SimpleButton
{
id: customisedSettings
visible: Cura.SimpleModeSettingsManager.isProfileCustomized || Cura.MachineManager.hasCustomQuality
height: visible ? UM.Theme.getSize("print_setup_icon").height : 0
width: height
anchors
{
right: parent.right
rightMargin: UM.Theme.getSize("default_margin").width
leftMargin: UM.Theme.getSize("default_margin").width
verticalCenter: parent.verticalCenter
}
color: hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button")
iconSource: UM.Theme.getIcon("reset")
onClicked:
{
// if the current profile is user-created, switch to a built-in quality
Cura.MachineManager.resetToUseDefaultQuality()
}
onEntered:
{
var tooltipContent = catalog.i18nc("@tooltip","You have modified some profile settings. If you want to change these go to custom mode.")
base.showTooltip(qualityRow, Qt.point(-UM.Theme.getSize("thick_margin").width, 0), tooltipContent)
}
onExited: base.hideTooltip()
}
}
// Show titles for the each quality slider ticks
Item
{
anchors.left: speedSlider.left
anchors.top: speedSlider.bottom
height: childrenRect.height
Repeater
{
model: qualityModel
Label
{
anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top
// The height has to be set manually, otherwise it's not automatically calculated in the repeater
height: UM.Theme.getSize("default_margin").height
color: (Cura.MachineManager.activeMachine != null && Cura.QualityProfilesDropDownMenuModel.getItem(index).available) ? UM.Theme.getColor("quality_slider_available") : UM.Theme.getColor("quality_slider_unavailable")
text:
{
var result = ""
if(Cura.MachineManager.activeMachine != null)
{
result = Cura.QualityProfilesDropDownMenuModel.getItem(index).layer_height
if(result == undefined)
{
result = "";
}
else
{
result = Number(Math.round(result + "e+2") + "e-2"); //Round to 2 decimals. Javascript makes this difficult...
if (result == undefined || result != result) //Parse failure.
{
result = "";
}
}
}
return result
}
x:
{
// Make sure the text aligns correctly with each tick
if (qualityModel.totalTicks == 0)
{
// If there is only one tick, align it centrally
return Math.round(((settingsColumnWidth) - width) / 2)
}
else if (index == 0)
{
return Math.round(settingsColumnWidth / qualityModel.totalTicks) * index
}
else if (index == qualityModel.totalTicks)
{
return Math.round(settingsColumnWidth / qualityModel.totalTicks) * index - width
}
else
{
return Math.round((settingsColumnWidth / qualityModel.totalTicks) * index - (width / 2))
}
}
font: UM.Theme.getFont("default")
}
}
}
// Print speed slider
// Two sliders are created, one at the bottom with the unavailable qualities
// and the other at the top with the available quality profiles and so the handle to select them.
Item
{
id: speedSlider
height: childrenRect.height
anchors
{
left: titleRow.right
left: parent.left
right: parent.right
verticalCenter: titleRow.verticalCenter
}
// Draw unavailable slider
Slider
spacing: UM.Theme.getSize("default_margin").height
Controls2.ButtonGroup
{
id: unavailableSlider
id: activeProfileButtonGroup
exclusive: true
onClicked: Cura.IntentManager.selectIntent(button.modelData.intent_category, button.modelData.quality_type)
}
width: parent.width
height: qualitySlider.height // Same height as the slider that is on top
updateValueWhileDragging : false
tickmarksEnabled: true
minimumValue: 0
// maximumValue must be greater than minimumValue to be able to see the handle. While the value is strictly
// speaking not always correct, it seems to have the correct behavior (switching from 0 available to 1 available)
maximumValue: qualityModel.totalTicks
stepSize: 1
style: SliderStyle
Item
{
height: childrenRect.height
anchors
{
//Draw Unvailable line
groove: Item
{
Rectangle
{
height: UM.Theme.getSize("print_setup_slider_groove").height
width: control.width - UM.Theme.getSize("print_setup_slider_handle").width
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
color: UM.Theme.getColor("quality_slider_unavailable")
}
}
handle: Item {}
tickmarks: Repeater
{
id: qualityRepeater
model: qualityModel.totalTicks > 0 ? qualityModel : 0
Rectangle
{
color: Cura.QualityProfilesDropDownMenuModel.getItem(index).available ? UM.Theme.getColor("quality_slider_available") : UM.Theme.getColor("quality_slider_unavailable")
implicitWidth: UM.Theme.getSize("print_setup_slider_tickmarks").width
implicitHeight: UM.Theme.getSize("print_setup_slider_tickmarks").height
anchors.verticalCenter: parent.verticalCenter
// Do not use Math.round otherwise the tickmarks won't be aligned
x: ((UM.Theme.getSize("print_setup_slider_handle").width / 2) - (implicitWidth / 2) + (qualityModel.qualitySliderStepWidth * index))
radius: Math.round(implicitWidth / 2)
}
}
left: parent.left
right: parent.right
}
// Create a mouse area on top of the unavailable profiles to show a specific tooltip
MouseArea
Cura.IconWithText
{
anchors.fill: parent
hoverEnabled: true
enabled: !Cura.MachineManager.hasCustomQuality
id: profileLabel
source: UM.Theme.getIcon("category_layer_height")
text: catalog.i18nc("@label", "Profiles")
font: UM.Theme.getFont("medium")
width: labelColumnWidth
}
UM.SimpleButton
{
id: customisedSettings
visible: Cura.SimpleModeSettingsManager.isProfileCustomized || Cura.MachineManager.hasCustomQuality
height: visible ? UM.Theme.getSize("print_setup_icon").height : 0
width: height
anchors
{
right: profileLabel.right
rightMargin: UM.Theme.getSize("default_margin").width
leftMargin: UM.Theme.getSize("default_margin").width
verticalCenter: parent.verticalCenter
}
color: hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button")
iconSource: UM.Theme.getIcon("reset")
onClicked:
{
// if the current profile is user-created, switch to a built-in quality
Cura.MachineManager.resetToUseDefaultQuality()
}
onEntered:
{
var tooltipContent = catalog.i18nc("@tooltip", "This quality profile is not available for your current material and nozzle configuration. Please change these to enable this quality profile.")
base.showTooltip(qualityRow, Qt.point(-UM.Theme.getSize("thick_margin").width, customisedSettings.height), tooltipContent)
var tooltipContent = catalog.i18nc("@tooltip","You have modified some profile settings. If you want to change these go to custom mode.")
base.showTooltip(qualityRow, Qt.point(-UM.Theme.getSize("thick_margin").width, 0), tooltipContent)
}
onExited: base.hideTooltip()
}
}
// Draw available slider
Slider
{
id: qualitySlider
width: qualityModel.qualitySliderStepWidth * (qualityModel.availableTotalTicks - 1) + UM.Theme.getSize("print_setup_slider_handle").width
height: UM.Theme.getSize("print_setup_slider_handle").height // The handle is the widest element of the slider
enabled: qualityModel.totalTicks > 0 && !Cura.SimpleModeSettingsManager.isProfileCustomized
visible: qualityModel.availableTotalTicks > 0
updateValueWhileDragging : false
anchors
Cura.LabelBar
{
right: parent.right
rightMargin: qualityModel.qualitySliderMarginRight
}
minimumValue: qualityModel.qualitySliderAvailableMin >= 0 ? qualityModel.qualitySliderAvailableMin : 0
// maximumValue must be greater than minimumValue to be able to see the handle. While the value is strictly
// speaking not always correct, it seems to have the correct behavior (switching from 0 available to 1 available)
maximumValue: qualityModel.qualitySliderAvailableMax >= 1 ? qualityModel.qualitySliderAvailableMax : 1
stepSize: 1
value: qualityModel.qualitySliderActiveIndex
style: SliderStyle
{
// Draw Available line
groove: Item
id: labelbar
anchors
{
Rectangle
{
height: UM.Theme.getSize("print_setup_slider_groove").height
width: control.width - UM.Theme.getSize("print_setup_slider_handle").width
anchors.verticalCenter: parent.verticalCenter
// Do not use Math.round otherwise the tickmarks won't be aligned
x: UM.Theme.getSize("print_setup_slider_handle").width / 2
color: UM.Theme.getColor("quality_slider_available")
}
left: profileLabel.right
right: parent.right
}
handle: Rectangle
{
id: qualityhandleButton
color: UM.Theme.getColor("primary")
implicitWidth: UM.Theme.getSize("print_setup_slider_handle").width
implicitHeight: implicitWidth
radius: Math.round(implicitWidth / 2)
visible: !Cura.SimpleModeSettingsManager.isProfileCustomized && !Cura.MachineManager.hasCustomQuality && qualityModel.existingQualityProfile
}
}
onValueChanged:
{
// only change if an active machine is set and the slider is visible at all.
if (Cura.MachineManager.activeMachine != null && visible)
{
// prevent updating during view initializing. Trigger only if the value changed by user
if (qualitySlider.value != qualityModel.qualitySliderActiveIndex && qualityModel.qualitySliderActiveIndex != -1)
{
// start updating with short delay
qualitySliderChangeTimer.start()
}
}
}
// This mouse area is only used to capture the onHover state and don't propagate it to the unavailable mouse area
MouseArea
{
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
enabled: !Cura.MachineManager.hasCustomQuality
model: Cura.QualityProfilesDropDownMenuModel
modelKey: "layer_height"
}
}
// This mouse area will only take the mouse events and show a tooltip when the profile in use is
// a user created profile
MouseArea
{
anchors.fill: parent
hoverEnabled: true
visible: Cura.MachineManager.hasCustomQuality
onEntered:
Repeater
{
model: Cura.IntentCategoryModel {}
Item
{
var tooltipContent = catalog.i18nc("@tooltip", "A custom profile is currently active. To enable the quality slider, choose a default quality profile in Custom tab")
base.showTooltip(qualityRow, Qt.point(-UM.Theme.getSize("thick_margin").width, customisedSettings.height), tooltipContent)
anchors
{
left: parent.left
right: parent.right
}
height: intentCategoryLabel.height
Label
{
id: intentCategoryLabel
text: model.name
width: labelColumnWidth - UM.Theme.getSize("section_icon").width
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("section_icon").width + UM.Theme.getSize("narrow_margin").width
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
elide: Text.ElideRight
}
NoIntentIcon
{
affected_extruders: Cura.MachineManager.extruderPositionsWithNonActiveIntent
intent_type: model.name
anchors.right: intentCategoryLabel.right
anchors.rightMargin: UM.Theme.getSize("narrow_margin").width
width: intentCategoryLabel.height * 0.75
anchors.verticalCenter: parent.verticalCenter
height: width
visible: Cura.MachineManager.activeIntentCategory == model.intent_category && affected_extruders.length
}
Cura.RadioCheckbar
{
anchors
{
left: intentCategoryLabel.right
right: parent.right
}
dataModel: model["qualities"]
buttonGroup: activeProfileButtonGroup
function checkedFunction(modelItem)
{
if(Cura.MachineManager.hasCustomQuality)
{
// When user created profile is active, no quality tickbox should be active.
return false
}
if(modelItem === null)
{
return false
}
return Cura.MachineManager.activeQualityType == modelItem.quality_type && Cura.MachineManager.activeIntentCategory == modelItem.intent_category
}
isCheckedFunction: checkedFunction
}
}
onExited: base.hideTooltip()
}
}
}

View file

@ -0,0 +1,152 @@
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import UM 1.1 as UM
Item
{
id: base
property ButtonGroup buttonGroup: null
property color activeColor: UM.Theme.getColor("primary")
property color inactiveColor: UM.Theme.getColor("slider_groove")
property color defaultItemColor: UM.Theme.getColor("small_button_active")
property int checkboxSize: Math.round(UM.Theme.getSize("radio_button").height * 0.75)
property int inactiveMarkerSize: 2 * barSize
property int barSize: UM.Theme.getSize("slider_groove_radius").height
property var isCheckedFunction // Function that accepts the modelItem and returns if the item should be active.
implicitWidth: 200 * screenScaleFactor
implicitHeight: checkboxSize
property var dataModel: null
// The horizontal inactive bar that sits behind the buttons
Rectangle
{
id: inactiveLine
color: inactiveColor
height: barSize
anchors
{
left: buttonBar.left
right: buttonBar.right
leftMargin: Math.round((checkboxSize - inactiveMarkerSize) / 2)
rightMargin: Math.round((checkboxSize - inactiveMarkerSize) / 2)
verticalCenter: parent.verticalCenter
}
}
RowLayout
{
id: buttonBar
anchors.top: parent.top
height: checkboxSize
width: parent.width
spacing: 0
Repeater
{
id: repeater
model: base.dataModel
height: checkboxSize
Item
{
Layout.fillWidth: true
Layout.fillHeight: true
// The last item of the repeater needs to be shorter, as we don't need another part to fit
// the horizontal bar. The others should essentially not be limited.
Layout.maximumWidth: index + 1 === repeater.count ? activeComponent.width : 200000000
property bool isEnabled: model.available
// The horizontal bar between the checkable options.
// Note that the horizontal bar points towards the previous item.
Rectangle
{
property Item previousItem: repeater.itemAt(index - 1)
height: barSize
width: Math.round(buttonBar.width / (repeater.count - 1) - activeComponent.width - 2)
color: defaultItemColor
anchors
{
right: activeComponent.left
verticalCenter: parent.verticalCenter
}
visible: previousItem !== null && previousItem.isEnabled && isEnabled
}
Loader
{
id: activeComponent
sourceComponent: isEnabled? checkboxComponent : disabledComponent
width: checkboxSize
property var modelItem: model
}
}
}
}
Component
{
id: disabledComponent
Item
{
height: checkboxSize
width: checkboxSize
Rectangle
{
// This can (and should) be done wiht a verticalCenter. For some reason it does work in QtCreator
// but not when using the exact same QML in Cura.
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
height: inactiveMarkerSize
width: inactiveMarkerSize
radius: Math.round(width / 2)
color: inactiveColor
}
}
}
Component
{
id: checkboxComponent
CheckBox
{
id: checkbox
ButtonGroup.group: buttonGroup
width: checkboxSize
height: checkboxSize
property var modelData: modelItem
checked: isCheckedFunction(modelItem)
indicator: Rectangle
{
height: checkboxSize
width: checkboxSize
radius: Math.round(width / 2)
border.color: defaultItemColor
Rectangle
{
anchors
{
fill: parent
}
radius: Math.round(width / 2)
color: activeColor
visible: checkbox.checked
}
}
}
}
}

View file

@ -20,7 +20,6 @@ SettingItem
textRole: "value"
anchors.fill: parent
highlighted: base.hovered
onActivated:
{

View file

@ -86,7 +86,11 @@ Item
{
id: machineList
cacheBuffer: 1000000 // Set a large cache to effectively just cache every list item.
// CURA-6793
// Enabling the buffer seems to cause the blank items issue. When buffer is enabled, if the ListView's
// individual item has a dynamic change on its visibility, the ListView doesn't redraw itself.
// The default value of cacheBuffer is platform-dependent, so we explicitly disable it here.
cacheBuffer: 0
model: UM.DefinitionContainersModel
{

View file

@ -14,40 +14,34 @@ import Cura 1.1 as Cura
ComboBox
{
id: control
property bool highlighted: false
states: [
State
{
name: "disabled"
when: !control.enabled
PropertyChanges { target: backgroundRectangle.border; color: UM.Theme.getColor("setting_control_disabled_border")}
PropertyChanges { target: backgroundRectangle; color: UM.Theme.getColor("setting_control_disabled")}
PropertyChanges { target: contentLabel; color: UM.Theme.getColor("setting_control_disabled_text")}
},
State
{
name: "highlighted"
when: control.hovered || control.activeFocus
PropertyChanges { target: backgroundRectangle.border; color: UM.Theme.getColor("setting_control_border_highlight") }
PropertyChanges { target: backgroundRectangle; color: UM.Theme.getColor("setting_control_highlight")}
}
]
background: Rectangle
{
color:
{
if (!enabled)
{
return UM.Theme.getColor("setting_control_disabled")
}
if (control.hovered || control.activeFocus || control.highlighted)
{
return UM.Theme.getColor("setting_control_highlight")
}
return UM.Theme.getColor("setting_control")
}
id: backgroundRectangle
color: UM.Theme.getColor("setting_control")
radius: UM.Theme.getSize("setting_control_radius").width
border.width: UM.Theme.getSize("default_lining").width
border.color:
{
if (!enabled)
{
return UM.Theme.getColor("setting_control_disabled_border")
}
border.color: UM.Theme.getColor("setting_control_border")
if (control.hovered || control.activeFocus || control.highlighted)
{
return UM.Theme.getColor("setting_control_border_highlight")
}
return UM.Theme.getColor("setting_control_border")
}
}
indicator: UM.RecolorImage
@ -67,6 +61,7 @@ ComboBox
contentItem: Label
{
id: contentLabel
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("setting_unit_margin").width
anchors.verticalCenter: parent.verticalCenter
@ -76,7 +71,7 @@ ComboBox
textFormat: Text.PlainText
renderType: Text.NativeRendering
font: UM.Theme.getFont("default")
color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text")
color: UM.Theme.getColor("setting_control_text")
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}

View file

@ -8,6 +8,7 @@ setting_version = 10
type = quality
quality_type = coarse
weight = 3
global_quality = True
[values]
layer_height = 0.08

View file

@ -8,6 +8,7 @@ setting_version = 10
type = quality
quality_type = draft
weight = -2
global_quality = True
[values]
layer_height = 0.1

View file

@ -8,6 +8,7 @@ setting_version = 10
type = quality
quality_type = extra_high
weight = 0
global_quality = True
[values]
layer_height = 0.02

View file

@ -8,6 +8,7 @@ setting_version = 10
type = quality
quality_type = high
weight = 1
global_quality = True
[values]
layer_height = 0.04

View file

@ -8,6 +8,7 @@ setting_version = 10
type = quality
quality_type = normal
weight = 0
global_quality = True
[values]
layer_height = 0.06

View file

@ -8,6 +8,7 @@ setting_version = 10
type = quality
quality_type = draft
weight = -2
global_quality = True
[values]
brim_width = 4.0

View file

@ -8,6 +8,7 @@ setting_version = 10
type = quality
quality_type = high
weight = 1
global_quality = True
[values]
brim_width = 4.0

View file

@ -8,6 +8,7 @@ setting_version = 10
type = quality
quality_type = normal
weight = 0
global_quality = True
[values]
brim_width = 4.0

View file

@ -34,8 +34,8 @@ layer_start_x = 250
layer_start_y = 250
coasting_enable = False
wall_line_count = 2
material_print_temperature = =default_material_print_temperature
material_initial_print_temperature = =material_print_temperature
material_final_print_temperature = =material_print_temperature
material_print_temperature = =default_material_print_temperature
material_initial_print_temperature = =material_print_temperature
material_final_print_temperature = =material_print_temperature
z_seam_corner = z_seam_corner_none
optimize_wall_printing_order = True

View file

@ -1,36 +0,0 @@
[general]
version = 4
name = Fast
definition = ultimaker_s5
[metadata]
setting_version = 10
type = quality
quality_type = draft
weight = -2
material = generic_abs
variant = AA 0.4
buildplate = Aluminum
[values]
machine_nozzle_cool_down_speed = 0.85
machine_nozzle_heat_up_speed = 1.5
material_print_temperature = =default_material_print_temperature + 20
material_initial_print_temperature = =material_print_temperature - 15
material_final_print_temperature = =material_print_temperature - 20
prime_tower_enable = False
skin_overlap = 20
speed_print = 60
speed_layer_0 = 20
speed_topbottom = =math.ceil(speed_print * 35 / 60)
speed_wall = =math.ceil(speed_print * 45 / 60)
speed_wall_0 = =math.ceil(speed_wall * 35 / 45)
wall_thickness = 1
infill_line_width = =round(line_width * 0.4 / 0.35, 2)
speed_infill = =math.ceil(speed_print * 50 / 60)
material_bed_temperature_layer_0 = 100
default_material_bed_temperature = 90
prime_blob_enable = False
layer_height_0 = 0.17

View file

@ -1,35 +0,0 @@
[general]
version = 4
name = Normal
definition = ultimaker_s5
[metadata]
setting_version = 10
type = quality
quality_type = fast
weight = -1
material = generic_abs
variant = AA 0.4
buildplate = Aluminum
[values]
cool_min_speed = 7
machine_nozzle_cool_down_speed = 0.85
machine_nozzle_heat_up_speed = 1.5
material_print_temperature = =default_material_print_temperature + 15
material_initial_print_temperature = =material_print_temperature - 15
material_final_print_temperature = =material_print_temperature - 20
prime_tower_enable = False
speed_print = 60
speed_layer_0 = 20
speed_topbottom = =math.ceil(speed_print * 30 / 60)
speed_wall = =math.ceil(speed_print * 40 / 60)
speed_wall_0 = =math.ceil(speed_wall * 30 / 40)
infill_line_width = =round(line_width * 0.4 / 0.35, 2)
speed_infill = =math.ceil(speed_print * 45 / 60)
material_bed_temperature_layer_0 = 100
default_material_bed_temperature = 90
prime_blob_enable = False
layer_height_0 = 0.17

View file

@ -1,34 +0,0 @@
[general]
version = 4
name = Extra Fine
definition = ultimaker_s5
[metadata]
setting_version = 10
type = quality
quality_type = high
weight = 1
material = generic_abs
variant = AA 0.4
buildplate = Aluminum
[values]
cool_min_speed = 12
machine_nozzle_cool_down_speed = 0.8
machine_nozzle_heat_up_speed = 1.5
material_print_temperature = =default_material_print_temperature + 5
material_initial_print_temperature = =material_print_temperature - 15
material_final_print_temperature = =material_print_temperature - 20
prime_tower_enable = False
speed_print = 50
speed_layer_0 = 20
speed_topbottom = =math.ceil(speed_print * 30 / 50)
speed_wall = =math.ceil(speed_print * 30 / 50)
infill_line_width = =round(line_width * 0.4 / 0.35, 2)
speed_infill = =math.ceil(speed_print * 40 / 50)
material_bed_temperature_layer_0 = 100
default_material_bed_temperature = 90
prime_blob_enable = False
layer_height_0 = 0.17

View file

@ -1,34 +0,0 @@
[general]
version = 4
name = Fine
definition = ultimaker_s5
[metadata]
setting_version = 10
type = quality
quality_type = normal
weight = 0
material = generic_abs
variant = AA 0.4
buildplate = Aluminum
[values]
machine_nozzle_cool_down_speed = 0.85
machine_nozzle_heat_up_speed = 1.5
material_print_temperature = =default_material_print_temperature + 10
material_initial_print_temperature = =material_print_temperature - 15
material_final_print_temperature = =material_print_temperature - 20
prime_tower_enable = False
speed_print = 55
speed_layer_0 = 20
speed_topbottom = =math.ceil(speed_print * 30 / 55)
speed_wall = =math.ceil(speed_print * 30 / 55)
infill_line_width = =round(line_width * 0.4 / 0.35, 2)
speed_infill = =math.ceil(speed_print * 40 / 55)
material_bed_temperature_layer_0 = 100
default_material_bed_temperature = 90
prime_blob_enable = False
layer_height_0 = 0.17

View file

@ -1,54 +0,0 @@
[general]
version = 4
name = Fast
definition = ultimaker_s5
[metadata]
setting_version = 10
type = quality
quality_type = draft
weight = -2
material = generic_cpe_plus
variant = AA 0.4
buildplate = Aluminum
[values]
acceleration_enabled = True
acceleration_print = 4000
cool_fan_speed_max = 80
cool_min_speed = 5
infill_line_width = =round(line_width * 0.35 / 0.35, 2)
infill_overlap = 0
infill_wipe_dist = 0
jerk_enabled = True
jerk_print = 25
machine_min_cool_heat_time_window = 15
material_final_print_temperature = =material_print_temperature - 10
material_initial_print_temperature = =material_print_temperature - 5
material_print_temperature = =default_material_print_temperature + 10
material_print_temperature_layer_0 = =material_print_temperature
multiple_mesh_overlap = 0
prime_tower_enable = True
prime_tower_wipe_enabled = True
retraction_combing_max_distance = 50
retraction_extrusion_window = 1
retraction_hop = 0.2
retraction_hop_enabled = False
retraction_hop_only_when_collides = True
skin_overlap = 20
speed_layer_0 = 20
speed_print = 50
speed_topbottom = =math.ceil(speed_print * 40 / 50)
speed_wall = =math.ceil(speed_print * 50 / 50)
speed_wall_0 = =math.ceil(speed_wall * 40 / 50)
support_bottom_distance = =support_z_distance
support_z_distance = =layer_height
wall_0_inset = 0
wall_thickness = 1
material_bed_temperature_layer_0 = 115
default_material_bed_temperature = 105
prime_blob_enable = False
layer_height_0 = 0.17

Some files were not shown because too many files have changed in this diff Show more