1# Copyright (c) 2020 Ultimaker B.V. 2# Cura is released under the terms of the LGPLv3 or higher. 3 4import time 5import re 6import unicodedata 7from typing import Any, List, Dict, TYPE_CHECKING, Optional, cast, Set 8 9from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer 10 11from UM.ConfigurationErrorMessage import ConfigurationErrorMessage 12from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator 13from UM.Settings.InstanceContainer import InstanceContainer 14from UM.Settings.Interfaces import ContainerInterface 15from UM.Signal import Signal 16from UM.FlameProfiler import pyqtSlot 17from UM import Util 18from UM.Logger import Logger 19from UM.Message import Message 20 21from UM.Settings.SettingFunction import SettingFunction 22from UM.Signal import postponeSignals, CompressTechnique 23 24import cura.CuraApplication # Imported like this to prevent circular references. 25from UM.Util import parseBool 26 27from cura.Machines.ContainerNode import ContainerNode 28from cura.Machines.ContainerTree import ContainerTree 29from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel 30 31from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionType 32from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel 33from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel 34from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel 35from cura.Settings.CuraContainerRegistry import CuraContainerRegistry 36from cura.Settings.ExtruderManager import ExtruderManager 37from cura.Settings.ExtruderStack import ExtruderStack 38from cura.Settings.cura_empty_instance_containers import (empty_definition_changes_container, empty_variant_container, 39 empty_material_container, empty_quality_container, 40 empty_quality_changes_container, empty_intent_container) 41from cura.UltimakerCloud.UltimakerCloudConstants import META_UM_LINKED_TO_ACCOUNT 42 43from .CuraStackBuilder import CuraStackBuilder 44 45from UM.i18n import i18nCatalog 46catalog = i18nCatalog("cura") 47from cura.Settings.GlobalStack import GlobalStack 48if TYPE_CHECKING: 49 from cura.CuraApplication import CuraApplication 50 from cura.Machines.MaterialNode import MaterialNode 51 from cura.Machines.QualityChangesGroup import QualityChangesGroup 52 from cura.Machines.QualityGroup import QualityGroup 53 from cura.Machines.VariantNode import VariantNode 54 55 56class MachineManager(QObject): 57 def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None: 58 super().__init__(parent) 59 60 self._active_container_stack = None # type: Optional[ExtruderStack] 61 self._global_container_stack = None # type: Optional[GlobalStack] 62 63 self._current_root_material_id = {} # type: Dict[str, str] 64 65 self._default_extruder_position = "0" # to be updated when extruders are switched on and off 66 self._num_user_settings = 0 67 68 self._instance_container_timer = QTimer() # type: QTimer 69 self._instance_container_timer.setInterval(250) 70 self._instance_container_timer.setSingleShot(True) 71 self._instance_container_timer.timeout.connect(self.__emitChangedSignals) 72 73 self._application = application 74 self._container_registry = self._application.getContainerRegistry() 75 self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged) 76 self._container_registry.containerLoadComplete.connect(self._onContainersChanged) 77 78 # When the global container is changed, active material probably needs to be updated. 79 self.globalContainerChanged.connect(self.activeMaterialChanged) 80 self.globalContainerChanged.connect(self.activeVariantChanged) 81 self.globalContainerChanged.connect(self.activeQualityChanged) 82 83 self.globalContainerChanged.connect(self.activeQualityChangesGroupChanged) 84 self.globalContainerChanged.connect(self.activeQualityGroupChanged) 85 86 self._stacks_have_errors = None # type: Optional[bool] 87 88 extruder_manager = self._application.getExtruderManager() 89 90 extruder_manager.activeExtruderChanged.connect(self._onActiveExtruderStackChanged) 91 extruder_manager.activeExtruderChanged.connect(self.activeMaterialChanged) 92 extruder_manager.activeExtruderChanged.connect(self.activeVariantChanged) 93 extruder_manager.activeExtruderChanged.connect(self.activeQualityChanged) 94 95 self.globalContainerChanged.connect(self.activeStackChanged) 96 ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeStackChanged) 97 self.activeStackChanged.connect(self.activeStackValueChanged) 98 99 self._application.getPreferences().addPreference("cura/active_machine", "") 100 101 self._printer_output_devices = [] # type: List[PrinterOutputDevice] 102 self._application.getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) 103 # There might already be some output devices by the time the signal is connected 104 self._onOutputDevicesChanged() 105 106 self._current_printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer 107 self.activeMaterialChanged.connect(self._onCurrentConfigurationChanged) 108 self.activeVariantChanged.connect(self._onCurrentConfigurationChanged) 109 # Force to compute the current configuration 110 self._onCurrentConfigurationChanged() 111 112 self._application.callLater(self.setInitialActiveMachine) 113 114 containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) # type: List[InstanceContainer] 115 if containers: 116 containers[0].nameChanged.connect(self._onMaterialNameChanged) 117 118 self.rootMaterialChanged.connect(self._onRootMaterialChanged) 119 120 # Emit the printerConnectedStatusChanged when either globalContainerChanged or outputDevicesChanged are emitted 121 self.globalContainerChanged.connect(self.printerConnectedStatusChanged) 122 self.outputDevicesChanged.connect(self.printerConnectedStatusChanged) 123 124 # For updating active quality display name 125 self.activeQualityChanged.connect(self.activeQualityDisplayNameChanged) 126 self.activeIntentChanged.connect(self.activeQualityDisplayNameChanged) 127 self.activeQualityGroupChanged.connect(self.activeQualityDisplayNameChanged) 128 self.activeQualityChangesGroupChanged.connect(self.activeQualityDisplayNameChanged) 129 130 self.activeStackValueChanged.connect(self._reCalculateNumUserSettings) 131 self.numberExtrudersEnabledChanged.connect(self.correctPrintSequence) 132 133 activeQualityDisplayNameChanged = pyqtSignal() 134 135 activeQualityGroupChanged = pyqtSignal() 136 activeQualityChangesGroupChanged = pyqtSignal() 137 138 globalContainerChanged = pyqtSignal() # Emitted whenever the global stack is changed (ie: when changing between printers, changing a global profile, but not when changing a value) 139 activeMaterialChanged = pyqtSignal() 140 activeVariantChanged = pyqtSignal() 141 activeQualityChanged = pyqtSignal() 142 activeIntentChanged = pyqtSignal() 143 activeStackChanged = pyqtSignal() # Emitted whenever the active extruder stack is changed (ie: when switching the active extruder tab or changing between printers) 144 extruderChanged = pyqtSignal() # Emitted whenever an extruder is activated or deactivated or the default extruder changes. 145 146 activeStackValueChanged = pyqtSignal() # Emitted whenever a value inside the active stack is changed. 147 activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed 148 stacksValidationChanged = pyqtSignal() # Emitted whenever a validation is changed 149 numberExtrudersEnabledChanged = pyqtSignal() # Emitted when the number of extruders that are enabled changed 150 151 blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly 152 153 outputDevicesChanged = pyqtSignal() 154 currentConfigurationChanged = pyqtSignal() # Emitted every time the current configurations of the machine changes 155 printerConnectedStatusChanged = pyqtSignal() # Emitted every time the active machine change or the outputdevices change 156 157 rootMaterialChanged = pyqtSignal() 158 numUserSettingsChanged = pyqtSignal() 159 160 def _reCalculateNumUserSettings(self): 161 if not self._global_container_stack: 162 if self._num_user_settings != 0: 163 self.numUserSettingsChanged.emit() 164 self._num_user_settings = 0 165 return 166 num_user_settings = self._global_container_stack.getTop().getNumInstances() 167 stacks = self._global_container_stack.extruderList 168 for stack in stacks: 169 num_user_settings += stack.getTop().getNumInstances() 170 171 if self._num_user_settings != num_user_settings: 172 self._num_user_settings = num_user_settings 173 self.numUserSettingsChanged.emit() 174 175 def setInitialActiveMachine(self) -> None: 176 active_machine_id = self._application.getPreferences().getValue("cura/active_machine") 177 if active_machine_id != "" and CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = active_machine_id): 178 # An active machine was saved, so restore it. 179 self.setActiveMachine(active_machine_id) 180 181 def _onOutputDevicesChanged(self) -> None: 182 self._printer_output_devices = [] 183 for printer_output_device in self._application.getOutputDeviceManager().getOutputDevices(): 184 if isinstance(printer_output_device, PrinterOutputDevice): 185 self._printer_output_devices.append(printer_output_device) 186 187 self.outputDevicesChanged.emit() 188 189 @pyqtProperty(QObject, notify = currentConfigurationChanged) 190 def currentConfiguration(self) -> PrinterConfigurationModel: 191 return self._current_printer_configuration 192 193 def _onCurrentConfigurationChanged(self) -> None: 194 if not self._global_container_stack: 195 return 196 197 # Create the configuration model with the current data in Cura 198 self._current_printer_configuration.printerType = self._global_container_stack.definition.getName() 199 200 if len(self._current_printer_configuration.extruderConfigurations) != len(self._global_container_stack.extruderList): 201 self._current_printer_configuration.extruderConfigurations = [ExtruderConfigurationModel() for extruder in self._global_container_stack.extruderList] 202 203 for extruder, extruder_configuration in zip(self._global_container_stack.extruderList, self._current_printer_configuration.extruderConfigurations): 204 # For compare just the GUID is needed at this moment 205 mat_type = extruder.material.getMetaDataEntry("material") if extruder.material != empty_material_container else None 206 mat_guid = extruder.material.getMetaDataEntry("GUID") if extruder.material != empty_material_container else None 207 mat_color = extruder.material.getMetaDataEntry("color_name") if extruder.material != empty_material_container else None 208 mat_brand = extruder.material.getMetaDataEntry("brand") if extruder.material != empty_material_container else None 209 mat_name = extruder.material.getMetaDataEntry("name") if extruder.material != empty_material_container else None 210 material_model = MaterialOutputModel(mat_guid, mat_type, mat_color, mat_brand, mat_name) 211 212 extruder_configuration.position = int(extruder.getMetaDataEntry("position")) 213 extruder_configuration.material = material_model 214 extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != empty_variant_container else None 215 216 # An empty build plate configuration from the network printer is presented as an empty string, so use "" for an 217 # empty build plate. 218 self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value")\ 219 if self._global_container_stack.variant != empty_variant_container else self._global_container_stack.getProperty("machine_buildplate_type", "default_value") 220 self.currentConfigurationChanged.emit() 221 222 @pyqtSlot(QObject, result = bool) 223 def matchesConfiguration(self, configuration: PrinterConfigurationModel) -> bool: 224 return self._current_printer_configuration == configuration 225 226 @pyqtProperty("QVariantList", notify = outputDevicesChanged) 227 def printerOutputDevices(self) -> List[PrinterOutputDevice]: 228 return self._printer_output_devices 229 230 @pyqtProperty(int, constant=True) 231 def totalNumberOfSettings(self) -> int: 232 return len(self.getAllSettingKeys()) 233 234 def getAllSettingKeys(self) -> Set[str]: 235 general_definition_containers = CuraContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter") 236 if not general_definition_containers: 237 return set() 238 return general_definition_containers[0].getAllKeys() 239 240 def _onGlobalContainerChanged(self) -> None: 241 """Triggered when the global container stack is changed in CuraApplication.""" 242 243 if self._global_container_stack: 244 try: 245 self._global_container_stack.containersChanged.disconnect(self._onContainersChanged) 246 except TypeError: 247 pass 248 try: 249 self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) 250 except TypeError: 251 pass 252 253 for extruder_stack in self._global_container_stack.extruderList: 254 extruder_stack.propertyChanged.disconnect(self._onPropertyChanged) 255 extruder_stack.containersChanged.disconnect(self._onContainersChanged) 256 257 # Update the local global container stack reference 258 self._global_container_stack = self._application.getGlobalContainerStack() 259 if self._global_container_stack: 260 self.updateDefaultExtruder() 261 self.updateNumberExtrudersEnabled() 262 self.globalContainerChanged.emit() 263 264 # After switching the global stack we reconnect all the signals and set the variant and material references 265 if self._global_container_stack: 266 self._application.getPreferences().setValue("cura/active_machine", self._global_container_stack.getId()) 267 268 self._global_container_stack.containersChanged.connect(self._onContainersChanged) 269 self._global_container_stack.propertyChanged.connect(self._onPropertyChanged) 270 271 # Global stack can have only a variant if it is a buildplate 272 global_variant = self._global_container_stack.variant 273 if global_variant != empty_variant_container: 274 if global_variant.getMetaDataEntry("hardware_type") != "buildplate": 275 self._global_container_stack.setVariant(empty_variant_container) 276 277 # Set the global material to empty as we now use the extruder stack at all times - CURA-4482 278 global_material = self._global_container_stack.material 279 if global_material != empty_material_container: 280 self._global_container_stack.setMaterial(empty_material_container) 281 282 # Listen for changes on all extruder stacks 283 for extruder_stack in self._global_container_stack.extruderList: 284 extruder_stack.propertyChanged.connect(self._onPropertyChanged) 285 extruder_stack.containersChanged.connect(self._onContainersChanged) 286 287 self._onRootMaterialChanged() 288 289 self.activeQualityGroupChanged.emit() 290 291 def _onActiveExtruderStackChanged(self) -> None: 292 self.blurSettings.emit() # Ensure no-one has focus. 293 self._active_container_stack = ExtruderManager.getInstance().getActiveExtruderStack() 294 295 def __emitChangedSignals(self) -> None: 296 self.activeQualityChanged.emit() 297 self.activeVariantChanged.emit() 298 self.activeMaterialChanged.emit() 299 self.activeIntentChanged.emit() 300 301 self.rootMaterialChanged.emit() 302 self.numberExtrudersEnabledChanged.emit() 303 304 def _onContainersChanged(self, container: ContainerInterface) -> None: 305 self._instance_container_timer.start() 306 307 def _onPropertyChanged(self, key: str, property_name: str) -> None: 308 if property_name == "value": 309 # Notify UI items, such as the "changed" star in profile pull down menu. 310 self.activeStackValueChanged.emit() 311 312 @pyqtSlot(str) 313 def setActiveMachine(self, stack_id: Optional[str]) -> None: 314 self.blurSettings.emit() # Ensure no-one has focus. 315 316 if not stack_id: 317 self._application.setGlobalContainerStack(None) 318 self.globalContainerChanged.emit() 319 self._application.showAddPrintersUncancellableDialog.emit() 320 return 321 322 container_registry = CuraContainerRegistry.getInstance() 323 containers = container_registry.findContainerStacks(id = stack_id) 324 if not containers: 325 return 326 327 global_stack = cast(GlobalStack, containers[0]) 328 329 # Make sure that the default machine actions for this machine have been added 330 self._application.getMachineActionManager().addDefaultMachineActions(global_stack) 331 332 extruder_manager = ExtruderManager.getInstance() 333 extruder_manager.fixSingleExtrusionMachineExtruderDefinition(global_stack) 334 if not global_stack.isValid(): 335 # Mark global stack as invalid 336 ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId()) 337 return # We're done here 338 339 self._global_container_stack = global_stack 340 extruder_manager.addMachineExtruders(global_stack) 341 self._application.setGlobalContainerStack(global_stack) 342 343 # Switch to the first enabled extruder 344 self.updateDefaultExtruder() 345 default_extruder_position = int(self.defaultExtruderPosition) 346 old_active_extruder_index = extruder_manager.activeExtruderIndex 347 extruder_manager.setActiveExtruderIndex(default_extruder_position) 348 if old_active_extruder_index == default_extruder_position: 349 # This signal might not have been emitted yet (if it didn't change) but we still want the models to update that depend on it because we changed the contents of the containers too. 350 extruder_manager.activeExtruderChanged.emit() 351 352 self._validateVariantsAndMaterials(global_stack) 353 354 def _validateVariantsAndMaterials(self, global_stack) -> None: 355 # Validate if the machine has the correct variants and materials. 356 # It can happen that a variant or material is empty, even though the machine has them. This will ensure that 357 # that situation will be fixed (and not occur again, since it switches it out to the preferred variant or 358 # variant instead!) 359 machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()] 360 if not self._global_container_stack: 361 return 362 for extruder in self._global_container_stack.extruderList: 363 variant_name = extruder.variant.getName() 364 variant_node = machine_node.variants.get(variant_name) 365 if variant_node is None: 366 Logger.log("w", "An extruder has an unknown variant, switching it to the preferred variant") 367 self.setVariantByName(extruder.getMetaDataEntry("position"), machine_node.preferred_variant_name) 368 variant_node = machine_node.variants.get(machine_node.preferred_variant_name) 369 370 material_node = variant_node.materials.get(extruder.material.getMetaDataEntry("base_file")) 371 if material_node is None: 372 Logger.log("w", "An extruder has an unknown material, switching it to the preferred material") 373 if not self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material): 374 Logger.log("w", "Failed to switch to %s keeping old material instead", machine_node.preferred_material) 375 376 377 @staticmethod 378 def getMachine(definition_id: str, metadata_filter: Optional[Dict[str, str]] = None) -> Optional["GlobalStack"]: 379 """Given a definition id, return the machine with this id. 380 381 Optional: add a list of keys and values to filter the list of machines with the given definition id 382 :param definition_id: :type{str} definition id that needs to look for 383 :param metadata_filter: :type{dict} list of metadata keys and values used for filtering 384 """ 385 386 if metadata_filter is None: 387 metadata_filter = {} 388 machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) 389 for machine in machines: 390 if machine.definition.getId() == definition_id: 391 return cast(GlobalStack, machine) 392 return None 393 394 @pyqtSlot(str, result=bool) 395 @pyqtSlot(str, str, result = bool) 396 def addMachine(self, definition_id: str, name: Optional[str] = None) -> bool: 397 Logger.log("i", "Trying to add a machine with the definition id [%s]", definition_id) 398 if name is None: 399 definitions = CuraContainerRegistry.getInstance().findDefinitionContainers(id = definition_id) 400 if definitions: 401 name = definitions[0].getName() 402 else: 403 name = definition_id 404 405 new_stack = CuraStackBuilder.createMachine(cast(str, name), definition_id) 406 if new_stack: 407 # Instead of setting the global container stack here, we set the active machine and so the signals are emitted 408 self.setActiveMachine(new_stack.getId()) 409 else: 410 Logger.log("w", "Failed creating a new machine!") 411 return False 412 return True 413 414 def _checkStacksHaveErrors(self) -> bool: 415 time_start = time.time() 416 if self._global_container_stack is None: #No active machine. 417 return False 418 419 if self._global_container_stack.hasErrors(): 420 Logger.log("d", "Checking global stack for errors took %0.2f s and we found an error" % (time.time() - time_start)) 421 return True 422 423 # Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are 424 machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") 425 extruder_stacks = self._global_container_stack.extruderList 426 count = 1 # We start with the global stack 427 for stack in extruder_stacks: 428 md = stack.getMetaData() 429 if "position" in md and int(md["position"]) >= machine_extruder_count: 430 continue 431 count += 1 432 if stack.hasErrors(): 433 Logger.log("d", "Checking %s stacks for errors took %.2f s and we found an error in stack [%s]" % (count, time.time() - time_start, str(stack))) 434 return True 435 436 Logger.log("d", "Checking %s stacks for errors took %.2f s" % (count, time.time() - time_start)) 437 return False 438 439 @pyqtProperty(bool, notify = numUserSettingsChanged) 440 def hasUserSettings(self) -> bool: 441 return self._num_user_settings != 0 442 443 @pyqtProperty(int, notify = numUserSettingsChanged) 444 def numUserSettings(self) -> int: 445 return self._num_user_settings 446 447 @pyqtSlot(str) 448 def clearUserSettingAllCurrentStacks(self, key: str) -> None: 449 """Delete a user setting from the global stack and all extruder stacks. 450 451 :param key: :type{str} the name of the key to delete 452 """ 453 Logger.log("i", "Clearing the setting [%s] from all stacks", key) 454 if not self._global_container_stack: 455 return 456 457 send_emits_containers = [] 458 459 top_container = self._global_container_stack.getTop() 460 top_container.removeInstance(key, postpone_emit=True) 461 send_emits_containers.append(top_container) 462 463 linked = not self._global_container_stack.getProperty(key, "settable_per_extruder") or \ 464 self._global_container_stack.getProperty(key, "limit_to_extruder") != "-1" 465 466 if not linked: 467 stack = ExtruderManager.getInstance().getActiveExtruderStack() 468 stacks = [stack] 469 else: 470 stacks = self._global_container_stack.extruderList 471 472 for stack in stacks: 473 if stack is not None: 474 container = stack.getTop() 475 container.removeInstance(key, postpone_emit=True) 476 send_emits_containers.append(container) 477 478 for container in send_emits_containers: 479 container.sendPostponedEmits() 480 481 @pyqtProperty(bool, notify = stacksValidationChanged) 482 def stacksHaveErrors(self) -> bool: 483 """Check if none of the stacks contain error states 484 485 Note that the _stacks_have_errors is cached due to performance issues 486 Calling _checkStack(s)ForErrors on every change is simply too expensive 487 """ 488 return bool(self._stacks_have_errors) 489 490 @pyqtProperty(str, notify = globalContainerChanged) 491 def activeMachineFirmwareVersion(self) -> str: 492 if not self._printer_output_devices: 493 return "" 494 return self._printer_output_devices[0].firmwareVersion 495 496 @pyqtProperty(str, notify = globalContainerChanged) 497 def activeMachineAddress(self) -> str: 498 if not self._printer_output_devices: 499 return "" 500 return self._printer_output_devices[0].address 501 502 @pyqtProperty(bool, notify = printerConnectedStatusChanged) 503 def printerConnected(self) -> bool: 504 return bool(self._printer_output_devices) 505 506 @pyqtProperty(bool, notify = printerConnectedStatusChanged) 507 def activeMachineIsGroup(self) -> bool: 508 if self.activeMachine is None: 509 return False 510 511 group_size = int(self.activeMachine.getMetaDataEntry("group_size", "-1")) 512 return group_size > 1 513 514 @pyqtProperty(bool, notify = printerConnectedStatusChanged) 515 def activeMachineIsLinkedToCurrentAccount(self) -> bool: 516 return parseBool(self.activeMachine.getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "True")) 517 518 @pyqtProperty(bool, notify = printerConnectedStatusChanged) 519 def activeMachineHasNetworkConnection(self) -> bool: 520 # A network connection is only available if any output device is actually a network connected device. 521 return any(d.connectionType == ConnectionType.NetworkConnection for d in self._printer_output_devices) 522 523 @pyqtProperty(bool, notify = printerConnectedStatusChanged) 524 def activeMachineHasCloudConnection(self) -> bool: 525 # A cloud connection is only available if any output device actually is a cloud connected device. 526 return any(d.connectionType == ConnectionType.CloudConnection for d in self._printer_output_devices) 527 528 @pyqtProperty(bool, notify = printerConnectedStatusChanged) 529 def activeMachineHasCloudRegistration(self) -> bool: 530 return self.activeMachine is not None and ConnectionType.CloudConnection in self.activeMachine.configuredConnectionTypes 531 532 @pyqtProperty(bool, notify = printerConnectedStatusChanged) 533 def activeMachineIsUsingCloudConnection(self) -> bool: 534 return self.activeMachineHasCloudConnection and not self.activeMachineHasNetworkConnection 535 536 def activeMachineNetworkKey(self) -> str: 537 if self._global_container_stack: 538 return self._global_container_stack.getMetaDataEntry("um_network_key", "") 539 return "" 540 541 @pyqtProperty(str, notify = printerConnectedStatusChanged) 542 def activeMachineNetworkGroupName(self) -> str: 543 if self._global_container_stack: 544 return self._global_container_stack.getMetaDataEntry("group_name", "") 545 return "" 546 547 @pyqtProperty(QObject, notify = globalContainerChanged) 548 def activeMachine(self) -> Optional["GlobalStack"]: 549 return self._global_container_stack 550 551 @pyqtProperty(str, notify = activeStackChanged) 552 def activeStackId(self) -> str: 553 if self._active_container_stack: 554 return self._active_container_stack.getId() 555 return "" 556 557 @pyqtProperty(QObject, notify = activeStackChanged) 558 def activeStack(self) -> Optional["ExtruderStack"]: 559 return self._active_container_stack 560 561 @pyqtProperty(str, notify = activeMaterialChanged) 562 def activeMaterialId(self) -> str: 563 if self._active_container_stack: 564 material = self._active_container_stack.material 565 if material: 566 return material.getId() 567 return "" 568 569 @pyqtProperty(float, notify = activeQualityGroupChanged) 570 def activeQualityLayerHeight(self) -> float: 571 """Gets the layer height of the currently active quality profile. 572 573 This is indicated together with the name of the active quality profile. 574 575 :return: The layer height of the currently active quality profile. If 576 there is no quality profile, this returns the default layer height. 577 """ 578 579 if not self._global_container_stack: 580 return 0 581 value = self._global_container_stack.getRawProperty("layer_height", "value", skip_until_container = self._global_container_stack.qualityChanges.getId()) 582 if isinstance(value, SettingFunction): 583 value = value(self._global_container_stack) 584 return value 585 586 @pyqtProperty(str, notify = activeVariantChanged) 587 def globalVariantName(self) -> str: 588 if self._global_container_stack: 589 variant = self._global_container_stack.variant 590 if variant and not isinstance(variant, type(empty_variant_container)): 591 return variant.getName() 592 return "" 593 594 @pyqtProperty(str, notify = activeQualityGroupChanged) 595 def activeQualityType(self) -> str: 596 global_stack = self._application.getGlobalContainerStack() 597 if not global_stack: 598 return "" 599 return global_stack.quality.getMetaDataEntry("quality_type") 600 601 @pyqtProperty(bool, notify = activeQualityGroupChanged) 602 def isActiveQualitySupported(self) -> bool: 603 global_container_stack = self._application.getGlobalContainerStack() 604 if not global_container_stack: 605 return False 606 active_quality_group = self.activeQualityGroup() 607 if active_quality_group is None: 608 return False 609 return active_quality_group.is_available 610 611 612 @pyqtProperty(bool, notify = activeQualityGroupChanged) 613 def isActiveQualityExperimental(self) -> bool: 614 global_container_stack = self._application.getGlobalContainerStack() 615 if not global_container_stack: 616 return False 617 active_quality_group = self.activeQualityGroup() 618 if active_quality_group is None: 619 return False 620 return active_quality_group.is_experimental 621 622 @pyqtProperty(str, notify = activeIntentChanged) 623 def activeIntentCategory(self) -> str: 624 global_container_stack = self._application.getGlobalContainerStack() 625 626 if not global_container_stack: 627 return "" 628 return global_container_stack.getIntentCategory() 629 630 # Provies a list of extruder positions that have a different intent from the active one. 631 @pyqtProperty("QStringList", notify=activeIntentChanged) 632 def extruderPositionsWithNonActiveIntent(self): 633 global_container_stack = self._application.getGlobalContainerStack() 634 635 if not global_container_stack: 636 return [] 637 638 active_intent_category = self.activeIntentCategory 639 result = [] 640 for extruder in global_container_stack.extruderList: 641 if not extruder.isEnabled: 642 continue 643 category = extruder.intent.getMetaDataEntry("intent_category", "default") 644 if category != active_intent_category: 645 result.append(str(int(extruder.getMetaDataEntry("position")) + 1)) 646 647 return result 648 649 @pyqtProperty(bool, notify = activeQualityChanged) 650 def isCurrentSetupSupported(self) -> bool: 651 """Returns whether there is anything unsupported in the current set-up. 652 653 The current set-up signifies the global stack and all extruder stacks, 654 so this indicates whether there is any container in any of the container 655 stacks that is not marked as supported. 656 """ 657 658 if not self._global_container_stack: 659 return False 660 for stack in [self._global_container_stack] + self._global_container_stack.extruderList: 661 for container in stack.getContainers(): 662 if not container: 663 return False 664 if not Util.parseBool(container.getMetaDataEntry("supported", True)): 665 return False 666 return True 667 668 @pyqtSlot(str) 669 def copyValueToExtruders(self, key: str) -> None: 670 """Copy the value of the setting of the current extruder to all other extruders as well as the global container.""" 671 672 if self._active_container_stack is None or self._global_container_stack is None: 673 return 674 new_value = self._active_container_stack.getProperty(key, "value") 675 676 # Check in which stack the value has to be replaced 677 for extruder_stack in self._global_container_stack.extruderList: 678 if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value: 679 extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved 680 681 @pyqtSlot() 682 def copyAllValuesToExtruders(self) -> None: 683 """Copy the value of all manually changed settings of the current extruder to all other extruders.""" 684 685 if self._active_container_stack is None or self._global_container_stack is None: 686 return 687 688 for extruder_stack in self._global_container_stack.extruderList: 689 if extruder_stack != self._active_container_stack: 690 for key in self._active_container_stack.userChanges.getAllKeys(): 691 new_value = self._active_container_stack.getProperty(key, "value") 692 693 # Check if the value has to be replaced 694 extruder_stack.userChanges.setProperty(key, "value", new_value) 695 696 @pyqtProperty(str, notify = globalContainerChanged) 697 def activeQualityDefinitionId(self) -> str: 698 """Get the Definition ID to use to select quality profiles for the currently active machine 699 700 :returns: DefinitionID (string) if found, empty string otherwise 701 """ 702 global_stack = self._application.getGlobalContainerStack() 703 if not global_stack: 704 return "" 705 return ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition 706 707 @pyqtProperty(str, notify = globalContainerChanged) 708 def activeDefinitionVariantsName(self) -> str: 709 """Gets how the active definition calls variants 710 711 Caveat: per-definition-variant-title is currently not translated (though the fallback is) 712 """ 713 fallback_title = catalog.i18nc("@label", "Nozzle") 714 if self._global_container_stack: 715 return self._global_container_stack.definition.getMetaDataEntry("variants_name", fallback_title) 716 717 return fallback_title 718 719 @pyqtSlot(str, str) 720 def renameMachine(self, machine_id: str, new_name: str) -> None: 721 container_registry = CuraContainerRegistry.getInstance() 722 machine_stack = container_registry.findContainerStacks(id = machine_id) 723 if machine_stack: 724 new_name = container_registry.createUniqueName("machine", machine_stack[0].getName(), new_name, machine_stack[0].definition.getName()) 725 machine_stack[0].setName(new_name) 726 self.globalContainerChanged.emit() 727 728 @pyqtSlot(str) 729 def removeMachine(self, machine_id: str) -> None: 730 Logger.log("i", "Attempting to remove a machine with the id [%s]", machine_id) 731 # If the machine that is being removed is the currently active machine, set another machine as the active machine. 732 activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id) 733 734 # Activate a new machine before removing a machine because this is safer 735 if activate_new_machine: 736 machine_stacks = CuraContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine") 737 other_machine_stacks = [s for s in machine_stacks if s["id"] != machine_id] 738 if other_machine_stacks: 739 self.setActiveMachine(other_machine_stacks[0]["id"]) 740 else: 741 self.setActiveMachine(None) 742 743 metadatas = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id) 744 if not metadatas: 745 return # machine_id doesn't exist. Nothing to remove. 746 metadata = metadatas[0] 747 ExtruderManager.getInstance().removeMachineExtruders(machine_id) 748 containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id) 749 for container in containers: 750 CuraContainerRegistry.getInstance().removeContainer(container["id"]) 751 machine_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", name = machine_id) 752 if machine_stacks: 753 CuraContainerRegistry.getInstance().removeContainer(machine_stacks[0].definitionChanges.getId()) 754 CuraContainerRegistry.getInstance().removeContainer(machine_id) 755 756 # If the printer that is being removed is a network printer, the hidden printers have to be also removed 757 group_id = metadata.get("group_id", None) 758 if group_id: 759 metadata_filter = {"group_id": group_id, "hidden": True} 760 hidden_containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) 761 if hidden_containers: 762 # This reuses the method and remove all printers recursively 763 self.removeMachine(hidden_containers[0].getId()) 764 765 @pyqtProperty(bool, notify = activeMaterialChanged) 766 def variantBuildplateCompatible(self) -> bool: 767 """The selected buildplate is compatible if it is compatible with all the materials in all the extruders""" 768 769 if not self._global_container_stack: 770 return True 771 772 buildplate_compatible = True # It is compatible by default 773 for stack in self._global_container_stack.extruderList: 774 if not stack.isEnabled: 775 continue 776 material_container = stack.material 777 if material_container == empty_material_container: 778 continue 779 if material_container.getMetaDataEntry("buildplate_compatible"): 780 active_buildplate_name = self.activeMachine.variant.name 781 buildplate_compatible = buildplate_compatible and material_container.getMetaDataEntry("buildplate_compatible")[active_buildplate_name] 782 783 return buildplate_compatible 784 785 @pyqtProperty(bool, notify = activeMaterialChanged) 786 def variantBuildplateUsable(self) -> bool: 787 """The selected buildplate is usable if it is usable for all materials OR it is compatible for one but not compatible 788 789 for the other material but the buildplate is still usable 790 """ 791 if not self._global_container_stack: 792 return True 793 794 # Here the next formula is being calculated: 795 # result = (not (material_left_compatible and material_right_compatible)) and 796 # (material_left_compatible or material_left_usable) and 797 # (material_right_compatible or material_right_usable) 798 result = not self.variantBuildplateCompatible 799 800 for stack in self._global_container_stack.extruderList: 801 material_container = stack.material 802 if material_container == empty_material_container: 803 continue 804 buildplate_compatible = material_container.getMetaDataEntry("buildplate_compatible")[self.activeVariantBuildplateName] if material_container.getMetaDataEntry("buildplate_compatible") else True 805 buildplate_usable = material_container.getMetaDataEntry("buildplate_recommended")[self.activeVariantBuildplateName] if material_container.getMetaDataEntry("buildplate_recommended") else True 806 807 result = result and (buildplate_compatible or buildplate_usable) 808 809 return result 810 811 @pyqtSlot(str, result = str) 812 def getDefinitionByMachineId(self, machine_id: str) -> Optional[str]: 813 """Get the Definition ID of a machine (specified by ID) 814 815 :param machine_id: string machine id to get the definition ID of 816 :returns: DefinitionID if found, None otherwise 817 """ 818 containers = CuraContainerRegistry.getInstance().findContainerStacks(id = machine_id) 819 if containers: 820 return containers[0].definition.getId() 821 return None 822 823 def getIncompatibleSettingsOnEnabledExtruders(self, container: InstanceContainer) -> List[str]: 824 if self._global_container_stack is None: 825 return [] 826 extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") 827 result = [] # type: List[str] 828 for setting_instance in container.findInstances(): 829 setting_key = setting_instance.definition.key 830 if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"): 831 continue 832 833 old_value = container.getProperty(setting_key, "value") 834 if isinstance(old_value, SettingFunction): 835 old_value = old_value(self._global_container_stack) 836 if int(old_value) < 0: 837 continue 838 if int(old_value) >= extruder_count or not self._global_container_stack.extruderList[int(old_value)].isEnabled: 839 result.append(setting_key) 840 Logger.log("d", "Reset setting [%s] in [%s] because its old value [%s] is no longer valid", setting_key, container, old_value) 841 return result 842 843 def correctExtruderSettings(self) -> None: 844 """Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed""" 845 846 if self._global_container_stack is None: 847 return 848 for setting_key in self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.userChanges): 849 self._global_container_stack.userChanges.removeInstance(setting_key) 850 add_user_changes = self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.qualityChanges) 851 for setting_key in add_user_changes: 852 # Apply quality changes that are incompatible to user changes, so we do not change the quality changes itself. 853 self._global_container_stack.userChanges.setProperty(setting_key, "value", self._default_extruder_position) 854 if add_user_changes: 855 caution_message = Message( 856 catalog.i18nc("@info:message Followed by a list of settings.", "Settings have been changed to match the current availability of extruders:") + " [{settings_list}]".format(settings_list = ", ".join(add_user_changes)), 857 lifetime = 0, 858 title = catalog.i18nc("@info:title", "Settings updated")) 859 caution_message.show() 860 861 def correctPrintSequence(self) -> None: 862 """ 863 Sets the Print Sequence setting to "all-at-once" when there are more than one enabled extruders. 864 865 This setting has to be explicitly changed whenever we have more than one enabled extruders to make sure that the 866 Cura UI is properly updated to reset all the UI elements changes that occur due to the one-at-a-time mode (such 867 as the reduced build volume, the different convex hulls of the objects etc.). 868 """ 869 870 setting_key = "print_sequence" 871 new_value = "all_at_once" 872 873 if self._global_container_stack is None \ 874 or self._global_container_stack.getProperty(setting_key, "value") == new_value \ 875 or self.numberExtrudersEnabled < 2: 876 return 877 878 user_changes_container = self._global_container_stack.userChanges 879 quality_changes_container = self._global_container_stack.qualityChanges 880 print_sequence_quality_changes = quality_changes_container.getProperty(setting_key, "value") 881 print_sequence_user_changes = user_changes_container.getProperty(setting_key, "value") 882 883 # If the user changes container has a value and its the incorrect value, then reset the setting in the user 884 # changes (so that the circular revert-changes arrow will now show up in the interface) 885 if print_sequence_user_changes and print_sequence_user_changes != new_value: 886 user_changes_container.removeInstance(setting_key) 887 Logger.log("d", "Resetting '{}' in container '{}' because there are more than 1 enabled extruders.".format(setting_key, user_changes_container)) 888 # If the print sequence doesn't exist in either the user changes or the quality changes (yet it still has the 889 # wrong value in the global stack), or it exists in the quality changes and it has the wrong value, then set it 890 # in the user changes 891 elif (not print_sequence_quality_changes and not print_sequence_user_changes) \ 892 or (print_sequence_quality_changes and print_sequence_quality_changes != new_value): 893 user_changes_container.setProperty(setting_key, "value", new_value) 894 Logger.log("d", "Setting '{}' in '{}' to '{}' because there are more than 1 enabled extruders.".format(setting_key, user_changes_container, new_value)) 895 896 def setActiveMachineExtruderCount(self, extruder_count: int) -> None: 897 """Set the amount of extruders on the active machine (global stack) 898 899 :param extruder_count: int the number of extruders to set 900 """ 901 if self._global_container_stack is None: 902 return 903 extruder_manager = self._application.getExtruderManager() 904 905 definition_changes_container = self._global_container_stack.definitionChanges 906 if not self._global_container_stack or definition_changes_container == empty_definition_changes_container: 907 return 908 909 previous_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") 910 if extruder_count == previous_extruder_count: 911 return 912 913 definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count) 914 915 self.updateDefaultExtruder() 916 self.numberExtrudersEnabledChanged.emit() 917 self.correctExtruderSettings() 918 919 # Check to see if any objects are set to print with an extruder that will no longer exist 920 root_node = self._application.getController().getScene().getRoot() 921 for node in DepthFirstIterator(root_node): 922 if node.getMeshData(): 923 extruder_nr = node.callDecoration("getActiveExtruderPosition") 924 925 if extruder_nr is not None and int(extruder_nr) > extruder_count - 1: 926 extruder = extruder_manager.getExtruderStack(extruder_count - 1) 927 if extruder is not None: 928 node.callDecoration("setActiveExtruder", extruder.getId()) 929 else: 930 Logger.log("w", "Could not find extruder to set active.") 931 932 # Make sure one of the extruder stacks is active 933 extruder_manager.setActiveExtruderIndex(0) 934 935 # Move settable_per_extruder values out of the global container 936 # After CURA-4482 this should not be the case anymore, but we still want to support older project files. 937 global_user_container = self._global_container_stack.userChanges 938 939 for setting_instance in global_user_container.findInstances(): 940 setting_key = setting_instance.definition.key 941 settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder") 942 943 if settable_per_extruder: 944 limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder")) 945 extruder_position = max(0, limit_to_extruder) 946 extruder_stack = self._global_container_stack.extruderList[extruder_position] 947 if extruder_stack: 948 extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value")) 949 else: 950 Logger.log("e", "Unable to find extruder on position %s", extruder_position) 951 global_user_container.removeInstance(setting_key) 952 953 # Signal that the global stack has changed 954 self._application.globalContainerStackChanged.emit() 955 self.forceUpdateAllSettings() 956 957 def updateDefaultExtruder(self) -> None: 958 if self._global_container_stack is None: 959 return 960 961 old_position = self._default_extruder_position 962 new_default_position = "0" 963 for extruder in self._global_container_stack.extruderList: 964 if extruder.isEnabled: 965 new_default_position = extruder.getMetaDataEntry("position", "0") 966 break 967 if new_default_position != old_position: 968 self._default_extruder_position = new_default_position 969 self.extruderChanged.emit() 970 971 def updateNumberExtrudersEnabled(self) -> None: 972 if self._global_container_stack is None: 973 return 974 definition_changes_container = self._global_container_stack.definitionChanges 975 machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") 976 extruder_count = 0 977 for position, extruder in enumerate(self._global_container_stack.extruderList): 978 if extruder.isEnabled and int(position) < machine_extruder_count: 979 extruder_count += 1 980 if self.numberExtrudersEnabled != extruder_count: 981 definition_changes_container.setProperty("extruders_enabled_count", "value", extruder_count) 982 self.numberExtrudersEnabledChanged.emit() 983 984 @pyqtProperty(int, notify = numberExtrudersEnabledChanged) 985 def numberExtrudersEnabled(self) -> int: 986 if self._global_container_stack is None: 987 return 1 988 extruders_enabled_count = self._global_container_stack.definitionChanges.getProperty("extruders_enabled_count", "value") 989 if extruders_enabled_count is None: 990 extruders_enabled_count = len(self._global_container_stack.extruderList) 991 return extruders_enabled_count 992 993 @pyqtProperty(str, notify = extruderChanged) 994 def defaultExtruderPosition(self) -> str: 995 return self._default_extruder_position 996 997 @pyqtSlot() 998 def forceUpdateAllSettings(self) -> None: 999 """This will fire the propertiesChanged for all settings so they will be updated in the front-end""" 1000 1001 if self._global_container_stack is None: 1002 return 1003 property_names = ["value", "resolve", "validationState"] 1004 for container in [self._global_container_stack] + self._global_container_stack.extruderList: 1005 for setting_key in container.getAllKeys(): 1006 container.propertiesChanged.emit(setting_key, property_names) 1007 1008 @pyqtSlot(int, bool) 1009 def setExtruderEnabled(self, position: int, enabled: bool) -> None: 1010 if self._global_container_stack is None or position >= len(self._global_container_stack.extruderList): 1011 Logger.log("w", "Could not find extruder on position %s.", position) 1012 return 1013 extruder = self._global_container_stack.extruderList[position] 1014 1015 extruder.setEnabled(enabled) 1016 self.updateDefaultExtruder() 1017 self.updateNumberExtrudersEnabled() 1018 self.correctExtruderSettings() 1019 1020 # Ensure that the quality profile is compatible with current combination, or choose a compatible one if available 1021 self._updateQualityWithMaterial() 1022 self.extruderChanged.emit() 1023 # Update material compatibility color 1024 self.activeQualityGroupChanged.emit() 1025 # Update items in SettingExtruder 1026 ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId()) 1027 1028 # Also trigger the build plate compatibility to update 1029 self.activeMaterialChanged.emit() 1030 self.activeIntentChanged.emit() 1031 1032 # Force an update of resolve values 1033 property_names = ["resolve", "validationState"] 1034 for setting_key in self._global_container_stack.getAllKeys(): 1035 self._global_container_stack.propertiesChanged.emit(setting_key, property_names) 1036 1037 def _onMaterialNameChanged(self) -> None: 1038 self.activeMaterialChanged.emit() 1039 1040 def _getContainerChangedSignals(self) -> List[Signal]: 1041 """Get the signals that signal that the containers changed for all stacks. 1042 1043 This includes the global stack and all extruder stacks. So if any 1044 container changed anywhere. 1045 """ 1046 1047 if self._global_container_stack is None: 1048 return [] 1049 return [s.containersChanged for s in self._global_container_stack.extruderList + [self._global_container_stack]] 1050 1051 @pyqtSlot(str, str, str) 1052 def setSettingForAllExtruders(self, setting_name: str, property_name: str, property_value: str) -> None: 1053 if self._global_container_stack is None: 1054 return 1055 for extruder in self._global_container_stack.extruderList: 1056 container = extruder.userChanges 1057 container.setProperty(setting_name, property_name, property_value) 1058 1059 @pyqtSlot(str) 1060 def resetSettingForAllExtruders(self, setting_name: str) -> None: 1061 """Reset all setting properties of a setting for all extruders. 1062 1063 :param setting_name: The ID of the setting to reset. 1064 """ 1065 if self._global_container_stack is None: 1066 return 1067 for extruder in self._global_container_stack.extruderList: 1068 container = extruder.userChanges 1069 container.removeInstance(setting_name) 1070 1071 def _onRootMaterialChanged(self) -> None: 1072 """Update _current_root_material_id when the current root material was changed.""" 1073 1074 self._current_root_material_id = {} 1075 1076 changed = False 1077 1078 if self._global_container_stack: 1079 for extruder in self._global_container_stack.extruderList: 1080 material_id = extruder.material.getMetaDataEntry("base_file") 1081 position = extruder.getMetaDataEntry("position") 1082 if position not in self._current_root_material_id or material_id != self._current_root_material_id[position]: 1083 changed = True 1084 self._current_root_material_id[position] = material_id 1085 1086 if changed: 1087 self.activeMaterialChanged.emit() 1088 1089 @pyqtProperty("QVariant", notify = rootMaterialChanged) 1090 def currentRootMaterialId(self) -> Dict[str, str]: 1091 return self._current_root_material_id 1092 1093 # Sets all quality and quality_changes containers to empty_quality and empty_quality_changes containers 1094 # for all stacks in the currently active machine. 1095 # 1096 def _setEmptyQuality(self) -> None: 1097 if self._global_container_stack is None: 1098 return 1099 self._global_container_stack.quality = empty_quality_container 1100 self._global_container_stack.qualityChanges = empty_quality_changes_container 1101 for extruder in self._global_container_stack.extruderList: 1102 extruder.quality = empty_quality_container 1103 extruder.qualityChanges = empty_quality_changes_container 1104 1105 self.activeQualityGroupChanged.emit() 1106 self.activeQualityChangesGroupChanged.emit() 1107 self._updateIntentWithQuality() 1108 1109 def _setQualityGroup(self, quality_group: Optional["QualityGroup"], empty_quality_changes: bool = True) -> None: 1110 if self._global_container_stack is None: 1111 return 1112 if quality_group is None: 1113 self._setEmptyQuality() 1114 return 1115 1116 if quality_group.node_for_global is None or quality_group.node_for_global.container is None: 1117 return 1118 for node in quality_group.nodes_for_extruders.values(): 1119 if node.container is None: 1120 return 1121 1122 # Set quality and quality_changes for the GlobalStack 1123 self._global_container_stack.quality = quality_group.node_for_global.container 1124 if empty_quality_changes: 1125 self._global_container_stack.qualityChanges = empty_quality_changes_container 1126 1127 # Set quality and quality_changes for each ExtruderStack 1128 for position, node in quality_group.nodes_for_extruders.items(): 1129 self._global_container_stack.extruderList[position].quality = node.container 1130 if empty_quality_changes: 1131 self._global_container_stack.extruderList[position].qualityChanges = empty_quality_changes_container 1132 1133 self.activeQualityGroupChanged.emit() 1134 self.activeQualityChangesGroupChanged.emit() 1135 self._updateIntentWithQuality() 1136 1137 def _fixQualityChangesGroupToNotSupported(self, quality_changes_group: "QualityChangesGroup") -> None: 1138 metadatas = [quality_changes_group.metadata_for_global] + list(quality_changes_group.metadata_per_extruder.values()) 1139 for metadata in metadatas: 1140 metadata["quality_type"] = "not_supported" # This actually changes the metadata of the container since they are stored by reference! 1141 quality_changes_group.quality_type = "not_supported" 1142 quality_changes_group.intent_category = "default" 1143 1144 def _setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None: 1145 if self._global_container_stack is None: 1146 return # Can't change that. 1147 quality_type = quality_changes_group.quality_type 1148 # A custom quality can be created based on "not supported". 1149 # In that case, do not set quality containers to empty. 1150 quality_group = None 1151 if quality_type != "not_supported": # Find the quality group that the quality changes was based on. 1152 quality_group = ContainerTree.getInstance().getCurrentQualityGroups().get(quality_type) 1153 if quality_group is None: 1154 self._fixQualityChangesGroupToNotSupported(quality_changes_group) 1155 1156 container_registry = self._application.getContainerRegistry() 1157 quality_changes_container = empty_quality_changes_container 1158 quality_container = empty_quality_container # type: InstanceContainer 1159 if quality_changes_group.metadata_for_global: 1160 global_containers = container_registry.findContainers(id = quality_changes_group.metadata_for_global["id"]) 1161 if global_containers: 1162 quality_changes_container = global_containers[0] 1163 if quality_changes_group.metadata_for_global: 1164 containers = container_registry.findContainers(id = quality_changes_group.metadata_for_global["id"]) 1165 if containers: 1166 quality_changes_container = cast(InstanceContainer, containers[0]) 1167 if quality_group is not None and quality_group.node_for_global and quality_group.node_for_global.container: 1168 quality_container = quality_group.node_for_global.container 1169 1170 self._global_container_stack.quality = quality_container 1171 self._global_container_stack.qualityChanges = quality_changes_container 1172 1173 for position, extruder in enumerate(self._global_container_stack.extruderList): 1174 quality_node = None 1175 if quality_group is not None: 1176 quality_node = quality_group.nodes_for_extruders.get(position) 1177 1178 quality_changes_container = empty_quality_changes_container 1179 quality_container = empty_quality_container 1180 quality_changes_metadata = quality_changes_group.metadata_per_extruder.get(position) 1181 if quality_changes_metadata: 1182 containers = container_registry.findContainers(id = quality_changes_metadata["id"]) 1183 if containers: 1184 quality_changes_container = cast(InstanceContainer, containers[0]) 1185 if quality_node and quality_node.container: 1186 quality_container = quality_node.container 1187 1188 extruder.quality = quality_container 1189 extruder.qualityChanges = quality_changes_container 1190 1191 self.setIntentByCategory(quality_changes_group.intent_category) 1192 self._reCalculateNumUserSettings() 1193 1194 self.activeQualityGroupChanged.emit() 1195 self.activeQualityChangesGroupChanged.emit() 1196 1197 def _setVariantNode(self, position: str, variant_node: "VariantNode") -> None: 1198 if self._global_container_stack is None: 1199 return 1200 self._global_container_stack.extruderList[int(position)].variant = variant_node.container 1201 self.activeVariantChanged.emit() 1202 1203 def _setGlobalVariant(self, container_node: "ContainerNode") -> None: 1204 if self._global_container_stack is None: 1205 return 1206 self._global_container_stack.variant = container_node.container 1207 if not self._global_container_stack.variant: 1208 self._global_container_stack.variant = self._application.empty_variant_container 1209 1210 def _setMaterial(self, position: str, material_node: Optional["MaterialNode"] = None) -> None: 1211 if self._global_container_stack is None: 1212 return 1213 if material_node and material_node.container: 1214 material_container = material_node.container 1215 self._global_container_stack.extruderList[int(position)].material = material_container 1216 root_material_id = material_container.getMetaDataEntry("base_file", None) 1217 else: 1218 self._global_container_stack.extruderList[int(position)].material = empty_material_container 1219 root_material_id = None 1220 # The _current_root_material_id is used in the MaterialMenu to see which material is selected 1221 if position not in self._current_root_material_id or root_material_id != self._current_root_material_id[position]: 1222 self._current_root_material_id[position] = root_material_id 1223 self.rootMaterialChanged.emit() 1224 1225 def activeMaterialsCompatible(self) -> bool: 1226 # Check material - variant compatibility 1227 if self._global_container_stack is not None: 1228 if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)): 1229 for extruder in self._global_container_stack.extruderList: 1230 if not extruder.isEnabled: 1231 continue 1232 if not extruder.material.getMetaDataEntry("compatible"): 1233 return False 1234 return True 1235 1236 def _updateQualityWithMaterial(self, *args: Any) -> None: 1237 """Update current quality type and machine after setting material""" 1238 1239 global_stack = self._application.getGlobalContainerStack() 1240 if global_stack is None: 1241 return 1242 Logger.log("d", "Updating quality/quality_changes due to material change") 1243 current_quality_type = global_stack.quality.getMetaDataEntry("quality_type") 1244 candidate_quality_groups = ContainerTree.getInstance().getCurrentQualityGroups() 1245 available_quality_types = {qt for qt, g in candidate_quality_groups.items() if g.is_available} 1246 1247 Logger.log("d", "Current quality type = [%s]", current_quality_type) 1248 if not self.activeMaterialsCompatible(): 1249 if current_quality_type is not None: 1250 Logger.log("i", "Active materials are not compatible, setting all qualities to empty (Not Supported).") 1251 self._setEmptyQuality() 1252 return 1253 1254 if not available_quality_types: 1255 Logger.log("i", "No available quality types found, setting all qualities to empty (Not Supported).") 1256 self._setEmptyQuality() 1257 return 1258 1259 if current_quality_type in available_quality_types: 1260 Logger.log("i", "Current available quality type [%s] is available, applying changes.", current_quality_type) 1261 self._setQualityGroup(candidate_quality_groups[current_quality_type], empty_quality_changes = False) 1262 return 1263 1264 # The current quality type is not available so we use the preferred quality type if it's available, 1265 # otherwise use one of the available quality types. 1266 quality_type = sorted(list(available_quality_types))[0] 1267 if self._global_container_stack is None: 1268 Logger.log("e", "Global stack not present!") 1269 return 1270 preferred_quality_type = self._global_container_stack.getMetaDataEntry("preferred_quality_type") 1271 if preferred_quality_type in available_quality_types: 1272 quality_type = preferred_quality_type 1273 1274 Logger.log("i", "The current quality type [%s] is not available, switching to [%s] instead", 1275 current_quality_type, quality_type) 1276 self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True) 1277 1278 def _updateIntentWithQuality(self): 1279 """Update the current intent after the quality changed""" 1280 1281 global_stack = self._application.getGlobalContainerStack() 1282 if global_stack is None: 1283 return 1284 Logger.log("d", "Updating intent due to quality change") 1285 1286 category = "default" 1287 1288 for extruder in global_stack.extruderList: 1289 if not extruder.isEnabled: 1290 continue 1291 current_category = extruder.intent.getMetaDataEntry("intent_category", "default") 1292 if current_category != "default" and current_category != category: 1293 category = current_category 1294 continue 1295 # It's also possible that the qualityChanges has an opinion about the intent_category. 1296 # This is in the case that a QC was made on an intent, but none of the materials have that intent. 1297 # If the user switches back, we do want the intent to be selected again. 1298 # 1299 # Do not ask empty quality changes for intent category. 1300 if extruder.qualityChanges.getId() == empty_quality_changes_container.getId(): 1301 continue 1302 current_category = extruder.qualityChanges.getMetaDataEntry("intent_category", "default") 1303 if current_category != "default" and current_category != category: 1304 category = current_category 1305 self.setIntentByCategory(category) 1306 1307 @pyqtSlot() 1308 def updateMaterialWithVariant(self, position: Optional[str] = None) -> None: 1309 """Update the material profile in the current stacks when the variant is 1310 1311 changed. 1312 :param position: The extruder stack to update. If provided with None, all 1313 extruder stacks will be updated. 1314 """ 1315 if self._global_container_stack is None: 1316 return 1317 if position is None: 1318 position_list = [str(position) for position in range(len(self._global_container_stack.extruderList))] 1319 else: 1320 position_list = [position] 1321 1322 for position_item in position_list: 1323 try: 1324 extruder = self._global_container_stack.extruderList[int(position_item)] 1325 except IndexError: 1326 continue 1327 1328 current_material_base_name = extruder.material.getMetaDataEntry("base_file") 1329 current_nozzle_name = extruder.variant.getMetaDataEntry("name") 1330 1331 # If we can keep the current material after the switch, try to do so. 1332 nozzle_node = ContainerTree.getInstance().machines[self._global_container_stack.definition.getId()].variants[current_nozzle_name] 1333 candidate_materials = nozzle_node.materials 1334 old_approximate_material_diameter = int(extruder.material.getMetaDataEntry("approximate_diameter", default = 3)) 1335 new_approximate_material_diameter = int(self._global_container_stack.extruderList[int(position_item)].getApproximateMaterialDiameter()) 1336 1337 # Only switch to the old candidate material if the approximate material diameter of the extruder stays the 1338 # same. 1339 if new_approximate_material_diameter == old_approximate_material_diameter and \ 1340 current_material_base_name in candidate_materials: # The current material is also available after the switch. Retain it. 1341 new_material = candidate_materials[current_material_base_name] 1342 self._setMaterial(position_item, new_material) 1343 else: 1344 # The current material is not available, find the preferred one. 1345 approximate_material_diameter = int(self._global_container_stack.extruderList[int(position_item)].getApproximateMaterialDiameter()) 1346 material_node = nozzle_node.preferredMaterial(approximate_material_diameter) 1347 self._setMaterial(position_item, material_node) 1348 1349 @pyqtSlot(str) 1350 def switchPrinterType(self, machine_name: str) -> None: 1351 """Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new 1352 1353 instance with the same network key. 1354 """ 1355 # Don't switch if the user tries to change to the same type of printer 1356 if self._global_container_stack is None or self._global_container_stack.definition.name == machine_name: 1357 return 1358 Logger.log("i", "Attempting to switch the printer type to [%s]", machine_name) 1359 # Get the definition id corresponding to this machine name 1360 definitions = CuraContainerRegistry.getInstance().findDefinitionContainers(name=machine_name) 1361 if not definitions: 1362 Logger.log("e", "Unable to switch printer type since it could not be found!") 1363 return 1364 machine_definition_id = definitions[0].getId() 1365 # Try to find a machine with the same network key 1366 metadata_filter = {"group_id": self._global_container_stack.getMetaDataEntry("group_id")} 1367 new_machine = self.getMachine(machine_definition_id, metadata_filter = metadata_filter) 1368 # If there is no machine, then create a new one and set it to the non-hidden instance 1369 if not new_machine: 1370 new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id) 1371 if not new_machine: 1372 Logger.log("e", "Failed to create new machine when switching configuration.") 1373 return 1374 1375 for metadata_key in self._global_container_stack.getMetaData(): 1376 if metadata_key in new_machine.getMetaData(): 1377 continue # Don't copy the already preset stuff. 1378 new_machine.setMetaDataEntry(metadata_key, self._global_container_stack.getMetaDataEntry(metadata_key)) 1379 # Special case, group_id should be overwritten! 1380 new_machine.setMetaDataEntry("group_id", self._global_container_stack.getMetaDataEntry("group_id")) 1381 else: 1382 Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey()) 1383 1384 # Set the current printer instance to hidden (the metadata entry must exist) 1385 new_machine.setMetaDataEntry("hidden", False) 1386 self._global_container_stack.setMetaDataEntry("hidden", True) 1387 1388 # The new_machine does not contain user changes (global or per-extruder user changes). 1389 # Keep a temporary copy of the global and per-extruder user changes and transfer them to the user changes 1390 # of the new machine after the new_machine becomes active. 1391 global_user_changes = self._global_container_stack.userChanges 1392 per_extruder_user_changes = [extruder_stack.userChanges for extruder_stack in self._global_container_stack.extruderList] 1393 1394 self.setActiveMachine(new_machine.getId()) 1395 1396 # Apply the global and per-extruder userChanges to the new_machine (which is of different type than the 1397 # previous one). 1398 self._global_container_stack.setUserChanges(global_user_changes) 1399 for i, user_changes in enumerate(per_extruder_user_changes): 1400 self._global_container_stack.extruderList[i].setUserChanges(per_extruder_user_changes[i]) 1401 1402 @pyqtSlot(QObject) 1403 def applyRemoteConfiguration(self, configuration: PrinterConfigurationModel) -> None: 1404 if self._global_container_stack is None: 1405 return 1406 self.blurSettings.emit() 1407 container_registry = CuraContainerRegistry.getInstance() 1408 with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): 1409 self.switchPrinterType(configuration.printerType) 1410 1411 extruders_to_disable = set() 1412 1413 # If an extruder that's currently used to print a model gets disabled due to the syncing, we need to show 1414 # a message explaining why. 1415 need_to_show_message = False 1416 1417 for extruder_configuration in configuration.extruderConfigurations: 1418 # We support "" or None, since the cloud uses None instead of empty strings 1419 extruder_has_hotend = extruder_configuration.hotendID not in ["", None] 1420 extruder_has_material = extruder_configuration.material.guid not in [None, "", "00000000-0000-0000-0000-000000000000"] 1421 1422 # If the machine doesn't have a hotend or material, disable this extruder 1423 if not extruder_has_hotend or not extruder_has_material: 1424 extruders_to_disable.add(extruder_configuration.position) 1425 1426 # If there's no material and/or nozzle on the printer, enable the first extruder and disable the rest. 1427 if len(extruders_to_disable) == len(self._global_container_stack.extruderList): 1428 extruders_to_disable.remove(min(extruders_to_disable)) 1429 1430 for extruder_configuration in configuration.extruderConfigurations: 1431 position = str(extruder_configuration.position) 1432 if int(position) >= len(self._global_container_stack.extruderList): 1433 Logger.warning("Received a configuration for extruder {position}, which is out of bounds for this printer.".format(position=position)) 1434 continue # Remote printer gave more extruders than what Cura had locally, e.g. because the user switched to a single-extruder printer while the sync was being processed. 1435 1436 # If the machine doesn't have a hotend or material, disable this extruder 1437 if int(position) in extruders_to_disable: 1438 self._global_container_stack.extruderList[int(position)].setEnabled(False) 1439 1440 need_to_show_message = True 1441 1442 else: 1443 machine_node = ContainerTree.getInstance().machines.get(self._global_container_stack.definition.getId()) 1444 variant_node = machine_node.variants.get(extruder_configuration.hotendID) 1445 if variant_node is None: 1446 continue 1447 self._setVariantNode(position, variant_node) 1448 1449 # Find the material profile that the printer has stored. 1450 # 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. 1451 approximate_diameter = int(self._global_container_stack.extruderList[int(position)].getApproximateMaterialDiameter()) 1452 materials_with_guid = container_registry.findInstanceContainersMetadata(GUID = extruder_configuration.material.guid, approximate_diameter = str(approximate_diameter), ignore_case = True) 1453 material_container_node = variant_node.preferredMaterial(approximate_diameter) 1454 if materials_with_guid: # We also have the material profile that the printer wants to share. 1455 base_file = materials_with_guid[0]["base_file"] 1456 material_container_node = variant_node.materials.get(base_file, material_container_node) 1457 1458 self._setMaterial(position, material_container_node) 1459 self._global_container_stack.extruderList[int(position)].setEnabled(True) 1460 self.updateMaterialWithVariant(position) 1461 1462 self.updateDefaultExtruder() 1463 self.updateNumberExtrudersEnabled() 1464 self._updateQualityWithMaterial() 1465 1466 if need_to_show_message: 1467 msg_str = "{extruders} is disabled because there is no material loaded. Please load a material or use custom configurations." 1468 1469 # Show human-readable extruder names such as "Extruder Left", "Extruder Front" instead of "Extruder 1, 2, 3". 1470 extruder_names = [] 1471 for extruder_position in sorted(extruders_to_disable): 1472 extruder_stack = self._global_container_stack.extruderList[int(extruder_position)] 1473 extruder_name = extruder_stack.definition.getName() 1474 extruder_names.append(extruder_name) 1475 extruders_str = ", ".join(extruder_names) 1476 msg_str = msg_str.format(extruders = extruders_str) 1477 message = Message(catalog.i18nc("@info:status", msg_str), 1478 title = catalog.i18nc("@info:title", "Extruder(s) Disabled")) 1479 message.show() 1480 1481 # See if we need to show the Discard or Keep changes screen 1482 if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1: 1483 self._application.discardOrKeepProfileChanges() 1484 1485 @pyqtSlot("QVariant") 1486 def setGlobalVariant(self, container_node: "ContainerNode") -> None: 1487 self.blurSettings.emit() 1488 with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): 1489 self._setGlobalVariant(container_node) 1490 self.updateMaterialWithVariant(None) # Update all materials 1491 self._updateQualityWithMaterial() 1492 1493 @pyqtSlot(str, str, result = bool) 1494 def setMaterialById(self, position: str, root_material_id: str) -> bool: 1495 if self._global_container_stack is None: 1496 return False 1497 1498 machine_definition_id = self._global_container_stack.definition.id 1499 position = str(position) 1500 extruder_stack = self._global_container_stack.extruderList[int(position)] 1501 nozzle_name = extruder_stack.variant.getName() 1502 1503 materials = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials 1504 if root_material_id in materials: 1505 self.setMaterial(position, materials[root_material_id]) 1506 return True 1507 return False 1508 1509 @pyqtSlot(str, "QVariant") 1510 def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None: 1511 """Global_stack: if you want to provide your own global_stack instead of the current active one 1512 1513 if you update an active machine, special measures have to be taken. 1514 """ 1515 if global_stack is not None and global_stack != self._global_container_stack: 1516 global_stack.extruderList[int(position)].material = container_node.container 1517 return 1518 position = str(position) 1519 self.blurSettings.emit() 1520 with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): 1521 self._setMaterial(position, container_node) 1522 self._updateQualityWithMaterial() 1523 1524 # See if we need to show the Discard or Keep changes screen 1525 if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1: 1526 self._application.discardOrKeepProfileChanges() 1527 1528 @pyqtSlot(str, str) 1529 def setVariantByName(self, position: str, variant_name: str) -> None: 1530 if self._global_container_stack is None: 1531 return 1532 machine_definition_id = self._global_container_stack.definition.id 1533 machine_node = ContainerTree.getInstance().machines.get(machine_definition_id) 1534 variant_node = machine_node.variants.get(variant_name) 1535 if variant_node is None: 1536 Logger.error("There is no variant with the name {variant_name}.") 1537 return 1538 self.setVariant(position, variant_node) 1539 1540 @pyqtSlot(str, "QVariant") 1541 def setVariant(self, position: str, variant_node: "VariantNode") -> None: 1542 position = str(position) 1543 self.blurSettings.emit() 1544 with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): 1545 self._setVariantNode(position, variant_node) 1546 self.updateMaterialWithVariant(position) 1547 self._updateQualityWithMaterial() 1548 1549 # See if we need to show the Discard or Keep changes screen 1550 if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1: 1551 self._application.discardOrKeepProfileChanges() 1552 1553 @pyqtSlot(str) 1554 def setQualityGroupByQualityType(self, quality_type: str) -> None: 1555 if self._global_container_stack is None: 1556 return 1557 # Get all the quality groups for this global stack and filter out by quality_type 1558 self.setQualityGroup(ContainerTree.getInstance().getCurrentQualityGroups()[quality_type]) 1559 1560 @pyqtSlot(QObject) 1561 def setQualityGroup(self, quality_group: "QualityGroup", no_dialog: bool = False, global_stack: Optional["GlobalStack"] = None) -> None: 1562 """Optionally provide global_stack if you want to use your own 1563 1564 The active global_stack is treated differently. 1565 """ 1566 if global_stack is not None and global_stack != self._global_container_stack: 1567 if quality_group is None: 1568 Logger.log("e", "Could not set quality group because quality group is None") 1569 return 1570 if quality_group.node_for_global is None: 1571 Logger.log("e", "Could not set quality group [%s] because it has no node_for_global", str(quality_group)) 1572 return 1573 # This is not changing the quality for the active machine !!!!!!!! 1574 global_stack.quality = quality_group.node_for_global.container 1575 for extruder_nr, extruder_stack in enumerate(global_stack.extruderList): 1576 quality_container = empty_quality_container 1577 if extruder_nr in quality_group.nodes_for_extruders: 1578 container = quality_group.nodes_for_extruders[extruder_nr].container 1579 quality_container = container if container is not None else quality_container 1580 extruder_stack.quality = quality_container 1581 return 1582 1583 self.blurSettings.emit() 1584 with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): 1585 self._setQualityGroup(quality_group) 1586 1587 # See if we need to show the Discard or Keep changes screen 1588 if not no_dialog and self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1: 1589 self._application.discardOrKeepProfileChanges() 1590 1591 # The display name map of currently active quality. 1592 # The display name has 2 parts, a main part and a suffix part. 1593 # This display name is: 1594 # - For built-in qualities (quality/intent): the quality type name, such as "Fine", "Normal", etc. 1595 # - For custom qualities: <custom_quality_name> - <intent_name> - <quality_type_name> 1596 # Examples: 1597 # - "my_profile - Fine" (only based on a default quality, no intent involved) 1598 # - "my_profile - Engineering - Fine" (based on an intent) 1599 @pyqtProperty("QVariantMap", notify = activeQualityDisplayNameChanged) 1600 def activeQualityDisplayNameMap(self) -> Dict[str, str]: 1601 global_stack = self._application.getGlobalContainerStack() 1602 if global_stack is None: 1603 return {"main": "", 1604 "suffix": ""} 1605 1606 display_name = global_stack.quality.getName() 1607 1608 intent_category = self.activeIntentCategory 1609 if intent_category != "default": 1610 intent_display_name = IntentCategoryModel.translation(intent_category, 1611 "name", 1612 catalog.i18nc("@label", "Unknown")) 1613 display_name = "{intent_name} - {the_rest}".format(intent_name = intent_display_name, 1614 the_rest = display_name) 1615 1616 main_part = display_name 1617 suffix_part = "" 1618 1619 # Not a custom quality 1620 if global_stack.qualityChanges != empty_quality_changes_container: 1621 main_part = self.activeQualityOrQualityChangesName 1622 suffix_part = display_name 1623 1624 return {"main": main_part, 1625 "suffix": suffix_part} 1626 1627 @pyqtSlot(str) 1628 def setIntentByCategory(self, intent_category: str) -> None: 1629 """Change the intent category of the current printer. 1630 1631 All extruders can change their profiles. If an intent profile is 1632 available with the desired intent category, that one will get chosen. 1633 Otherwise the intent profile will be left to the empty profile, which 1634 represents the "default" intent category. 1635 :param intent_category: The intent category to change to. 1636 """ 1637 1638 global_stack = self._application.getGlobalContainerStack() 1639 if global_stack is None: 1640 return 1641 container_tree = ContainerTree.getInstance() 1642 for extruder in global_stack.extruderList: 1643 definition_id = global_stack.definition.getId() 1644 variant_name = extruder.variant.getName() 1645 material_base_file = extruder.material.getMetaDataEntry("base_file") 1646 quality_id = extruder.quality.getId() 1647 if quality_id == empty_quality_container.getId(): 1648 extruder.intent = empty_intent_container 1649 continue 1650 1651 # Yes, we can find this in a single line of code. This makes it easier to read and it has the benefit 1652 # that it doesn't lump key errors together for the crashlogs 1653 try: 1654 machine_node = container_tree.machines[definition_id] 1655 variant_node = machine_node.variants[variant_name] 1656 material_node = variant_node.materials[material_base_file] 1657 quality_node = material_node.qualities[quality_id] 1658 except KeyError as e: 1659 Logger.error("Can't set the intent category '{category}' since the profile '{profile}' in the stack is not supported according to the container tree.".format(category = intent_category, profile = e)) 1660 continue 1661 1662 for intent_node in quality_node.intents.values(): 1663 if intent_node.intent_category == intent_category: # Found an intent with the correct category. 1664 extruder.intent = intent_node.container 1665 break 1666 else: # No intent had the correct category. 1667 extruder.intent = empty_intent_container 1668 1669 def activeQualityGroup(self) -> Optional["QualityGroup"]: 1670 """Get the currently activated quality group. 1671 1672 If no printer is added yet or the printer doesn't have quality profiles, 1673 this returns ``None``. 1674 :return: The currently active quality group. 1675 """ 1676 1677 global_stack = self._application.getGlobalContainerStack() 1678 if not global_stack or global_stack.quality == empty_quality_container: 1679 return None 1680 return ContainerTree.getInstance().getCurrentQualityGroups().get(self.activeQualityType) 1681 1682 @pyqtProperty(str, notify = activeQualityGroupChanged) 1683 def activeQualityGroupName(self) -> str: 1684 """Get the name of the active quality group. 1685 1686 :return: The name of the active quality group. 1687 """ 1688 quality_group = self.activeQualityGroup() 1689 if quality_group is None: 1690 return "" 1691 return quality_group.getName() 1692 1693 @pyqtSlot(QObject) 1694 def setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", no_dialog: bool = False) -> None: 1695 self.blurSettings.emit() 1696 with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): 1697 self._setQualityChangesGroup(quality_changes_group) 1698 1699 # See if we need to show the Discard or Keep changes screen 1700 if not no_dialog and self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1: 1701 self._application.discardOrKeepProfileChanges() 1702 1703 @pyqtSlot() 1704 def resetToUseDefaultQuality(self) -> None: 1705 if self._global_container_stack is None: 1706 return 1707 with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): 1708 self._setQualityGroup(self.activeQualityGroup()) 1709 for stack in [self._global_container_stack] + self._global_container_stack.extruderList: 1710 stack.userChanges.clear() 1711 1712 @pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged) 1713 def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]: 1714 global_stack = self._application.getGlobalContainerStack() 1715 if global_stack is None or global_stack.qualityChanges == empty_quality_changes_container: 1716 return None 1717 1718 all_group_list = ContainerTree.getInstance().getCurrentQualityChangesGroups() 1719 the_group = None 1720 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. 1721 if group.metadata_for_global and group.metadata_for_global["id"] == global_stack.qualityChanges.getId(): 1722 the_group = group 1723 break 1724 1725 return the_group 1726 1727 @pyqtProperty(bool, notify = activeQualityChangesGroupChanged) 1728 def hasCustomQuality(self) -> bool: 1729 global_stack = self._application.getGlobalContainerStack() 1730 return global_stack is None or global_stack.qualityChanges != empty_quality_changes_container 1731 1732 @pyqtProperty(str, notify = activeQualityGroupChanged) 1733 def activeQualityOrQualityChangesName(self) -> str: 1734 global_container_stack = self._application.getGlobalContainerStack() 1735 if not global_container_stack: 1736 return empty_quality_container.getName() 1737 if global_container_stack.qualityChanges != empty_quality_changes_container: 1738 return global_container_stack.qualityChanges.getName() 1739 return global_container_stack.quality.getName() 1740 1741 @pyqtProperty(bool, notify = activeQualityGroupChanged) 1742 def hasNotSupportedQuality(self) -> bool: 1743 global_container_stack = self._application.getGlobalContainerStack() 1744 return (not global_container_stack is None) and global_container_stack.quality == empty_quality_container and global_container_stack.qualityChanges == empty_quality_changes_container 1745 1746 @pyqtProperty(bool, notify = activeQualityGroupChanged) 1747 def isActiveQualityCustom(self) -> bool: 1748 global_stack = self._application.getGlobalContainerStack() 1749 if global_stack is None: 1750 return False 1751 return global_stack.qualityChanges != empty_quality_changes_container 1752 1753 def updateUponMaterialMetadataChange(self) -> None: 1754 if self._global_container_stack is None: 1755 return 1756 with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): 1757 self.updateMaterialWithVariant(None) 1758 self._updateQualityWithMaterial() 1759 1760 @pyqtSlot(str, result = str) 1761 def getAbbreviatedMachineName(self, machine_type_name: str) -> str: 1762 """This function will translate any printer type name to an abbreviated printer type name""" 1763 1764 abbr_machine = "" 1765 for word in re.findall(r"[\w']+", machine_type_name): 1766 if word.lower() == "ultimaker": 1767 abbr_machine += "UM" 1768 elif word.isdigit(): 1769 abbr_machine += word 1770 else: 1771 stripped_word = "".join(char for char in unicodedata.normalize("NFD", word.upper()) if unicodedata.category(char) != "Mn") 1772 # - use only the first character if the word is too long (> 3 characters) 1773 # - use the whole word if it's not too long (<= 3 characters) 1774 if len(stripped_word) > 3: 1775 stripped_word = stripped_word[0] 1776 abbr_machine += stripped_word 1777 1778 return abbr_machine 1779