CuraContainerRegistry.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. # Copyright (c) 2017 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import os
  4. import os.path
  5. import re
  6. import configparser
  7. from typing import Optional
  8. from PyQt5.QtWidgets import QMessageBox
  9. from UM.Decorators import override
  10. from UM.Settings.ContainerRegistry import ContainerRegistry
  11. from UM.Settings.ContainerStack import ContainerStack
  12. from UM.Settings.InstanceContainer import InstanceContainer
  13. from UM.Application import Application
  14. from UM.Logger import Logger
  15. from UM.Message import Message
  16. from UM.Platform import Platform
  17. from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with.
  18. from UM.Util import parseBool
  19. from UM.Resources import Resources
  20. from . import ExtruderStack
  21. from . import GlobalStack
  22. from .ContainerManager import ContainerManager
  23. from .ExtruderManager import ExtruderManager
  24. from cura.CuraApplication import CuraApplication
  25. from UM.i18n import i18nCatalog
  26. catalog = i18nCatalog("cura")
  27. class CuraContainerRegistry(ContainerRegistry):
  28. def __init__(self, *args, **kwargs):
  29. super().__init__(*args, **kwargs)
  30. # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack
  31. # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack
  32. # is added, we check to see if an extruder stack needs to be added.
  33. self.containerAdded.connect(self._onContainerAdded)
  34. ## Overridden from ContainerRegistry
  35. #
  36. # Adds a container to the registry.
  37. #
  38. # This will also try to convert a ContainerStack to either Extruder or
  39. # Global stack based on metadata information.
  40. @override(ContainerRegistry)
  41. def addContainer(self, container):
  42. # Note: Intentional check with type() because we want to ignore subclasses
  43. if type(container) == ContainerStack:
  44. container = self._convertContainerStack(container)
  45. if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
  46. # Check against setting version of the definition.
  47. required_setting_version = CuraApplication.SettingVersion
  48. actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
  49. if required_setting_version != actual_setting_version:
  50. 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))
  51. return #Don't add.
  52. super().addContainer(container)
  53. ## Create a name that is not empty and unique
  54. # \param container_type \type{string} Type of the container (machine, quality, ...)
  55. # \param current_name \type{} Current name of the container, which may be an acceptable option
  56. # \param new_name \type{string} Base name, which may not be unique
  57. # \param fallback_name \type{string} Name to use when (stripped) new_name is empty
  58. # \return \type{string} Name that is unique for the specified type and name/id
  59. def createUniqueName(self, container_type, current_name, new_name, fallback_name):
  60. new_name = new_name.strip()
  61. num_check = re.compile("(.*?)\s*#\d+$").match(new_name)
  62. if num_check:
  63. new_name = num_check.group(1)
  64. if new_name == "":
  65. new_name = fallback_name
  66. unique_name = new_name
  67. i = 1
  68. # In case we are renaming, the current name of the container is also a valid end-result
  69. while self._containerExists(container_type, unique_name) and unique_name != current_name:
  70. i += 1
  71. unique_name = "%s #%d" % (new_name, i)
  72. return unique_name
  73. ## Check if a container with of a certain type and a certain name or id exists
  74. # Both the id and the name are checked, because they may not be the same and it is better if they are both unique
  75. # \param container_type \type{string} Type of the container (machine, quality, ...)
  76. # \param container_name \type{string} Name to check
  77. def _containerExists(self, container_type, container_name):
  78. container_class = ContainerStack if container_type == "machine" else InstanceContainer
  79. return self.findContainersMetadata(id = container_name, type = container_type, ignore_case = True) or \
  80. self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type)
  81. ## Exports an profile to a file
  82. #
  83. # \param instance_ids \type{list} the IDs of the profiles to export.
  84. # \param file_name \type{str} the full path and filename to export to.
  85. # \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
  86. def exportProfile(self, instance_ids, file_name, file_type):
  87. # Parse the fileType to deduce what plugin can save the file format.
  88. # fileType has the format "<description> (*.<extension>)"
  89. split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
  90. if split < 0: # Not found. Invalid format.
  91. Logger.log("e", "Invalid file format identifier %s", file_type)
  92. return
  93. description = file_type[:split]
  94. extension = file_type[split + 4:-1] # Leave out the " (*." and ")".
  95. if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any.
  96. file_name += "." + extension
  97. # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself.
  98. if not Platform.isWindows():
  99. if os.path.exists(file_name):
  100. result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
  101. 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))
  102. if result == QMessageBox.No:
  103. return
  104. found_containers = []
  105. extruder_positions = []
  106. for instance_id in instance_ids:
  107. containers = ContainerRegistry.getInstance().findInstanceContainers(id = instance_id)
  108. if containers:
  109. found_containers.append(containers[0])
  110. # Determine the position of the extruder of this container
  111. extruder_id = containers[0].getMetaDataEntry("extruder", "")
  112. if extruder_id == "":
  113. # Global stack
  114. extruder_positions.append(-1)
  115. else:
  116. extruder_containers = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = extruder_id)
  117. if extruder_containers:
  118. extruder_positions.append(int(extruder_containers[0].get("position", 0)))
  119. else:
  120. extruder_positions.append(0)
  121. # Ensure the profiles are always exported in order (global, extruder 0, extruder 1, ...)
  122. found_containers = [containers for (positions, containers) in sorted(zip(extruder_positions, found_containers))]
  123. profile_writer = self._findProfileWriter(extension, description)
  124. try:
  125. success = profile_writer.write(file_name, found_containers)
  126. except Exception as e:
  127. Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
  128. 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)),
  129. lifetime = 0,
  130. title = catalog.i18nc("@info:title", "Error"))
  131. m.show()
  132. return
  133. if not success:
  134. Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name)
  135. 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),
  136. lifetime = 0,
  137. title = catalog.i18nc("@info:title", "Error"))
  138. m.show()
  139. return
  140. m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Exported profile to <filename>{0}</filename>", file_name),
  141. title = catalog.i18nc("@info:title", "Export succeeded"))
  142. m.show()
  143. ## Gets the plugin object matching the criteria
  144. # \param extension
  145. # \param description
  146. # \return The plugin object matching the given extension and description.
  147. def _findProfileWriter(self, extension, description):
  148. plugin_registry = PluginRegistry.getInstance()
  149. for plugin_id, meta_data in self._getIOPlugins("profile_writer"):
  150. for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write.
  151. supported_extension = supported_type.get("extension", None)
  152. if supported_extension == extension: # This plugin supports a file type with the same extension.
  153. supported_description = supported_type.get("description", None)
  154. if supported_description == description: # The description is also identical. Assume it's the same file type.
  155. return plugin_registry.getPluginObject(plugin_id)
  156. return None
  157. ## Imports a profile from a file
  158. #
  159. # \param file_name \type{str} the full path and filename of the profile to import
  160. # \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key
  161. # containing a message for the user
  162. def importProfile(self, file_name):
  163. Logger.log("d", "Attempting to import profile %s", file_name)
  164. if not file_name:
  165. 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")}
  166. plugin_registry = PluginRegistry.getInstance()
  167. extension = file_name.split(".")[-1]
  168. global_container_stack = Application.getInstance().getGlobalContainerStack()
  169. if not global_container_stack:
  170. return
  171. machine_extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()))
  172. machine_extruders.sort(key = lambda k: k.getMetaDataEntry("position"))
  173. for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
  174. if meta_data["profile_reader"][0]["extension"] != extension:
  175. continue
  176. profile_reader = plugin_registry.getPluginObject(plugin_id)
  177. try:
  178. profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
  179. except Exception as e:
  180. # 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.
  181. Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name,profile_reader.getPluginId(), str(e))
  182. 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, str(e))}
  183. if profile_or_list:
  184. name_seed = os.path.splitext(os.path.basename(file_name))[0]
  185. new_name = self.uniqueName(name_seed)
  186. # Ensure it is always a list of profiles
  187. if type(profile_or_list) is not list:
  188. profile_or_list = [profile_or_list]
  189. if len(profile_or_list) == 1:
  190. # If there is only 1 stack file it means we're loading a legacy (pre-3.1) .curaprofile.
  191. # In that case we find the per-extruder settings and put those in a new quality_changes container
  192. # so that it is compatible with the new stack setup.
  193. profile = profile_or_list[0]
  194. extruder_stack_quality_changes_container = ContainerManager.getInstance().duplicateContainerInstance(profile)
  195. extruder_stack_quality_changes_container.addMetaDataEntry("extruder", "fdmextruder")
  196. for quality_changes_setting_key in extruder_stack_quality_changes_container.getAllKeys():
  197. settable_per_extruder = extruder_stack_quality_changes_container.getProperty(quality_changes_setting_key, "settable_per_extruder")
  198. if settable_per_extruder:
  199. profile.removeInstance(quality_changes_setting_key, postpone_emit = True)
  200. else:
  201. extruder_stack_quality_changes_container.removeInstance(quality_changes_setting_key, postpone_emit = True)
  202. # We add the new container to the profile list so things like extruder positions are taken care of
  203. # in the next code segment.
  204. profile_or_list.append(extruder_stack_quality_changes_container)
  205. # Import all profiles
  206. for profile_index, profile in enumerate(profile_or_list):
  207. if profile_index == 0:
  208. # This is assumed to be the global profile
  209. profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
  210. elif len(machine_extruders) > profile_index:
  211. # This is assumed to be an extruder profile
  212. extruder_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_extruders[profile_index - 1].getBottom())
  213. if not profile.getMetaDataEntry("extruder"):
  214. profile.addMetaDataEntry("extruder", extruder_id)
  215. else:
  216. profile.setMetaDataEntry("extruder", extruder_id)
  217. profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
  218. result = self._configureProfile(profile, profile_id, new_name)
  219. if result is not None:
  220. return {"status": "error", "message": catalog.i18nc(
  221. "@info:status Don't translate the XML tags <filename> or <message>!",
  222. "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>",
  223. file_name, result)}
  224. return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
  225. # If it hasn't returned by now, none of the plugins loaded the profile successfully.
  226. return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
  227. @override(ContainerRegistry)
  228. def load(self):
  229. super().load()
  230. self._registerSingleExtrusionMachinesExtruderStacks()
  231. self._connectUpgradedExtruderStacksToMachines()
  232. ## Update an imported profile to match the current machine configuration.
  233. #
  234. # \param profile The profile to configure.
  235. # \param id_seed The base ID for the profile. May be changed so it does not conflict with existing containers.
  236. # \param new_name The new name for the profile.
  237. #
  238. # \return None if configuring was successful or an error message if an error occurred.
  239. def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str) -> Optional[str]:
  240. profile.setDirty(True) # Ensure the profiles are correctly saved
  241. new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile"))
  242. profile._id = new_id
  243. profile.setName(new_name)
  244. if "type" in profile.getMetaData():
  245. profile.setMetaDataEntry("type", "quality_changes")
  246. else:
  247. profile.addMetaDataEntry("type", "quality_changes")
  248. quality_type = profile.getMetaDataEntry("quality_type")
  249. if not quality_type:
  250. return catalog.i18nc("@info:status", "Profile is missing a quality type.")
  251. quality_type_criteria = {"quality_type": quality_type}
  252. if self._machineHasOwnQualities():
  253. profile.setDefinition(self._activeQualityDefinition().getId())
  254. if self._machineHasOwnMaterials():
  255. active_material_id = self._activeMaterialId()
  256. if active_material_id and active_material_id != "empty": # only update if there is an active material
  257. profile.addMetaDataEntry("material", active_material_id)
  258. quality_type_criteria["material"] = active_material_id
  259. quality_type_criteria["definition"] = profile.getDefinition().getId()
  260. else:
  261. profile.setDefinition(fdmprinter)
  262. quality_type_criteria["definition"] = "fdmprinter"
  263. machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
  264. del quality_type_criteria["definition"]
  265. # materials = None
  266. if "material" in quality_type_criteria:
  267. # materials = ContainerRegistry.getInstance().findInstanceContainers(id = quality_type_criteria["material"])
  268. del quality_type_criteria["material"]
  269. # Do not filter quality containers here with materials because we are trying to import a profile, so it should
  270. # NOT be restricted by the active materials on the current machine.
  271. materials = None
  272. # Check to make sure the imported profile actually makes sense in context of the current configuration.
  273. # This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as
  274. # successfully imported but then fail to show up.
  275. from cura.QualityManager import QualityManager
  276. qualities = QualityManager.getInstance()._getFilteredContainersForStack(machine_definition, materials, **quality_type_criteria)
  277. if not qualities:
  278. return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
  279. ContainerRegistry.getInstance().addContainer(profile)
  280. return None
  281. ## Gets a list of profile writer plugins
  282. # \return List of tuples of (plugin_id, meta_data).
  283. def _getIOPlugins(self, io_type):
  284. plugin_registry = PluginRegistry.getInstance()
  285. active_plugin_ids = plugin_registry.getActivePlugins()
  286. result = []
  287. for plugin_id in active_plugin_ids:
  288. meta_data = plugin_registry.getMetaData(plugin_id)
  289. if io_type in meta_data:
  290. result.append( (plugin_id, meta_data) )
  291. return result
  292. ## Get the definition to use to select quality profiles for the active machine
  293. # \return the active quality definition object or None if there is no quality definition
  294. def _activeQualityDefinition(self):
  295. global_container_stack = Application.getInstance().getGlobalContainerStack()
  296. if global_container_stack:
  297. definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(global_container_stack.getBottom())
  298. definition = self.findDefinitionContainers(id = definition_id)[0]
  299. if definition:
  300. return definition
  301. return None
  302. ## Returns true if the current machine requires its own materials
  303. # \return True if the current machine requires its own materials
  304. def _machineHasOwnMaterials(self):
  305. global_container_stack = Application.getInstance().getGlobalContainerStack()
  306. if global_container_stack:
  307. return global_container_stack.getMetaDataEntry("has_materials", False)
  308. return False
  309. ## Gets the ID of the active material
  310. # \return the ID of the active material or the empty string
  311. def _activeMaterialId(self):
  312. global_container_stack = Application.getInstance().getGlobalContainerStack()
  313. if global_container_stack and global_container_stack.material:
  314. return global_container_stack.material.getId()
  315. return ""
  316. ## Returns true if the current machine requires its own quality profiles
  317. # \return true if the current machine requires its own quality profiles
  318. def _machineHasOwnQualities(self):
  319. global_container_stack = Application.getInstance().getGlobalContainerStack()
  320. if global_container_stack:
  321. return parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", False))
  322. return False
  323. ## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
  324. def _convertContainerStack(self, container):
  325. assert type(container) == ContainerStack
  326. container_type = container.getMetaDataEntry("type")
  327. if container_type not in ("extruder_train", "machine"):
  328. # It is not an extruder or machine, so do nothing with the stack
  329. return container
  330. Logger.log("d", "Converting ContainerStack {stack} to {type}", stack = container.getId(), type = container_type)
  331. new_stack = None
  332. if container_type == "extruder_train":
  333. new_stack = ExtruderStack.ExtruderStack(container.getId())
  334. else:
  335. new_stack = GlobalStack.GlobalStack(container.getId())
  336. container_contents = container.serialize()
  337. new_stack.deserialize(container_contents)
  338. # Delete the old configuration file so we do not get double stacks
  339. if os.path.isfile(container.getPath()):
  340. os.remove(container.getPath())
  341. return new_stack
  342. def _registerSingleExtrusionMachinesExtruderStacks(self):
  343. machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"})
  344. for machine in machines:
  345. extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId())
  346. if not extruder_stacks:
  347. self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder")
  348. def _onContainerAdded(self, container):
  349. # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack
  350. # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack
  351. # is added, we check to see if an extruder stack needs to be added.
  352. if not isinstance(container, ContainerStack) or container.getMetaDataEntry("type") != "machine":
  353. return
  354. extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = container.getId())
  355. if not extruder_stacks:
  356. self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder")
  357. def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id):
  358. new_extruder_id = extruder_id
  359. extruder_definitions = self.findDefinitionContainers(id = new_extruder_id)
  360. if not extruder_definitions:
  361. Logger.log("w", "Could not find definition containers for extruder %s", new_extruder_id)
  362. return
  363. extruder_definition = extruder_definitions[0]
  364. unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id)
  365. extruder_stack = ExtruderStack.ExtruderStack(unique_name)
  366. extruder_stack.setName(extruder_definition.getName())
  367. extruder_stack.setDefinition(extruder_definition)
  368. extruder_stack.addMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
  369. # create empty user changes container otherwise
  370. user_container = InstanceContainer(extruder_stack.id + "_user")
  371. user_container.addMetaDataEntry("type", "user")
  372. user_container.addMetaDataEntry("machine", extruder_stack.getId())
  373. from cura.CuraApplication import CuraApplication
  374. user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
  375. user_container.setDefinition(machine.definition)
  376. if machine.userChanges:
  377. # for the newly created extruder stack, we need to move all "per-extruder" settings to the user changes
  378. # container to the extruder stack.
  379. for user_setting_key in machine.userChanges.getAllKeys():
  380. settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder")
  381. if settable_per_extruder:
  382. user_container.addInstance(machine.userChanges.getInstance(user_setting_key))
  383. machine.userChanges.removeInstance(user_setting_key, postpone_emit = True)
  384. self.addContainer(user_container)
  385. extruder_stack.setUserChanges(user_container)
  386. variant_id = "default"
  387. if machine.variant.getId() not in ("empty", "empty_variant"):
  388. variant_id = machine.variant.getId()
  389. else:
  390. variant_id = "empty_variant"
  391. extruder_stack.setVariantById(variant_id)
  392. material_id = "default"
  393. if machine.material.getId() not in ("empty", "empty_material"):
  394. material_id = machine.material.getId()
  395. else:
  396. material_id = "empty_material"
  397. extruder_stack.setMaterialById(material_id)
  398. quality_id = "default"
  399. if machine.quality.getId() not in ("empty", "empty_quality"):
  400. quality_id = machine.quality.getId()
  401. else:
  402. quality_id = "empty_quality"
  403. extruder_stack.setQualityById(quality_id)
  404. if machine.qualityChanges.getId() not in ("empty", "empty_quality_changes"):
  405. extruder_quality_changes_container = self.findInstanceContainers(name = machine.qualityChanges.getName(), extruder = extruder_id)
  406. if extruder_quality_changes_container:
  407. extruder_quality_changes_container = extruder_quality_changes_container[0]
  408. quality_changes_id = extruder_quality_changes_container.getId()
  409. extruder_stack.setQualityChangesById(quality_changes_id)
  410. else:
  411. # Some extruder quality_changes containers can be created at runtime as files in the qualities
  412. # folder. Those files won't be loaded in the registry immediately. So we also need to search
  413. # the folder to see if the quality_changes exists.
  414. extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine.qualityChanges.getName())
  415. if extruder_quality_changes_container:
  416. quality_changes_id = extruder_quality_changes_container.getId()
  417. extruder_stack.setQualityChangesById(quality_changes_id)
  418. if not extruder_quality_changes_container:
  419. Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]",
  420. machine.qualityChanges.getName(), extruder_stack.getId())
  421. else:
  422. extruder_stack.setQualityChangesById("empty_quality_changes")
  423. self.addContainer(extruder_stack)
  424. # Set next stack at the end
  425. extruder_stack.setNextStack(machine)
  426. return extruder_stack
  427. def _findQualityChangesContainerInCuraFolder(self, name):
  428. quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
  429. instance_container = None
  430. for item in os.listdir(quality_changes_dir):
  431. file_path = os.path.join(quality_changes_dir, item)
  432. if not os.path.isfile(file_path):
  433. continue
  434. parser = configparser.ConfigParser()
  435. try:
  436. parser.read([file_path])
  437. except:
  438. # skip, it is not a valid stack file
  439. continue
  440. if not parser.has_option("general", "name"):
  441. continue
  442. if parser["general"]["name"] == name:
  443. # load the container
  444. container_id = os.path.basename(file_path).replace(".inst.cfg", "")
  445. instance_container = InstanceContainer(container_id)
  446. with open(file_path, "r") as f:
  447. serialized = f.read()
  448. instance_container.deserialize(serialized, file_path)
  449. self.addContainer(instance_container)
  450. break
  451. return instance_container
  452. # Fix the extruders that were upgraded to ExtruderStack instances during addContainer.
  453. # The stacks are now responsible for setting the next stack on deserialize. However,
  454. # due to problems with loading order, some stacks may not have the proper next stack
  455. # set after upgrading, because the proper global stack was not yet loaded. This method
  456. # makes sure those extruders also get the right stack set.
  457. def _connectUpgradedExtruderStacksToMachines(self):
  458. extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack)
  459. for extruder_stack in extruder_stacks:
  460. if extruder_stack.getNextStack():
  461. # Has the right next stack, so ignore it.
  462. continue
  463. machines = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack.getMetaDataEntry("machine", ""))
  464. if machines:
  465. extruder_stack.setNextStack(machines[0])
  466. else:
  467. Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId())