CuraContainerRegistry.py 48 KB

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