CuraContainerRegistry.py 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import os
  4. import re
  5. import configparser
  6. from typing import cast, Optional
  7. from PyQt5.QtWidgets import QMessageBox
  8. from UM.Decorators import override
  9. from UM.Settings.ContainerFormatError import ContainerFormatError
  10. from UM.Settings.ContainerRegistry import ContainerRegistry
  11. from UM.Settings.ContainerStack import ContainerStack
  12. from UM.Settings.InstanceContainer import InstanceContainer
  13. from UM.Settings.SettingInstance import SettingInstance
  14. from UM.Application import Application
  15. from UM.Logger import Logger
  16. from UM.Message import Message
  17. from UM.Platform import Platform
  18. from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with.
  19. from UM.Util import parseBool
  20. from UM.Resources import Resources
  21. from . import ExtruderStack
  22. from . import GlobalStack
  23. import cura.CuraApplication
  24. from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
  25. from cura.ReaderWriters.ProfileReader import NoProfileException
  26. from UM.i18n import i18nCatalog
  27. catalog = i18nCatalog("cura")
  28. class CuraContainerRegistry(ContainerRegistry):
  29. def __init__(self, *args, **kwargs):
  30. super().__init__(*args, **kwargs)
  31. # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack
  32. # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack
  33. # is added, we check to see if an extruder stack needs to be added.
  34. self.containerAdded.connect(self._onContainerAdded)
  35. ## Overridden from ContainerRegistry
  36. #
  37. # Adds a container to the registry.
  38. #
  39. # This will also try to convert a ContainerStack to either Extruder or
  40. # Global stack based on metadata information.
  41. @override(ContainerRegistry)
  42. def addContainer(self, container):
  43. # Note: Intentional check with type() because we want to ignore subclasses
  44. if type(container) == ContainerStack:
  45. container = self._convertContainerStack(container)
  46. if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
  47. # Check against setting version of the definition.
  48. required_setting_version = cura.CuraApplication.CuraApplication.SettingVersion
  49. actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
  50. if required_setting_version != actual_setting_version:
  51. Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version))
  52. return #Don't add.
  53. super().addContainer(container)
  54. ## Create a name that is not empty and unique
  55. # \param container_type \type{string} Type of the container (machine, quality, ...)
  56. # \param current_name \type{} Current name of the container, which may be an acceptable option
  57. # \param new_name \type{string} Base name, which may not be unique
  58. # \param fallback_name \type{string} Name to use when (stripped) new_name is empty
  59. # \return \type{string} Name that is unique for the specified type and name/id
  60. def createUniqueName(self, container_type, current_name, new_name, fallback_name):
  61. new_name = new_name.strip()
  62. num_check = re.compile("(.*?)\s*#\d+$").match(new_name)
  63. if num_check:
  64. new_name = num_check.group(1)
  65. if new_name == "":
  66. new_name = fallback_name
  67. unique_name = new_name
  68. i = 1
  69. # In case we are renaming, the current name of the container is also a valid end-result
  70. while self._containerExists(container_type, unique_name) and unique_name != current_name:
  71. i += 1
  72. unique_name = "%s #%d" % (new_name, i)
  73. return unique_name
  74. ## Check if a container with of a certain type and a certain name or id exists
  75. # Both the id and the name are checked, because they may not be the same and it is better if they are both unique
  76. # \param container_type \type{string} Type of the container (machine, quality, ...)
  77. # \param container_name \type{string} Name to check
  78. def _containerExists(self, container_type, container_name):
  79. container_class = ContainerStack if container_type == "machine" else InstanceContainer
  80. return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \
  81. self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type)
  82. ## Exports an profile to a file
  83. #
  84. # \param instance_ids \type{list} the IDs of the profiles to export.
  85. # \param file_name \type{str} the full path and filename to export to.
  86. # \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
  87. def exportQualityProfile(self, container_list, file_name, file_type):
  88. # Parse the fileType to deduce what plugin can save the file format.
  89. # fileType has the format "<description> (*.<extension>)"
  90. split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
  91. if split < 0: # Not found. Invalid format.
  92. Logger.log("e", "Invalid file format identifier %s", file_type)
  93. return
  94. description = file_type[:split]
  95. extension = file_type[split + 4:-1] # Leave out the " (*." and ")".
  96. if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any.
  97. file_name += "." + extension
  98. # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself.
  99. if not Platform.isWindows():
  100. if os.path.exists(file_name):
  101. result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
  102. catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
  103. if result == QMessageBox.No:
  104. return
  105. profile_writer = self._findProfileWriter(extension, description)
  106. try:
  107. success = profile_writer.write(file_name, container_list)
  108. except Exception as e:
  109. Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
  110. m = Message(catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", file_name, str(e)),
  111. lifetime = 0,
  112. title = catalog.i18nc("@info:title", "Error"))
  113. m.show()
  114. return
  115. if not success:
  116. Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name)
  117. m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name),
  118. lifetime = 0,
  119. title = catalog.i18nc("@info:title", "Error"))
  120. m.show()
  121. return
  122. m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Exported profile to <filename>{0}</filename>", file_name),
  123. title = catalog.i18nc("@info:title", "Export succeeded"))
  124. m.show()
  125. ## Gets the plugin object matching the criteria
  126. # \param extension
  127. # \param description
  128. # \return The plugin object matching the given extension and description.
  129. def _findProfileWriter(self, extension, description):
  130. plugin_registry = PluginRegistry.getInstance()
  131. for plugin_id, meta_data in self._getIOPlugins("profile_writer"):
  132. for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write.
  133. supported_extension = supported_type.get("extension", None)
  134. if supported_extension == extension: # This plugin supports a file type with the same extension.
  135. supported_description = supported_type.get("description", None)
  136. if supported_description == description: # The description is also identical. Assume it's the same file type.
  137. return plugin_registry.getPluginObject(plugin_id)
  138. return None
  139. ## Imports a profile from a file
  140. #
  141. # \param file_name \type{str} the full path and filename of the profile to import
  142. # \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key
  143. # containing a message for the user
  144. def importProfile(self, file_name):
  145. Logger.log("d", "Attempting to import profile %s", file_name)
  146. if not file_name:
  147. return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "Invalid path")}
  148. plugin_registry = PluginRegistry.getInstance()
  149. extension = file_name.split(".")[-1]
  150. global_stack = Application.getInstance().getGlobalContainerStack()
  151. if not global_stack:
  152. return
  153. machine_extruders = []
  154. for position in sorted(global_stack.extruders):
  155. machine_extruders.append(global_stack.extruders[position])
  156. for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
  157. if meta_data["profile_reader"][0]["extension"] != extension:
  158. continue
  159. profile_reader = plugin_registry.getPluginObject(plugin_id)
  160. try:
  161. profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
  162. except NoProfileException:
  163. return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "No custom profile to import in file <filename>{0}</filename>", file_name)}
  164. except Exception as e:
  165. # Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None.
  166. Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name, profile_reader.getPluginId(), str(e))
  167. return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "\n" + str(e))}
  168. if profile_or_list:
  169. # Ensure it is always a list of profiles
  170. if not isinstance(profile_or_list, list):
  171. profile_or_list = [profile_or_list]
  172. # First check if this profile is suitable for this machine
  173. global_profile = None
  174. extruder_profiles = []
  175. if len(profile_or_list) == 1:
  176. global_profile = profile_or_list[0]
  177. else:
  178. for profile in profile_or_list:
  179. if not profile.getMetaDataEntry("position"):
  180. global_profile = profile
  181. else:
  182. extruder_profiles.append(profile)
  183. extruder_profiles = sorted(extruder_profiles, key = lambda x: int(x.getMetaDataEntry("position")))
  184. profile_or_list = [global_profile] + extruder_profiles
  185. if not global_profile:
  186. Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name)
  187. return { "status": "error",
  188. "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)}
  189. profile_definition = global_profile.getMetaDataEntry("definition")
  190. # Make sure we have a profile_definition in the file:
  191. if profile_definition is None:
  192. break
  193. machine_definition = self.findDefinitionContainers(id = profile_definition)
  194. if not machine_definition:
  195. Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
  196. return {"status": "error",
  197. "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)
  198. }
  199. machine_definition = machine_definition[0]
  200. # Get the expected machine definition.
  201. # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode...
  202. profile_definition = getMachineDefinitionIDForQualitySearch(machine_definition)
  203. expected_machine_definition = getMachineDefinitionIDForQualitySearch(global_stack.definition)
  204. # And check if the profile_definition matches either one (showing error if not):
  205. if profile_definition != expected_machine_definition:
  206. Logger.log("e", "Profile [%s] is for machine [%s] but the current active machine is [%s]. Will not import the profile", file_name, profile_definition, expected_machine_definition)
  207. return { "status": "error",
  208. "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "The machine defined in profile <filename>{0}</filename> ({1}) doesn't match with your current machine ({2}), could not import it.", file_name, profile_definition, expected_machine_definition)}
  209. # Fix the global quality profile's definition field in case it's not correct
  210. global_profile.setMetaDataEntry("definition", expected_machine_definition)
  211. quality_name = global_profile.getName()
  212. quality_type = global_profile.getMetaDataEntry("quality_type")
  213. name_seed = os.path.splitext(os.path.basename(file_name))[0]
  214. new_name = self.uniqueName(name_seed)
  215. # Ensure it is always a list of profiles
  216. if type(profile_or_list) is not list:
  217. profile_or_list = [profile_or_list]
  218. # Make sure that there are also extruder stacks' quality_changes, not just one for the global stack
  219. if len(profile_or_list) == 1:
  220. global_profile = profile_or_list[0]
  221. extruder_profiles = []
  222. for idx, extruder in enumerate(global_stack.extruders.values()):
  223. profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1))
  224. profile = InstanceContainer(profile_id)
  225. profile.setName(quality_name)
  226. profile.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion)
  227. profile.setMetaDataEntry("type", "quality_changes")
  228. profile.setMetaDataEntry("definition", expected_machine_definition)
  229. profile.setMetaDataEntry("quality_type", quality_type)
  230. profile.setMetaDataEntry("position", "0")
  231. profile.setDirty(True)
  232. if idx == 0:
  233. # move all per-extruder settings to the first extruder's quality_changes
  234. for qc_setting_key in global_profile.getAllKeys():
  235. settable_per_extruder = global_stack.getProperty(qc_setting_key,
  236. "settable_per_extruder")
  237. if settable_per_extruder:
  238. setting_value = global_profile.getProperty(qc_setting_key, "value")
  239. setting_definition = global_stack.getSettingDefinition(qc_setting_key)
  240. new_instance = SettingInstance(setting_definition, profile)
  241. new_instance.setProperty("value", setting_value)
  242. new_instance.resetState() # Ensure that the state is not seen as a user state.
  243. profile.addInstance(new_instance)
  244. profile.setDirty(True)
  245. global_profile.removeInstance(qc_setting_key, postpone_emit=True)
  246. extruder_profiles.append(profile)
  247. for profile in extruder_profiles:
  248. profile_or_list.append(profile)
  249. # Import all profiles
  250. for profile_index, profile in enumerate(profile_or_list):
  251. if profile_index == 0:
  252. # This is assumed to be the global profile
  253. profile_id = (global_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
  254. elif profile_index < len(machine_extruders) + 1:
  255. # This is assumed to be an extruder profile
  256. extruder_id = machine_extruders[profile_index - 1].definition.getId()
  257. extruder_position = str(profile_index - 1)
  258. if not profile.getMetaDataEntry("position"):
  259. profile.setMetaDataEntry("position", extruder_position)
  260. else:
  261. profile.setMetaDataEntry("position", extruder_position)
  262. profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
  263. else: #More extruders in the imported file than in the machine.
  264. continue #Delete the additional profiles.
  265. result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
  266. if result is not None:
  267. return {"status": "error", "message": catalog.i18nc(
  268. "@info:status Don't translate the XML tags <filename> or <message>!",
  269. "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>",
  270. file_name, result)}
  271. return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
  272. # This message is throw when the profile reader doesn't find any profile in the file
  273. return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)}
  274. # If it hasn't returned by now, none of the plugins loaded the profile successfully.
  275. return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
  276. @override(ContainerRegistry)
  277. def load(self):
  278. super().load()
  279. self._registerSingleExtrusionMachinesExtruderStacks()
  280. self._connectUpgradedExtruderStacksToMachines()
  281. ## Update an imported profile to match the current machine configuration.
  282. #
  283. # \param profile The profile to configure.
  284. # \param id_seed The base ID for the profile. May be changed so it does not conflict with existing containers.
  285. # \param new_name The new name for the profile.
  286. #
  287. # \return None if configuring was successful or an error message if an error occurred.
  288. def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]:
  289. profile.setDirty(True) # Ensure the profiles are correctly saved
  290. new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile"))
  291. profile.setMetaDataEntry("id", new_id)
  292. profile.setName(new_name)
  293. # Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile
  294. # It also solves an issue with importing profiles from G-Codes
  295. profile.setMetaDataEntry("id", new_id)
  296. profile.setMetaDataEntry("definition", machine_definition_id)
  297. if "type" in profile.getMetaData():
  298. profile.setMetaDataEntry("type", "quality_changes")
  299. else:
  300. profile.setMetaDataEntry("type", "quality_changes")
  301. quality_type = profile.getMetaDataEntry("quality_type")
  302. if not quality_type:
  303. return catalog.i18nc("@info:status", "Profile is missing a quality type.")
  304. global_stack = Application.getInstance().getGlobalContainerStack()
  305. if global_stack is None:
  306. return None
  307. definition_id = getMachineDefinitionIDForQualitySearch(global_stack.definition)
  308. profile.setDefinition(definition_id)
  309. # Check to make sure the imported profile actually makes sense in context of the current configuration.
  310. # This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as
  311. # successfully imported but then fail to show up.
  312. quality_manager = cura.CuraApplication.CuraApplication.getInstance()._quality_manager
  313. quality_group_dict = quality_manager.getQualityGroupsForMachineDefinition(global_stack)
  314. if quality_type not in quality_group_dict:
  315. return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
  316. ContainerRegistry.getInstance().addContainer(profile)
  317. return None
  318. ## Gets a list of profile writer plugins
  319. # \return List of tuples of (plugin_id, meta_data).
  320. def _getIOPlugins(self, io_type):
  321. plugin_registry = PluginRegistry.getInstance()
  322. active_plugin_ids = plugin_registry.getActivePlugins()
  323. result = []
  324. for plugin_id in active_plugin_ids:
  325. meta_data = plugin_registry.getMetaData(plugin_id)
  326. if io_type in meta_data:
  327. result.append( (plugin_id, meta_data) )
  328. return result
  329. ## Returns true if the current machine requires its own materials
  330. # \return True if the current machine requires its own materials
  331. def _machineHasOwnMaterials(self):
  332. global_container_stack = Application.getInstance().getGlobalContainerStack()
  333. if global_container_stack:
  334. return global_container_stack.getMetaDataEntry("has_materials", False)
  335. return False
  336. ## Gets the ID of the active material
  337. # \return the ID of the active material or the empty string
  338. def _activeMaterialId(self):
  339. global_container_stack = Application.getInstance().getGlobalContainerStack()
  340. if global_container_stack and global_container_stack.material:
  341. return global_container_stack.material.getId()
  342. return ""
  343. ## Returns true if the current machine requires its own quality profiles
  344. # \return true if the current machine requires its own quality profiles
  345. def _machineHasOwnQualities(self):
  346. global_container_stack = Application.getInstance().getGlobalContainerStack()
  347. if global_container_stack:
  348. return parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", False))
  349. return False
  350. ## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
  351. def _convertContainerStack(self, container):
  352. assert type(container) == ContainerStack
  353. container_type = container.getMetaDataEntry("type")
  354. if container_type not in ("extruder_train", "machine"):
  355. # It is not an extruder or machine, so do nothing with the stack
  356. return container
  357. Logger.log("d", "Converting ContainerStack {stack} to {type}", stack = container.getId(), type = container_type)
  358. if container_type == "extruder_train":
  359. new_stack = ExtruderStack.ExtruderStack(container.getId())
  360. else:
  361. new_stack = GlobalStack.GlobalStack(container.getId())
  362. container_contents = container.serialize()
  363. new_stack.deserialize(container_contents)
  364. # Delete the old configuration file so we do not get double stacks
  365. if os.path.isfile(container.getPath()):
  366. os.remove(container.getPath())
  367. return new_stack
  368. def _registerSingleExtrusionMachinesExtruderStacks(self):
  369. machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"})
  370. for machine in machines:
  371. extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId())
  372. if not extruder_stacks:
  373. self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder")
  374. def _onContainerAdded(self, container):
  375. # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack
  376. # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack
  377. # is added, we check to see if an extruder stack needs to be added.
  378. if not isinstance(container, ContainerStack) or container.getMetaDataEntry("type") != "machine":
  379. return
  380. machine_extruder_trains = container.getMetaDataEntry("machine_extruder_trains")
  381. if machine_extruder_trains is not None and machine_extruder_trains != {"0": "fdmextruder"}:
  382. return
  383. extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = container.getId())
  384. if not extruder_stacks:
  385. self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder")
  386. #
  387. # new_global_quality_changes is optional. It is only used in project loading for a scenario like this:
  388. # - override the current machine
  389. # - create new for custom quality profile
  390. # new_global_quality_changes is the new global quality changes container in this scenario.
  391. # create_new_ids indicates if new unique ids must be created
  392. #
  393. def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id, new_global_quality_changes = None, create_new_ids = True):
  394. new_extruder_id = extruder_id
  395. application = cura.CuraApplication.CuraApplication.getInstance()
  396. extruder_definitions = self.findDefinitionContainers(id = new_extruder_id)
  397. if not extruder_definitions:
  398. Logger.log("w", "Could not find definition containers for extruder %s", new_extruder_id)
  399. return
  400. extruder_definition = extruder_definitions[0]
  401. unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id) if create_new_ids else machine.getName() + " " + new_extruder_id
  402. extruder_stack = ExtruderStack.ExtruderStack(unique_name)
  403. extruder_stack.setName(extruder_definition.getName())
  404. extruder_stack.setDefinition(extruder_definition)
  405. extruder_stack.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
  406. # create a new definition_changes container for the extruder stack
  407. definition_changes_id = self.uniqueName(extruder_stack.getId() + "_settings") if create_new_ids else extruder_stack.getId() + "_settings"
  408. definition_changes_name = definition_changes_id
  409. definition_changes = InstanceContainer(definition_changes_id, parent = application)
  410. definition_changes.setName(definition_changes_name)
  411. definition_changes.setMetaDataEntry("setting_version", application.SettingVersion)
  412. definition_changes.setMetaDataEntry("type", "definition_changes")
  413. definition_changes.setMetaDataEntry("definition", extruder_definition.getId())
  414. # move definition_changes settings if exist
  415. for setting_key in definition_changes.getAllKeys():
  416. if machine.definition.getProperty(setting_key, "settable_per_extruder"):
  417. setting_value = machine.definitionChanges.getProperty(setting_key, "value")
  418. if setting_value is not None:
  419. # move it to the extruder stack's definition_changes
  420. setting_definition = machine.getSettingDefinition(setting_key)
  421. new_instance = SettingInstance(setting_definition, definition_changes)
  422. new_instance.setProperty("value", setting_value)
  423. new_instance.resetState() # Ensure that the state is not seen as a user state.
  424. definition_changes.addInstance(new_instance)
  425. definition_changes.setDirty(True)
  426. machine.definitionChanges.removeInstance(setting_key, postpone_emit = True)
  427. self.addContainer(definition_changes)
  428. extruder_stack.setDefinitionChanges(definition_changes)
  429. # create empty user changes container otherwise
  430. user_container_id = self.uniqueName(extruder_stack.getId() + "_user") if create_new_ids else extruder_stack.getId() + "_user"
  431. user_container_name = user_container_id
  432. user_container = InstanceContainer(user_container_id, parent = application)
  433. user_container.setName(user_container_name)
  434. user_container.setMetaDataEntry("type", "user")
  435. user_container.setMetaDataEntry("machine", machine.getId())
  436. user_container.setMetaDataEntry("setting_version", application.SettingVersion)
  437. user_container.setDefinition(machine.definition.getId())
  438. user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
  439. if machine.userChanges:
  440. # for the newly created extruder stack, we need to move all "per-extruder" settings to the user changes
  441. # container to the extruder stack.
  442. for user_setting_key in machine.userChanges.getAllKeys():
  443. settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder")
  444. if settable_per_extruder:
  445. setting_value = machine.getProperty(user_setting_key, "value")
  446. setting_definition = machine.getSettingDefinition(user_setting_key)
  447. new_instance = SettingInstance(setting_definition, definition_changes)
  448. new_instance.setProperty("value", setting_value)
  449. new_instance.resetState() # Ensure that the state is not seen as a user state.
  450. user_container.addInstance(new_instance)
  451. user_container.setDirty(True)
  452. machine.userChanges.removeInstance(user_setting_key, postpone_emit = True)
  453. self.addContainer(user_container)
  454. extruder_stack.setUserChanges(user_container)
  455. empty_variant = application.empty_variant_container
  456. empty_material = application.empty_material_container
  457. empty_quality = application.empty_quality_container
  458. if machine.variant.getId() not in ("empty", "empty_variant"):
  459. variant = machine.variant
  460. else:
  461. variant = empty_variant
  462. extruder_stack.variant = variant
  463. if machine.material.getId() not in ("empty", "empty_material"):
  464. material = machine.material
  465. else:
  466. material = empty_material
  467. extruder_stack.material = material
  468. if machine.quality.getId() not in ("empty", "empty_quality"):
  469. quality = machine.quality
  470. else:
  471. quality = empty_quality
  472. extruder_stack.quality = quality
  473. machine_quality_changes = machine.qualityChanges
  474. if new_global_quality_changes is not None:
  475. machine_quality_changes = new_global_quality_changes
  476. if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"):
  477. extruder_quality_changes_container = self.findInstanceContainers(name = machine_quality_changes.getName(), extruder = extruder_id)
  478. if extruder_quality_changes_container:
  479. extruder_quality_changes_container = extruder_quality_changes_container[0]
  480. quality_changes_id = extruder_quality_changes_container.getId()
  481. extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0]
  482. else:
  483. # Some extruder quality_changes containers can be created at runtime as files in the qualities
  484. # folder. Those files won't be loaded in the registry immediately. So we also need to search
  485. # the folder to see if the quality_changes exists.
  486. extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName())
  487. if extruder_quality_changes_container:
  488. quality_changes_id = extruder_quality_changes_container.getId()
  489. extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
  490. extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0]
  491. else:
  492. # if we still cannot find a quality changes container for the extruder, create a new one
  493. container_name = machine_quality_changes.getName()
  494. container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name)
  495. extruder_quality_changes_container = InstanceContainer(container_id, parent = application)
  496. extruder_quality_changes_container.setName(container_name)
  497. extruder_quality_changes_container.setMetaDataEntry("type", "quality_changes")
  498. extruder_quality_changes_container.setMetaDataEntry("setting_version", application.SettingVersion)
  499. extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
  500. extruder_quality_changes_container.setMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type"))
  501. extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId())
  502. self.addContainer(extruder_quality_changes_container)
  503. extruder_stack.qualityChanges = extruder_quality_changes_container
  504. if not extruder_quality_changes_container:
  505. Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]",
  506. machine_quality_changes.getName(), extruder_stack.getId())
  507. else:
  508. # move all per-extruder settings to the extruder's quality changes
  509. for qc_setting_key in machine_quality_changes.getAllKeys():
  510. settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
  511. if settable_per_extruder:
  512. setting_value = machine_quality_changes.getProperty(qc_setting_key, "value")
  513. setting_definition = machine.getSettingDefinition(qc_setting_key)
  514. new_instance = SettingInstance(setting_definition, definition_changes)
  515. new_instance.setProperty("value", setting_value)
  516. new_instance.resetState() # Ensure that the state is not seen as a user state.
  517. extruder_quality_changes_container.addInstance(new_instance)
  518. extruder_quality_changes_container.setDirty(True)
  519. machine_quality_changes.removeInstance(qc_setting_key, postpone_emit=True)
  520. else:
  521. extruder_stack.qualityChanges = self.findInstanceContainers(id = "empty_quality_changes")[0]
  522. self.addContainer(extruder_stack)
  523. # Also need to fix the other qualities that are suitable for this machine. Those quality changes may still have
  524. # per-extruder settings in the container for the machine instead of the extruder.
  525. if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"):
  526. quality_changes_machine_definition_id = machine_quality_changes.getDefinition().getId()
  527. else:
  528. whole_machine_definition = machine.definition
  529. machine_entry = machine.definition.getMetaDataEntry("machine")
  530. if machine_entry is not None:
  531. container_registry = ContainerRegistry.getInstance()
  532. whole_machine_definition = container_registry.findDefinitionContainers(id = machine_entry)[0]
  533. quality_changes_machine_definition_id = "fdmprinter"
  534. if whole_machine_definition.getMetaDataEntry("has_machine_quality"):
  535. quality_changes_machine_definition_id = machine.definition.getMetaDataEntry("quality_definition",
  536. whole_machine_definition.getId())
  537. qcs = self.findInstanceContainers(type = "quality_changes", definition = quality_changes_machine_definition_id)
  538. qc_groups = {} # map of qc names -> qc containers
  539. for qc in qcs:
  540. qc_name = qc.getName()
  541. if qc_name not in qc_groups:
  542. qc_groups[qc_name] = []
  543. qc_groups[qc_name].append(qc)
  544. # try to find from the quality changes cura directory too
  545. quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName())
  546. if quality_changes_container:
  547. qc_groups[qc_name].append(quality_changes_container)
  548. for qc_name, qc_list in qc_groups.items():
  549. qc_dict = {"global": None, "extruders": []}
  550. for qc in qc_list:
  551. extruder_position = qc.getMetaDataEntry("position")
  552. if extruder_position is not None:
  553. qc_dict["extruders"].append(qc)
  554. else:
  555. qc_dict["global"] = qc
  556. if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1:
  557. # move per-extruder settings
  558. for qc_setting_key in qc_dict["global"].getAllKeys():
  559. settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
  560. if settable_per_extruder:
  561. setting_value = qc_dict["global"].getProperty(qc_setting_key, "value")
  562. setting_definition = machine.getSettingDefinition(qc_setting_key)
  563. new_instance = SettingInstance(setting_definition, definition_changes)
  564. new_instance.setProperty("value", setting_value)
  565. new_instance.resetState() # Ensure that the state is not seen as a user state.
  566. qc_dict["extruders"][0].addInstance(new_instance)
  567. qc_dict["extruders"][0].setDirty(True)
  568. qc_dict["global"].removeInstance(qc_setting_key, postpone_emit=True)
  569. # Set next stack at the end
  570. extruder_stack.setNextStack(machine)
  571. return extruder_stack
  572. def _findQualityChangesContainerInCuraFolder(self, name):
  573. quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer)
  574. instance_container = None
  575. for item in os.listdir(quality_changes_dir):
  576. file_path = os.path.join(quality_changes_dir, item)
  577. if not os.path.isfile(file_path):
  578. continue
  579. parser = configparser.ConfigParser(interpolation=None)
  580. try:
  581. parser.read([file_path])
  582. except:
  583. # skip, it is not a valid stack file
  584. continue
  585. if not parser.has_option("general", "name"):
  586. continue
  587. if parser["general"]["name"] == name:
  588. # load the container
  589. container_id = os.path.basename(file_path).replace(".inst.cfg", "")
  590. if self.findInstanceContainers(id = container_id):
  591. # this container is already in the registry, skip it
  592. continue
  593. instance_container = InstanceContainer(container_id)
  594. with open(file_path, "r", encoding = "utf-8") as f:
  595. serialized = f.read()
  596. try:
  597. instance_container.deserialize(serialized, file_path)
  598. except ContainerFormatError:
  599. Logger.logException("e", "Unable to deserialize InstanceContainer %s", file_path)
  600. continue
  601. self.addContainer(instance_container)
  602. break
  603. return instance_container
  604. # Fix the extruders that were upgraded to ExtruderStack instances during addContainer.
  605. # The stacks are now responsible for setting the next stack on deserialize. However,
  606. # due to problems with loading order, some stacks may not have the proper next stack
  607. # set after upgrading, because the proper global stack was not yet loaded. This method
  608. # makes sure those extruders also get the right stack set.
  609. def _connectUpgradedExtruderStacksToMachines(self):
  610. extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack)
  611. for extruder_stack in extruder_stacks:
  612. if extruder_stack.getNextStack():
  613. # Has the right next stack, so ignore it.
  614. continue
  615. machines = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack.getMetaDataEntry("machine", ""))
  616. if machines:
  617. extruder_stack.setNextStack(machines[0])
  618. else:
  619. Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId())
  620. #Override just for the type.
  621. @classmethod
  622. @override(ContainerRegistry)
  623. def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry":
  624. return cast(CuraContainerRegistry, super().getInstance(*args, **kwargs))