CuraContainerRegistry.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. # Copyright (c) 2017 Ultimaker B.V.
  2. # Cura is released under the terms of the AGPLv3 or higher.
  3. import os
  4. import os.path
  5. import re
  6. from PyQt5.QtWidgets import QMessageBox
  7. from UM.Decorators import override
  8. from UM.Settings.ContainerRegistry import ContainerRegistry
  9. from UM.Settings.ContainerStack import ContainerStack
  10. from UM.Settings.InstanceContainer import InstanceContainer
  11. from UM.Application import Application
  12. from UM.Logger import Logger
  13. from UM.Message import Message
  14. from UM.Platform import Platform
  15. from UM.PluginRegistry import PluginRegistry #For getting the possible profile writers to write with.
  16. from UM.Util import parseBool
  17. from . import ExtruderStack
  18. from . import GlobalStack
  19. from .ContainerManager import ContainerManager
  20. from .ExtruderManager import ExtruderManager
  21. from cura.CuraApplication import CuraApplication
  22. from UM.i18n import i18nCatalog
  23. catalog = i18nCatalog("cura")
  24. class CuraContainerRegistry(ContainerRegistry):
  25. def __init__(self, *args, **kwargs):
  26. super().__init__(*args, **kwargs)
  27. ## Overridden from ContainerRegistry
  28. #
  29. # Adds a container to the registry.
  30. #
  31. # This will also try to convert a ContainerStack to either Extruder or
  32. # Global stack based on metadata information.
  33. @override(ContainerRegistry)
  34. def addContainer(self, container):
  35. # Note: Intentional check with type() because we want to ignore subclasses
  36. if type(container) == ContainerStack:
  37. container = self._convertContainerStack(container)
  38. if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
  39. #Check against setting version of the definition.
  40. required_setting_version = CuraApplication.SettingVersion
  41. actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
  42. if required_setting_version != actual_setting_version:
  43. 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))
  44. return #Don't add.
  45. super().addContainer(container)
  46. ## Create a name that is not empty and unique
  47. # \param container_type \type{string} Type of the container (machine, quality, ...)
  48. # \param current_name \type{} Current name of the container, which may be an acceptable option
  49. # \param new_name \type{string} Base name, which may not be unique
  50. # \param fallback_name \type{string} Name to use when (stripped) new_name is empty
  51. # \return \type{string} Name that is unique for the specified type and name/id
  52. def createUniqueName(self, container_type, current_name, new_name, fallback_name):
  53. new_name = new_name.strip()
  54. num_check = re.compile("(.*?)\s*#\d+$").match(new_name)
  55. if num_check:
  56. new_name = num_check.group(1)
  57. if new_name == "":
  58. new_name = fallback_name
  59. unique_name = new_name
  60. i = 1
  61. # In case we are renaming, the current name of the container is also a valid end-result
  62. while self._containerExists(container_type, unique_name) and unique_name != current_name:
  63. i += 1
  64. unique_name = "%s #%d" % (new_name, i)
  65. return unique_name
  66. ## Check if a container with of a certain type and a certain name or id exists
  67. # Both the id and the name are checked, because they may not be the same and it is better if they are both unique
  68. # \param container_type \type{string} Type of the container (machine, quality, ...)
  69. # \param container_name \type{string} Name to check
  70. def _containerExists(self, container_type, container_name):
  71. container_class = ContainerStack if container_type == "machine" else InstanceContainer
  72. return self.findContainers(container_class, id = container_name, type = container_type, ignore_case = True) or \
  73. self.findContainers(container_class, name = container_name, type = container_type)
  74. ## Exports an profile to a file
  75. #
  76. # \param instance_ids \type{list} the IDs of the profiles to export.
  77. # \param file_name \type{str} the full path and filename to export to.
  78. # \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
  79. def exportProfile(self, instance_ids, file_name, file_type):
  80. # Parse the fileType to deduce what plugin can save the file format.
  81. # fileType has the format "<description> (*.<extension>)"
  82. split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
  83. if split < 0: # Not found. Invalid format.
  84. Logger.log("e", "Invalid file format identifier %s", file_type)
  85. return
  86. description = file_type[:split]
  87. extension = file_type[split + 4:-1] # Leave out the " (*." and ")".
  88. if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any.
  89. file_name += "." + extension
  90. # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself.
  91. if not Platform.isWindows():
  92. if os.path.exists(file_name):
  93. result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
  94. catalog.i18nc("@label", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
  95. if result == QMessageBox.No:
  96. return
  97. found_containers = []
  98. extruder_positions = []
  99. for instance_id in instance_ids:
  100. containers = ContainerRegistry.getInstance().findInstanceContainers(id=instance_id)
  101. if containers:
  102. found_containers.append(containers[0])
  103. # Determine the position of the extruder of this container
  104. extruder_id = containers[0].getMetaDataEntry("extruder", "")
  105. if extruder_id == "":
  106. # Global stack
  107. extruder_positions.append(-1)
  108. else:
  109. extruder_containers = ContainerRegistry.getInstance().findDefinitionContainers(id=extruder_id)
  110. if extruder_containers:
  111. extruder_positions.append(int(extruder_containers[0].getMetaDataEntry("position", 0)))
  112. else:
  113. extruder_positions.append(0)
  114. # Ensure the profiles are always exported in order (global, extruder 0, extruder 1, ...)
  115. found_containers = [containers for (positions, containers) in sorted(zip(extruder_positions, found_containers))]
  116. profile_writer = self._findProfileWriter(extension, description)
  117. try:
  118. success = profile_writer.write(file_name, found_containers)
  119. except Exception as e:
  120. Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
  121. m = Message(catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", file_name, str(e)), lifetime = 0)
  122. m.show()
  123. return
  124. if not success:
  125. Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name)
  126. m = Message(catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name), lifetime = 0)
  127. m.show()
  128. return
  129. m = Message(catalog.i18nc("@info:status", "Exported profile to <filename>{0}</filename>", file_name))
  130. m.show()
  131. ## Gets the plugin object matching the criteria
  132. # \param extension
  133. # \param description
  134. # \return The plugin object matching the given extension and description.
  135. def _findProfileWriter(self, extension, description):
  136. plugin_registry = PluginRegistry.getInstance()
  137. for plugin_id, meta_data in self._getIOPlugins("profile_writer"):
  138. for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write.
  139. supported_extension = supported_type.get("extension", None)
  140. if supported_extension == extension: # This plugin supports a file type with the same extension.
  141. supported_description = supported_type.get("description", None)
  142. if supported_description == description: # The description is also identical. Assume it's the same file type.
  143. return plugin_registry.getPluginObject(plugin_id)
  144. return None
  145. ## Imports a profile from a file
  146. #
  147. # \param file_name \type{str} the full path and filename of the profile to import
  148. # \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key
  149. # containing a message for the user
  150. def importProfile(self, file_name):
  151. Logger.log("d", "Attempting to import profile %s", file_name)
  152. if not file_name:
  153. return { "status": "error", "message": catalog.i18nc("@info:status", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "Invalid path")}
  154. plugin_registry = PluginRegistry.getInstance()
  155. extension = file_name.split(".")[-1]
  156. global_container_stack = Application.getInstance().getGlobalContainerStack()
  157. if not global_container_stack:
  158. return
  159. machine_extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()))
  160. machine_extruders.sort(key = lambda k: k.getMetaDataEntry("position"))
  161. for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
  162. if meta_data["profile_reader"][0]["extension"] != extension:
  163. continue
  164. profile_reader = plugin_registry.getPluginObject(plugin_id)
  165. try:
  166. profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
  167. except Exception as e:
  168. # 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.
  169. Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name,profile_reader.getPluginId(), str(e))
  170. return { "status": "error", "message": catalog.i18nc("@info:status", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, str(e))}
  171. if profile_or_list: # Success!
  172. name_seed = os.path.splitext(os.path.basename(file_name))[0]
  173. new_name = self.uniqueName(name_seed)
  174. if type(profile_or_list) is not list:
  175. profile = profile_or_list
  176. self._configureProfile(profile, name_seed, new_name)
  177. return { "status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName()) }
  178. else:
  179. profile_index = -1
  180. global_profile = None
  181. for profile in profile_or_list:
  182. if profile_index >= 0:
  183. if len(machine_extruders) > profile_index:
  184. extruder_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_extruders[profile_index].getBottom())
  185. # Ensure the extruder profiles get non-conflicting names
  186. # NB: these are not user-facing
  187. if "extruder" in profile.getMetaData():
  188. profile.setMetaDataEntry("extruder", extruder_id)
  189. else:
  190. profile.addMetaDataEntry("extruder", extruder_id)
  191. profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
  192. elif profile_index == 0:
  193. # Importing a multiextrusion profile into a single extrusion machine; merge 1st extruder profile into global profile
  194. profile._id = self.uniqueName("temporary_profile")
  195. self.addContainer(profile)
  196. ContainerManager.getInstance().mergeContainers(global_profile.getId(), profile.getId())
  197. self.removeContainer(profile.getId())
  198. break
  199. else:
  200. # The imported composite profile has a profile for an extruder that this machine does not have. Ignore this extruder-profile
  201. break
  202. else:
  203. global_profile = profile
  204. profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
  205. self._configureProfile(profile, profile_id, new_name)
  206. profile_index += 1
  207. return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
  208. # If it hasn't returned by now, none of the plugins loaded the profile successfully.
  209. return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
  210. @override(ContainerRegistry)
  211. def load(self):
  212. super().load()
  213. self._fixupExtruders()
  214. def _configureProfile(self, profile, id_seed, new_name):
  215. profile.setReadOnly(False)
  216. profile.setDirty(True) # Ensure the profiles are correctly saved
  217. new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile"))
  218. profile._id = new_id
  219. profile.setName(new_name)
  220. if "type" in profile.getMetaData():
  221. profile.setMetaDataEntry("type", "quality_changes")
  222. else:
  223. profile.addMetaDataEntry("type", "quality_changes")
  224. if self._machineHasOwnQualities():
  225. profile.setDefinition(self._activeQualityDefinition())
  226. if self._machineHasOwnMaterials():
  227. profile.addMetaDataEntry("material", self._activeMaterialId())
  228. else:
  229. profile.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0])
  230. ContainerRegistry.getInstance().addContainer(profile)
  231. ## Gets a list of profile writer plugins
  232. # \return List of tuples of (plugin_id, meta_data).
  233. def _getIOPlugins(self, io_type):
  234. plugin_registry = PluginRegistry.getInstance()
  235. active_plugin_ids = plugin_registry.getActivePlugins()
  236. result = []
  237. for plugin_id in active_plugin_ids:
  238. meta_data = plugin_registry.getMetaData(plugin_id)
  239. if io_type in meta_data:
  240. result.append( (plugin_id, meta_data) )
  241. return result
  242. ## Get the definition to use to select quality profiles for the active machine
  243. # \return the active quality definition object or None if there is no quality definition
  244. def _activeQualityDefinition(self):
  245. global_container_stack = Application.getInstance().getGlobalContainerStack()
  246. if global_container_stack:
  247. definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(global_container_stack.getBottom())
  248. definition = self.findDefinitionContainers(id=definition_id)[0]
  249. if definition:
  250. return definition
  251. return None
  252. ## Returns true if the current machine requires its own materials
  253. # \return True if the current machine requires its own materials
  254. def _machineHasOwnMaterials(self):
  255. global_container_stack = Application.getInstance().getGlobalContainerStack()
  256. if global_container_stack:
  257. return global_container_stack.getMetaDataEntry("has_materials", False)
  258. return False
  259. ## Gets the ID of the active material
  260. # \return the ID of the active material or the empty string
  261. def _activeMaterialId(self):
  262. global_container_stack = Application.getInstance().getGlobalContainerStack()
  263. if global_container_stack:
  264. material = global_container_stack.findContainer({"type": "material"})
  265. if material:
  266. return material.getId()
  267. return ""
  268. ## Returns true if the current machien requires its own quality profiles
  269. # \return true if the current machien requires its own quality profiles
  270. def _machineHasOwnQualities(self):
  271. global_container_stack = Application.getInstance().getGlobalContainerStack()
  272. if global_container_stack:
  273. return parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", False))
  274. return False
  275. ## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
  276. def _convertContainerStack(self, container):
  277. assert type(container) == ContainerStack
  278. container_type = container.getMetaDataEntry("type")
  279. if container_type not in ("extruder_train", "machine"):
  280. # It is not an extruder or machine, so do nothing with the stack
  281. return container
  282. Logger.log("d", "Converting ContainerStack {stack} to {type}", stack = container.getId(), type = container_type)
  283. new_stack = None
  284. if container_type == "extruder_train":
  285. new_stack = ExtruderStack.ExtruderStack(container.getId())
  286. else:
  287. new_stack = GlobalStack.GlobalStack(container.getId())
  288. container_contents = container.serialize()
  289. new_stack.deserialize(container_contents)
  290. # Delete the old configuration file so we do not get double stacks
  291. if os.path.isfile(container.getPath()):
  292. os.remove(container.getPath())
  293. return new_stack
  294. # Fix the extruders that were upgraded to ExtruderStack instances during addContainer.
  295. # The stacks are now responsible for setting the next stack on deserialize. However,
  296. # due to problems with loading order, some stacks may not have the proper next stack
  297. # set after upgrading, because the proper global stack was not yet loaded. This method
  298. # makes sure those extruders also get the right stack set.
  299. def _fixupExtruders(self):
  300. extruder_stacks = self.findContainers(ExtruderStack.ExtruderStack)
  301. for extruder_stack in extruder_stacks:
  302. if extruder_stack.getNextStack():
  303. # Has the right next stack, so ignore it.
  304. continue
  305. machines = ContainerRegistry.getInstance().findContainerStacks(id=extruder_stack.getMetaDataEntry("machine", ""))
  306. if machines:
  307. extruder_stack.setNextStack(machines[0])
  308. else:
  309. Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId())