MachineManager.py 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899
  1. # Copyright (c) 2016 Ultimaker B.V.
  2. # Cura is released under the terms of the AGPLv3 or higher.
  3. from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
  4. from PyQt5.QtWidgets import QMessageBox
  5. from UM.Application import Application
  6. from UM.Preferences import Preferences
  7. from UM.Logger import Logger
  8. import UM.Settings
  9. from cura.PrinterOutputDevice import PrinterOutputDevice
  10. from . import ExtruderManager
  11. from UM.i18n import i18nCatalog
  12. catalog = i18nCatalog("cura")
  13. import time
  14. class MachineManager(QObject):
  15. def __init__(self, parent = None):
  16. super().__init__(parent)
  17. self._active_container_stack = None
  18. self._global_container_stack = None
  19. Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
  20. self._global_stack_valid = None
  21. self._onGlobalContainerChanged()
  22. ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
  23. self._onActiveExtruderStackChanged()
  24. ## When the global container is changed, active material probably needs to be updated.
  25. self.globalContainerChanged.connect(self.activeMaterialChanged)
  26. self.globalContainerChanged.connect(self.activeVariantChanged)
  27. self.globalContainerChanged.connect(self.activeQualityChanged)
  28. ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeMaterialChanged)
  29. ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeVariantChanged)
  30. ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeQualityChanged)
  31. self.globalContainerChanged.connect(self.activeStackChanged)
  32. self.globalValueChanged.connect(self.activeStackChanged)
  33. ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeStackChanged)
  34. self._empty_variant_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_variant")[0]
  35. self._empty_material_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_material")[0]
  36. self._empty_quality_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_quality")[0]
  37. Preferences.getInstance().addPreference("cura/active_machine", "")
  38. self._global_event_keys = set()
  39. active_machine_id = Preferences.getInstance().getValue("cura/active_machine")
  40. self._printer_output_devices = []
  41. Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
  42. if active_machine_id != "":
  43. # An active machine was saved, so restore it.
  44. self.setActiveMachine(active_machine_id)
  45. if self._global_container_stack and self._global_container_stack.getProperty("machine_extruder_count", "value") > 1:
  46. # Make sure _active_container_stack is properly initiated
  47. ExtruderManager.getInstance().setActiveExtruderIndex(0)
  48. self._auto_change_material_hotend_flood_window = 10 # The minimum number of seconds between asking if the material or hotend on the machine should be used
  49. self._auto_change_material_hotend_flood_time = 0 # The last timestamp (in seconds) when the user was asked about changing the material or hotend to whatis loaded on the machine
  50. self._auto_change_material_hotend_flood_last_choice = None # The last choice that was made, so we can apply that choice again
  51. globalContainerChanged = pyqtSignal()
  52. activeMaterialChanged = pyqtSignal()
  53. activeVariantChanged = pyqtSignal()
  54. activeQualityChanged = pyqtSignal()
  55. activeStackChanged = pyqtSignal()
  56. globalValueChanged = pyqtSignal() # Emitted whenever a value inside global container is changed.
  57. globalValidationChanged = pyqtSignal() # Emitted whenever a validation inside global container is changed
  58. blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
  59. outputDevicesChanged = pyqtSignal()
  60. def _onOutputDevicesChanged(self):
  61. for printer_output_device in self._printer_output_devices:
  62. printer_output_device.hotendIdChanged.disconnect(self._onHotendIdChanged)
  63. printer_output_device.materialIdChanged.disconnect(self._onMaterialIdChanged)
  64. self._printer_output_devices.clear()
  65. for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
  66. if isinstance(printer_output_device, PrinterOutputDevice):
  67. self._printer_output_devices.append(printer_output_device)
  68. printer_output_device.hotendIdChanged.connect(self._onHotendIdChanged)
  69. printer_output_device.materialIdChanged.connect(self._onMaterialIdChanged)
  70. self.outputDevicesChanged.emit()
  71. @pyqtProperty("QVariantList", notify = outputDevicesChanged)
  72. def printerOutputDevices(self):
  73. return self._printer_output_devices
  74. def _onHotendIdChanged(self, index, hotend_id):
  75. if not self._global_container_stack:
  76. return
  77. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = self._global_container_stack.getBottom().getId(), name = hotend_id)
  78. if containers:
  79. extruder_manager = ExtruderManager.getInstance()
  80. old_index = extruder_manager.activeExtruderIndex
  81. if old_index != index:
  82. extruder_manager.setActiveExtruderIndex(index)
  83. else:
  84. old_index = None
  85. if self.activeVariantId != containers[0].getId():
  86. if time.time() - self._auto_change_material_hotend_flood_time > self._auto_change_material_hotend_flood_window:
  87. Application.getInstance().messageBox(catalog.i18nc("@window:title", "Changes on the Printer"), catalog.i18nc("@label", "Do you want to change the hotend to match the hotend in your printer?"),
  88. catalog.i18nc("@label", "The hotend on your printer was changed. For best results always slice for the hotend that is inserted in your printer."),
  89. buttons = QMessageBox.Yes + QMessageBox.No, icon = QMessageBox.Question, callback = self._hotendChangedDialogCallback, callback_arguments = [index, containers[0].getId()])
  90. else:
  91. self._hotendChangedDialogCallback(self._auto_change_material_hotend_flood_last_choice, index, containers[0].getId())
  92. if old_index is not None:
  93. extruder_manager.setActiveExtruderIndex(old_index)
  94. else:
  95. Logger.log("w", "No variant found for printer definition %s with id %s" % (self._global_container_stack.getBottom().getId(), hotend_id))
  96. def _hotendChangedDialogCallback(self, button, index, hotend_id):
  97. self._auto_change_material_hotend_flood_time = time.time()
  98. self._auto_change_material_hotend_flood_last_choice = button
  99. if button == QMessageBox.No:
  100. return
  101. Logger.log("d", "Setting hotend variant of hotend %d to %s" % (index, hotend_id))
  102. extruder_manager = ExtruderManager.getInstance()
  103. old_index = extruder_manager.activeExtruderIndex
  104. if old_index != index:
  105. extruder_manager.setActiveExtruderIndex(index)
  106. else:
  107. old_index = None
  108. self.setActiveVariant(hotend_id)
  109. if old_index is not None:
  110. extruder_manager.setActiveExtruderIndex(old_index)
  111. def _onMaterialIdChanged(self, index, material_id):
  112. if not self._global_container_stack:
  113. return
  114. definition_id = "fdmprinter"
  115. if self._global_container_stack.getMetaDataEntry("has_machine_materials", False):
  116. definition_id = self._global_container_stack.getBottom().getId()
  117. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "material", definition = definition_id, GUID = material_id)
  118. if containers:
  119. extruder_manager = ExtruderManager.getInstance()
  120. old_index = extruder_manager.activeExtruderIndex
  121. if old_index != index:
  122. extruder_manager.setActiveExtruderIndex(index)
  123. else:
  124. old_index = None
  125. if self.activeMaterialId != containers[0].getId():
  126. if time.time() - self._auto_change_material_hotend_flood_time > self._auto_change_material_hotend_flood_window:
  127. Application.getInstance().messageBox(catalog.i18nc("@window:title", "Changes on the Printer"), catalog.i18nc("@label", "Do you want to change the material to match the material in your printer?"),
  128. catalog.i18nc("@label", "The material on your printer was changed. For best results always slice for the material that is inserted in your printer."),
  129. buttons = QMessageBox.Yes + QMessageBox.No, icon = QMessageBox.Question, callback = self._materialIdChangedDialogCallback, callback_arguments = [index, containers[0].getId()])
  130. else:
  131. self._materialIdChangedDialogCallback(self._auto_change_material_hotend_flood_last_choice, index, containers[0].getId())
  132. if old_index is not None:
  133. extruder_manager.setActiveExtruderIndex(old_index)
  134. else:
  135. Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id))
  136. def _materialIdChangedDialogCallback(self, button, index, material_id):
  137. self._auto_change_material_hotend_flood_time = time.time()
  138. self._auto_change_material_hotend_flood_last_choice = button
  139. if button == QMessageBox.No:
  140. return
  141. Logger.log("d", "Setting material of hotend %d to %s" % (index, material_id))
  142. extruder_manager = ExtruderManager.getInstance()
  143. old_index = extruder_manager.activeExtruderIndex
  144. if old_index != index:
  145. extruder_manager.setActiveExtruderIndex(index)
  146. else:
  147. old_index = None
  148. self.setActiveMaterial(material_id)
  149. if old_index is not None:
  150. extruder_manager.setActiveExtruderIndex(old_index)
  151. def _onGlobalPropertyChanged(self, key, property_name):
  152. if property_name == "value":
  153. ## We can get recursion issues. So we store a list of keys that we are still handling to prevent this.
  154. if key in self._global_event_keys:
  155. return
  156. self._global_event_keys.add(key)
  157. self.globalValueChanged.emit()
  158. if self._active_container_stack and self._active_container_stack != self._global_container_stack:
  159. # Make the global current settings mirror the stack values appropriate for this setting
  160. if self._active_container_stack.getProperty("extruder_nr", "value") == int(self._active_container_stack.getProperty(key, "global_inherits_stack")):
  161. new_value = self._active_container_stack.getProperty(key, "value")
  162. self._global_container_stack.getTop().setProperty(key, "value", new_value)
  163. # Global-only setting values should be set on all extruders and the global stack
  164. if not self._global_container_stack.getProperty(key, "settable_per_extruder"):
  165. extruder_stacks = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
  166. target_stack_position = int(self._active_container_stack.getProperty(key, "global_inherits_stack"))
  167. if target_stack_position == -1: # Prevent -1 from selecting wrong stack.
  168. target_stack = self._active_container_stack
  169. else:
  170. target_stack = extruder_stacks[target_stack_position]
  171. new_value = target_stack.getProperty(key, "value")
  172. target_stack_has_user_value = target_stack.getTop().getInstance(key) != None
  173. for extruder_stack in extruder_stacks:
  174. if extruder_stack != target_stack:
  175. if target_stack_has_user_value:
  176. extruder_stack.getTop().setProperty(key, "value", new_value)
  177. else:
  178. # Remove from the value from the other stacks as well, unless the
  179. # top value from the other stacklevels is different than the new value
  180. for container in extruder_stack.getContainers():
  181. if container.__class__ == UM.Settings.InstanceContainer and container.getInstance(key) != None:
  182. if container.getProperty(key, "value") != new_value:
  183. # It could be that the setting needs to be removed instead of updated.
  184. temp = extruder_stack
  185. containers = extruder_stack.getContainers()
  186. # Ensure we have the entire 'chain'
  187. while temp.getNextStack():
  188. temp = temp.getNextStack()
  189. containers.extend(temp.getContainers())
  190. instance_needs_removal = False
  191. if len(containers) > 1:
  192. for index in range(1, len(containers)):
  193. deeper_container = containers[index]
  194. if deeper_container.getProperty(key, "value") is None:
  195. continue # Deeper container does not have the value, so continue.
  196. if deeper_container.getProperty(key, "value") == new_value:
  197. # Removal will result in correct value, so do that.
  198. # We do this to prevent the reset from showing up unneeded.
  199. instance_needs_removal = True
  200. break
  201. else:
  202. # Container has the value, but it's not the same. Stop looking.
  203. break
  204. if instance_needs_removal:
  205. extruder_stack.getTop().removeInstance(key)
  206. else:
  207. extruder_stack.getTop().setProperty(key, "value", new_value)
  208. else:
  209. # Check if we really need to remove something.
  210. if extruder_stack.getProperty(key, "value") != new_value:
  211. extruder_stack.getTop().removeInstance(key)
  212. break
  213. if self._global_container_stack.getProperty(key, "value") != new_value:
  214. self._global_container_stack.getTop().setProperty(key, "value", new_value)
  215. self._global_event_keys.remove(key)
  216. if property_name == "global_inherits_stack":
  217. if self._active_container_stack and self._active_container_stack != self._global_container_stack:
  218. # Update the global user value when the "global_inherits_stack" function points to a different stack
  219. stack_index = int(self._active_container_stack.getProperty(key, property_name))
  220. extruder_stacks = [stack for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())]
  221. if len(extruder_stacks) > stack_index:
  222. new_value = extruder_stacks[stack_index].getProperty(key, "value")
  223. if self._global_container_stack.getProperty(key, "value") != new_value:
  224. self._global_container_stack.getTop().setProperty(key, "value", new_value)
  225. if property_name == "validationState":
  226. if self._global_stack_valid:
  227. changed_validation_state = self._active_container_stack.getProperty(key, property_name)
  228. if changed_validation_state in (UM.Settings.ValidatorState.Exception, UM.Settings.ValidatorState.MaximumError, UM.Settings.ValidatorState.MinimumError):
  229. self._global_stack_valid = False
  230. self.globalValidationChanged.emit()
  231. else:
  232. has_errors = self._checkStackForErrors(self._active_container_stack)
  233. if not has_errors:
  234. self._global_stack_valid = True
  235. self.globalValidationChanged.emit()
  236. def _onGlobalContainerChanged(self):
  237. if self._global_container_stack:
  238. self._global_container_stack.nameChanged.disconnect(self._onMachineNameChanged)
  239. self._global_container_stack.containersChanged.disconnect(self._onInstanceContainersChanged)
  240. self._global_container_stack.propertyChanged.disconnect(self._onGlobalPropertyChanged)
  241. material = self._global_container_stack.findContainer({"type": "material"})
  242. material.nameChanged.disconnect(self._onMaterialNameChanged)
  243. quality = self._global_container_stack.findContainer({"type": "quality"})
  244. quality.nameChanged.disconnect(self._onQualityNameChanged)
  245. self._global_container_stack = Application.getInstance().getGlobalContainerStack()
  246. self._active_container_stack = self._global_container_stack
  247. self.globalContainerChanged.emit()
  248. if self._global_container_stack:
  249. Preferences.getInstance().setValue("cura/active_machine", self._global_container_stack.getId())
  250. self._global_container_stack.nameChanged.connect(self._onMachineNameChanged)
  251. self._global_container_stack.containersChanged.connect(self._onInstanceContainersChanged)
  252. self._global_container_stack.propertyChanged.connect(self._onGlobalPropertyChanged)
  253. self._global_stack_valid = not self._checkStackForErrors(self._global_container_stack)
  254. self.globalValidationChanged.emit()
  255. material = self._global_container_stack.findContainer({"type": "material"})
  256. material.nameChanged.connect(self._onMaterialNameChanged)
  257. quality = self._global_container_stack.findContainer({"type": "quality"})
  258. quality.nameChanged.connect(self._onQualityNameChanged)
  259. def _onActiveExtruderStackChanged(self):
  260. self.blurSettings.emit() # Ensure no-one has focus.
  261. if self._active_container_stack and self._active_container_stack != self._global_container_stack:
  262. self._active_container_stack.containersChanged.disconnect(self._onInstanceContainersChanged)
  263. self._active_container_stack.propertyChanged.disconnect(self._onGlobalPropertyChanged)
  264. self._active_container_stack = ExtruderManager.getInstance().getActiveExtruderStack()
  265. if self._active_container_stack:
  266. self._active_container_stack.containersChanged.connect(self._onInstanceContainersChanged)
  267. self._active_container_stack.propertyChanged.connect(self._onGlobalPropertyChanged)
  268. else:
  269. self._active_container_stack = self._global_container_stack
  270. def _onInstanceContainersChanged(self, container):
  271. container_type = container.getMetaDataEntry("type")
  272. if self._active_container_stack and self._active_container_stack != self._global_container_stack:
  273. if int(self._active_container_stack.getProperty("extruder_nr", "value")) == 0:
  274. global_container = self._global_container_stack.findContainer({"type": container_type})
  275. if global_container and global_container != container:
  276. container_index = self._global_container_stack.getContainerIndex(global_container)
  277. self._global_container_stack.replaceContainer(container_index, container)
  278. for key in container.getAllKeys():
  279. # Make sure the values in this profile are distributed to other stacks if necessary
  280. self._onGlobalPropertyChanged(key, "value")
  281. if container_type == "material":
  282. self.activeMaterialChanged.emit()
  283. elif container_type == "variant":
  284. self.activeVariantChanged.emit()
  285. elif container_type == "quality":
  286. self.activeQualityChanged.emit()
  287. @pyqtSlot(str)
  288. def setActiveMachine(self, stack_id):
  289. containers = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = stack_id)
  290. if containers:
  291. Application.getInstance().setGlobalContainerStack(containers[0])
  292. @pyqtSlot(str, str)
  293. def addMachine(self, name, definition_id):
  294. container_registry = UM.Settings.ContainerRegistry.getInstance()
  295. definitions = container_registry.findDefinitionContainers(id = definition_id)
  296. if definitions:
  297. definition = definitions[0]
  298. name = self._createUniqueName("machine", "", name, definition.getName())
  299. new_global_stack = UM.Settings.ContainerStack(name)
  300. new_global_stack.addMetaDataEntry("type", "machine")
  301. container_registry.addContainer(new_global_stack)
  302. variant_instance_container = self._updateVariantContainer(definition)
  303. material_instance_container = self._updateMaterialContainer(definition, variant_instance_container)
  304. quality_instance_container = self._updateQualityContainer(definition, material_instance_container)
  305. current_settings_instance_container = UM.Settings.InstanceContainer(name + "_current_settings")
  306. current_settings_instance_container.addMetaDataEntry("machine", name)
  307. current_settings_instance_container.addMetaDataEntry("type", "user")
  308. current_settings_instance_container.setDefinition(definitions[0])
  309. container_registry.addContainer(current_settings_instance_container)
  310. # If a definition is found, its a list. Should only have one item.
  311. new_global_stack.addContainer(definition)
  312. if variant_instance_container:
  313. new_global_stack.addContainer(variant_instance_container)
  314. if material_instance_container:
  315. new_global_stack.addContainer(material_instance_container)
  316. if quality_instance_container:
  317. new_global_stack.addContainer(quality_instance_container)
  318. new_global_stack.addContainer(current_settings_instance_container)
  319. ExtruderManager.getInstance().addMachineExtruders(definition, new_global_stack.getId())
  320. Application.getInstance().setGlobalContainerStack(new_global_stack)
  321. ## Create a name that is not empty and unique
  322. # \param container_type \type{string} Type of the container (machine, quality, ...)
  323. # \param current_name \type{} Current name of the container, which may be an acceptable option
  324. # \param new_name \type{string} Base name, which may not be unique
  325. # \param fallback_name \type{string} Name to use when (stripped) new_name is empty
  326. # \return \type{string} Name that is unique for the specified type and name/id
  327. def _createUniqueName(self, container_type, current_name, new_name, fallback_name):
  328. return UM.Settings.ContainerRegistry.getInstance().createUniqueName(container_type, current_name, new_name, fallback_name)
  329. ## Convenience function to check if a stack has errors.
  330. def _checkStackForErrors(self, stack):
  331. if stack is None:
  332. return False
  333. for key in stack.getAllKeys():
  334. validation_state = stack.getProperty(key, "validationState")
  335. if validation_state in (UM.Settings.ValidatorState.Exception, UM.Settings.ValidatorState.MaximumError, UM.Settings.ValidatorState.MinimumError):
  336. return True
  337. return False
  338. ## Remove all instances from the top instanceContainer (effectively removing all user-changed settings)
  339. @pyqtSlot()
  340. def clearUserSettings(self):
  341. if not self._active_container_stack:
  342. return
  343. self.blurSettings.emit()
  344. user_settings = self._active_container_stack.getTop()
  345. user_settings.clear()
  346. ## Check if the global_container has instances in the user container
  347. @pyqtProperty(bool, notify = activeStackChanged)
  348. def hasUserSettings(self):
  349. if not self._active_container_stack:
  350. return False
  351. user_settings = self._active_container_stack.getTop().findInstances(**{})
  352. return len(user_settings) != 0
  353. ## Check if the global profile does not contain error states
  354. # Note that the _global_stack_valid is cached due to performance issues
  355. # Calling _checkStackForErrors on every change is simply too expensive
  356. @pyqtProperty(bool, notify = globalValidationChanged)
  357. def isGlobalStackValid(self):
  358. return bool(self._global_stack_valid)
  359. @pyqtProperty(str, notify = activeStackChanged)
  360. def activeUserProfileId(self):
  361. if self._active_container_stack:
  362. return self._active_container_stack.getTop().getId()
  363. return ""
  364. @pyqtProperty(str, notify = globalContainerChanged)
  365. def activeMachineName(self):
  366. if self._global_container_stack:
  367. return self._global_container_stack.getName()
  368. return ""
  369. @pyqtProperty(str, notify = globalContainerChanged)
  370. def activeMachineId(self):
  371. if self._global_container_stack:
  372. return self._global_container_stack.getId()
  373. return ""
  374. @pyqtProperty(str, notify = activeStackChanged)
  375. def activeStackId(self):
  376. if self._active_container_stack:
  377. return self._active_container_stack.getId()
  378. return ""
  379. @pyqtProperty(str, notify = activeMaterialChanged)
  380. def activeMaterialName(self):
  381. if self._active_container_stack:
  382. material = self._active_container_stack.findContainer({"type":"material"})
  383. if material:
  384. return material.getName()
  385. return ""
  386. @pyqtProperty(str, notify=activeMaterialChanged)
  387. def activeMaterialId(self):
  388. if self._active_container_stack:
  389. material = self._active_container_stack.findContainer({"type": "material"})
  390. if material:
  391. return material.getId()
  392. return ""
  393. @pyqtProperty(str, notify=activeQualityChanged)
  394. def activeQualityName(self):
  395. if self._active_container_stack:
  396. quality = self._active_container_stack.findContainer({"type": "quality"})
  397. if quality:
  398. return quality.getName()
  399. return ""
  400. @pyqtProperty(str, notify=activeQualityChanged)
  401. def activeQualityId(self):
  402. if self._active_container_stack:
  403. quality = self._active_container_stack.findContainer({"type": "quality"})
  404. if quality:
  405. return quality.getId()
  406. return ""
  407. ## Check if a container is read_only
  408. @pyqtSlot(str, result = bool)
  409. def isReadOnly(self, container_id):
  410. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
  411. if not containers or not self._active_container_stack:
  412. return True
  413. return containers[0].isReadOnly()
  414. ## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
  415. @pyqtSlot(str)
  416. def copyValueToExtruders(self, key):
  417. if not self._active_container_stack or self._global_container_stack.getProperty("machine_extruder_count", "value") <= 1:
  418. return
  419. new_value = self._active_container_stack.getProperty(key, "value")
  420. stacks = [stack for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())]
  421. stacks.append(self._global_container_stack)
  422. for extruder_stack in stacks:
  423. if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
  424. extruder_stack.getTop().setProperty(key, "value", new_value)
  425. @pyqtSlot(result = str)
  426. def newQualityContainerFromQualityAndUser(self):
  427. new_container_id = self.duplicateContainer(self.activeQualityId)
  428. if new_container_id == "":
  429. return
  430. self.blurSettings.emit()
  431. self.updateQualityContainerFromUserContainer(new_container_id)
  432. self.setActiveQuality(new_container_id)
  433. return new_container_id
  434. @pyqtSlot(str, result=str)
  435. def duplicateContainer(self, container_id):
  436. if not self._active_container_stack:
  437. return ""
  438. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
  439. if containers:
  440. new_name = self._createUniqueName("quality", "", containers[0].getName(), catalog.i18nc("@label", "Custom profile"))
  441. new_container = containers[0].duplicate(new_name, new_name)
  442. UM.Settings.ContainerRegistry.getInstance().addContainer(new_container)
  443. return new_name
  444. return ""
  445. @pyqtSlot(str, str)
  446. def renameQualityContainer(self, container_id, new_name):
  447. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id, type = "quality")
  448. if containers:
  449. new_name = self._createUniqueName("quality", containers[0].getName(), new_name,
  450. catalog.i18nc("@label", "Custom profile"))
  451. if containers[0].getName() == new_name:
  452. # Nothing to do.
  453. return
  454. # As we also want the id of the container to be changed (so that profile name is the name of the file
  455. # on disk. We need to create a new instance and remove it (so the old file of the container is removed)
  456. # If we don't do that, we might get duplicates & other weird issues.
  457. new_container = UM.Settings.InstanceContainer("")
  458. new_container.deserialize(containers[0].serialize())
  459. # Actually set the name
  460. new_container.setName(new_name)
  461. new_container._id = new_name # Todo: Fix proper id change function for this.
  462. # Add the "new" container.
  463. UM.Settings.ContainerRegistry.getInstance().addContainer(new_container)
  464. # Ensure that the renamed profile is saved -before- we remove the old profile.
  465. Application.getInstance().saveSettings()
  466. # Actually set & remove new / old quality.
  467. self.setActiveQuality(new_name)
  468. self.removeQualityContainer(containers[0].getId())
  469. @pyqtSlot(str)
  470. def removeQualityContainer(self, container_id):
  471. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
  472. if not containers or not self._active_container_stack:
  473. return
  474. # If the container that is being removed is the currently active container, set another machine as the active container
  475. activate_new_container = container_id == self.activeQualityId
  476. UM.Settings.ContainerRegistry.getInstance().removeContainer(container_id)
  477. if activate_new_container:
  478. definition_id = "fdmprinter" if not self.filterQualityByMachine else self.activeDefinitionId
  479. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "quality", definition = definition_id)
  480. if containers:
  481. self.setActiveQuality(containers[0].getId())
  482. self.activeQualityChanged.emit()
  483. @pyqtSlot(str)
  484. @pyqtSlot()
  485. def updateQualityContainerFromUserContainer(self, quality_id = None):
  486. if not self._active_container_stack:
  487. return
  488. if quality_id:
  489. quality = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = quality_id, type = "quality")
  490. if quality:
  491. quality = quality[0]
  492. else:
  493. quality = self._active_container_stack.findContainer({"type": "quality"})
  494. if not quality:
  495. return
  496. user_settings = self._active_container_stack.getTop()
  497. for key in user_settings.getAllKeys():
  498. quality.setProperty(key, "value", user_settings.getProperty(key, "value"))
  499. self.clearUserSettings() # As all users settings are noq a quality, remove them.
  500. @pyqtSlot(str)
  501. def setActiveMaterial(self, material_id):
  502. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = material_id)
  503. if not containers or not self._active_container_stack:
  504. return
  505. old_material = self._active_container_stack.findContainer({"type":"material"})
  506. old_quality = self._active_container_stack.findContainer({"type": "quality"})
  507. if old_material:
  508. old_material.nameChanged.disconnect(self._onMaterialNameChanged)
  509. material_index = self._active_container_stack.getContainerIndex(old_material)
  510. self._active_container_stack.replaceContainer(material_index, containers[0])
  511. containers[0].nameChanged.connect(self._onMaterialNameChanged)
  512. preferred_quality_name = None
  513. if old_quality:
  514. preferred_quality_name = old_quality.getName()
  515. self.setActiveQuality(self._updateQualityContainer(self._global_container_stack.getBottom(), containers[0], preferred_quality_name).id)
  516. else:
  517. Logger.log("w", "While trying to set the active material, no material was found to replace.")
  518. @pyqtSlot(str)
  519. def setActiveVariant(self, variant_id):
  520. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = variant_id)
  521. if not containers or not self._active_container_stack:
  522. return
  523. old_variant = self._active_container_stack.findContainer({"type": "variant"})
  524. old_material = self._active_container_stack.findContainer({"type": "material"})
  525. if old_variant:
  526. variant_index = self._active_container_stack.getContainerIndex(old_variant)
  527. self._active_container_stack.replaceContainer(variant_index, containers[0])
  528. preferred_material = None
  529. if old_material:
  530. preferred_material_name = old_material.getName()
  531. self.setActiveMaterial(self._updateMaterialContainer(self._global_container_stack.getBottom(), containers[0], preferred_material_name).id)
  532. else:
  533. Logger.log("w", "While trying to set the active variant, no variant was found to replace.")
  534. @pyqtSlot(str)
  535. def setActiveQuality(self, quality_id):
  536. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = quality_id)
  537. if not containers or not self._active_container_stack:
  538. return
  539. old_quality = self._active_container_stack.findContainer({"type": "quality"})
  540. if old_quality and old_quality != containers[0]:
  541. old_quality.nameChanged.disconnect(self._onQualityNameChanged)
  542. quality_index = self._active_container_stack.getContainerIndex(old_quality)
  543. self._active_container_stack.replaceContainer(quality_index, containers[0])
  544. containers[0].nameChanged.connect(self._onQualityNameChanged)
  545. if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1:
  546. # Ask the user if the user profile should be cleared or not (discarding the current settings)
  547. # In Simple Mode we assume the user always wants to keep the (limited) current settings
  548. details = catalog.i18nc("@label", "You made changes to the following setting(s):")
  549. user_settings = self._active_container_stack.getTop().findInstances(**{})
  550. for setting in user_settings:
  551. details = details + "\n " + setting.definition.label
  552. Application.getInstance().messageBox(catalog.i18nc("@window:title", "Switched profiles"), catalog.i18nc("@label", "Do you want to transfer your changed settings to this profile?"),
  553. catalog.i18nc("@label", "If you transfer your settings they will override settings in the profile."), details,
  554. buttons = QMessageBox.Yes + QMessageBox.No, icon = QMessageBox.Question, callback = self._keepUserSettingsDialogCallback)
  555. else:
  556. Logger.log("w", "While trying to set the active quality, no quality was found to replace.")
  557. def _keepUserSettingsDialogCallback(self, button):
  558. if button == QMessageBox.Yes:
  559. # Yes, keep the settings in the user profile with this profile
  560. pass
  561. elif button == QMessageBox.No:
  562. # No, discard the settings in the user profile
  563. self.clearUserSettings()
  564. @pyqtProperty(str, notify = activeVariantChanged)
  565. def activeVariantName(self):
  566. if self._active_container_stack:
  567. variant = self._active_container_stack.findContainer({"type": "variant"})
  568. if variant:
  569. return variant.getName()
  570. return ""
  571. @pyqtProperty(str, notify = activeVariantChanged)
  572. def activeVariantId(self):
  573. if self._active_container_stack:
  574. variant = self._active_container_stack.findContainer({"type": "variant"})
  575. if variant:
  576. return variant.getId()
  577. return ""
  578. @pyqtProperty(str, notify = globalContainerChanged)
  579. def activeDefinitionId(self):
  580. if self._global_container_stack:
  581. definition = self._global_container_stack.getBottom()
  582. if definition:
  583. return definition.id
  584. return ""
  585. @pyqtSlot(str, str)
  586. def renameMachine(self, machine_id, new_name):
  587. containers = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
  588. if containers:
  589. new_name = self._createUniqueName("machine", containers[0].getName(), new_name, containers[0].getBottom().getName())
  590. containers[0].setName(new_name)
  591. self.globalContainerChanged.emit()
  592. @pyqtSlot(str)
  593. def removeMachine(self, machine_id):
  594. # If the machine that is being removed is the currently active machine, set another machine as the active machine.
  595. activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id)
  596. stacks = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
  597. if not stacks:
  598. return
  599. ExtruderManager.getInstance().removeMachineExtruders(stacks[0].getBottom().getId())
  600. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "user", machine = machine_id)
  601. for container in containers:
  602. UM.Settings.ContainerRegistry.getInstance().removeContainer(container.getId())
  603. UM.Settings.ContainerRegistry.getInstance().removeContainer(machine_id)
  604. if activate_new_machine:
  605. stacks = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(type = "machine")
  606. if stacks:
  607. Application.getInstance().setGlobalContainerStack(stacks[0])
  608. @pyqtProperty(bool, notify = globalContainerChanged)
  609. def hasMaterials(self):
  610. if self._global_container_stack:
  611. return bool(self._global_container_stack.getMetaDataEntry("has_materials", False))
  612. return False
  613. @pyqtProperty(bool, notify = globalContainerChanged)
  614. def hasVariants(self):
  615. if self._global_container_stack:
  616. return bool(self._global_container_stack.getMetaDataEntry("has_variants", False))
  617. return False
  618. ## Property to indicate if a machine has "specialized" material profiles.
  619. # Some machines have their own material profiles that "override" the default catch all profiles.
  620. @pyqtProperty(bool, notify = globalContainerChanged)
  621. def filterMaterialsByMachine(self):
  622. if self._global_container_stack:
  623. return bool(self._global_container_stack.getMetaDataEntry("has_machine_materials", False))
  624. return False
  625. ## Property to indicate if a machine has "specialized" quality profiles.
  626. # Some machines have their own quality profiles that "override" the default catch all profiles.
  627. @pyqtProperty(bool, notify = globalContainerChanged)
  628. def filterQualityByMachine(self):
  629. if self._global_container_stack:
  630. return bool(self._global_container_stack.getMetaDataEntry("has_machine_quality", False))
  631. return False
  632. ## Get the Definition ID of a machine (specified by ID)
  633. # \param machine_id string machine id to get the definition ID of
  634. # \returns DefinitionID (string) if found, None otherwise
  635. @pyqtSlot(str, result = str)
  636. def getDefinitionByMachineId(self, machine_id):
  637. containers = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id=machine_id)
  638. if containers:
  639. return containers[0].getBottom().getId()
  640. @staticmethod
  641. def createMachineManager(engine=None, script_engine=None):
  642. return MachineManager()
  643. def _updateVariantContainer(self, definition):
  644. if not definition.getMetaDataEntry("has_variants"):
  645. return self._empty_variant_container
  646. containers = []
  647. preferred_variant = definition.getMetaDataEntry("preferred_variant")
  648. if preferred_variant:
  649. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = definition.id, id = preferred_variant)
  650. if not containers:
  651. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = definition.id)
  652. if containers:
  653. return containers[0]
  654. return self._empty_variant_container
  655. def _updateMaterialContainer(self, definition, variant_container = None, preferred_material_name = None):
  656. if not definition.getMetaDataEntry("has_materials"):
  657. return self._empty_material_container
  658. search_criteria = { "type": "material" }
  659. if definition.getMetaDataEntry("has_machine_materials"):
  660. search_criteria["definition"] = definition.id
  661. if definition.getMetaDataEntry("has_variants") and variant_container:
  662. search_criteria["variant"] = variant_container.id
  663. else:
  664. search_criteria["definition"] = "fdmprinter"
  665. if preferred_material_name:
  666. search_criteria["name"] = preferred_material_name
  667. else:
  668. preferred_material = definition.getMetaDataEntry("preferred_material")
  669. if preferred_material:
  670. search_criteria["id"] = preferred_material
  671. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
  672. if containers:
  673. return containers[0]
  674. if "name" in search_criteria or "id" in search_criteria:
  675. # If a material by this name can not be found, try a wider set of search criteria
  676. search_criteria.pop("name", None)
  677. search_criteria.pop("id", None)
  678. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
  679. if containers:
  680. return containers[0]
  681. return self._empty_material_container
  682. def _updateQualityContainer(self, definition, material_container = None, preferred_quality_name = None):
  683. search_criteria = { "type": "quality" }
  684. if definition.getMetaDataEntry("has_machine_quality"):
  685. search_criteria["definition"] = definition.id
  686. if definition.getMetaDataEntry("has_materials") and material_container:
  687. search_criteria["material"] = material_container.id
  688. else:
  689. search_criteria["definition"] = "fdmprinter"
  690. if preferred_quality_name:
  691. search_criteria["name"] = preferred_quality_name
  692. else:
  693. preferred_quality = definition.getMetaDataEntry("preferred_quality")
  694. if preferred_quality:
  695. search_criteria["id"] = preferred_quality
  696. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
  697. if containers:
  698. return containers[0]
  699. if "name" in search_criteria or "id" in search_criteria:
  700. # If a quality by this name can not be found, try a wider set of search criteria
  701. search_criteria.pop("name", None)
  702. search_criteria.pop("id", None)
  703. containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
  704. if containers:
  705. return containers[0]
  706. return self._empty_quality_container
  707. def _onMachineNameChanged(self):
  708. self.globalContainerChanged.emit()
  709. def _onMaterialNameChanged(self):
  710. self.activeMaterialChanged.emit()
  711. def _onQualityNameChanged(self):
  712. self.activeQualityChanged.emit()