CuraContainerRegistry.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. # Copyright (c) 2016 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.Settings.ContainerRegistry import ContainerRegistry
  8. from UM.Settings.ContainerStack import ContainerStack
  9. from UM.Settings.InstanceContainer import InstanceContainer
  10. from UM.Application import Application
  11. from UM.Logger import Logger
  12. from UM.Message import Message
  13. from UM.Platform import Platform
  14. from UM.PluginRegistry import PluginRegistry #For getting the possible profile writers to write with.
  15. from UM.Util import parseBool
  16. from UM.i18n import i18nCatalog
  17. catalog = i18nCatalog("cura")
  18. class CuraContainerRegistry(ContainerRegistry):
  19. def __init__(self, *args, **kwargs):
  20. super().__init__(*args, **kwargs)
  21. ## Create a name that is not empty and unique
  22. # \param container_type \type{string} Type of the container (machine, quality, ...)
  23. # \param current_name \type{} Current name of the container, which may be an acceptable option
  24. # \param new_name \type{string} Base name, which may not be unique
  25. # \param fallback_name \type{string} Name to use when (stripped) new_name is empty
  26. # \return \type{string} Name that is unique for the specified type and name/id
  27. def createUniqueName(self, container_type, current_name, new_name, fallback_name):
  28. new_name = new_name.strip()
  29. num_check = re.compile("(.*?)\s*#\d+$").match(new_name)
  30. if num_check:
  31. new_name = num_check.group(1)
  32. if new_name == "":
  33. new_name = fallback_name
  34. unique_name = new_name
  35. i = 1
  36. # In case we are renaming, the current name of the container is also a valid end-result
  37. while self._containerExists(container_type, unique_name) and unique_name != current_name:
  38. i += 1
  39. unique_name = "%s #%d" % (new_name, i)
  40. return unique_name
  41. ## Check if a container with of a certain type and a certain name or id exists
  42. # Both the id and the name are checked, because they may not be the same and it is better if they are both unique
  43. # \param container_type \type{string} Type of the container (machine, quality, ...)
  44. # \param container_name \type{string} Name to check
  45. def _containerExists(self, container_type, container_name):
  46. container_class = ContainerStack if container_type == "machine" else InstanceContainer
  47. return self.findContainers(container_class, id = container_name, type = container_type, ignore_case = True) or \
  48. self.findContainers(container_class, name = container_name, type = container_type)
  49. ## Exports an profile to a file
  50. #
  51. # \param instance_ids \type{list} the IDs of the profiles to export.
  52. # \param file_name \type{str} the full path and filename to export to.
  53. # \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
  54. def exportProfile(self, instance_ids, file_name, file_type):
  55. # Parse the fileType to deduce what plugin can save the file format.
  56. # fileType has the format "<description> (*.<extension>)"
  57. split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
  58. if split < 0: # Not found. Invalid format.
  59. Logger.log("e", "Invalid file format identifier %s", file_type)
  60. return
  61. description = file_type[:split]
  62. extension = file_type[split + 4:-1] # Leave out the " (*." and ")".
  63. if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any.
  64. file_name += "." + extension
  65. # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself.
  66. if not Platform.isWindows():
  67. if os.path.exists(file_name):
  68. result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
  69. catalog.i18nc("@label", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
  70. if result == QMessageBox.No:
  71. return
  72. found_containers = []
  73. for instance_id in instance_ids:
  74. containers = ContainerRegistry.getInstance().findInstanceContainers(id=instance_id)
  75. if containers:
  76. found_containers.append(containers[0])
  77. profile_writer = self._findProfileWriter(extension, description)
  78. try:
  79. success = profile_writer.write(file_name, found_containers)
  80. except Exception as e:
  81. Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
  82. m = Message(catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", file_name, str(e)), lifetime = 0)
  83. m.show()
  84. return
  85. if not success:
  86. Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name)
  87. m = Message(catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name), lifetime = 0)
  88. m.show()
  89. return
  90. m = Message(catalog.i18nc("@info:status", "Exported profile to <filename>{0}</filename>", file_name))
  91. m.show()
  92. ## Gets the plugin object matching the criteria
  93. # \param extension
  94. # \param description
  95. # \return The plugin object matching the given extension and description.
  96. def _findProfileWriter(self, extension, description):
  97. plugin_registry = PluginRegistry.getInstance()
  98. for plugin_id, meta_data in self._getIOPlugins("profile_writer"):
  99. for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write.
  100. supported_extension = supported_type.get("extension", None)
  101. if supported_extension == extension: # This plugin supports a file type with the same extension.
  102. supported_description = supported_type.get("description", None)
  103. if supported_description == description: # The description is also identical. Assume it's the same file type.
  104. return plugin_registry.getPluginObject(plugin_id)
  105. return None
  106. ## Imports a profile from a file
  107. #
  108. # \param file_name \type{str} the full path and filename of the profile to import
  109. # \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key
  110. # containing a message for the user
  111. def importProfile(self, file_name):
  112. if not file_name:
  113. return { "status": "error", "message": catalog.i18nc("@info:status", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "Invalid path")}
  114. plugin_registry = PluginRegistry.getInstance()
  115. container_registry = ContainerRegistry.getInstance()
  116. for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
  117. profile_reader = plugin_registry.getPluginObject(plugin_id)
  118. try:
  119. profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
  120. except Exception as e:
  121. #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.
  122. Logger.log("e", "Failed to import profile from %s: %s", file_name, str(e))
  123. return { "status": "error", "message": catalog.i18nc("@info:status", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, str(e))}
  124. if profile_or_list: # Success!
  125. name_seed = os.path.splitext(os.path.basename(file_name))[0]
  126. if type(profile_or_list) is not list:
  127. profile = profile_or_list
  128. self._configureProfile(profile, name_seed)
  129. return { "status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName()) }
  130. else:
  131. new_name = self.createUniqueName("quality_changes", "", name_seed, catalog.i18nc("@label", "Custom profile"))
  132. for profile in profile_or_list:
  133. profile.setDirty(True) # Ensure the profiles are correctly saved
  134. self._configureProfile(profile, name_seed)
  135. profile.setName(new_name)
  136. if len(profile_or_list) == 1:
  137. return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
  138. else:
  139. profile_names = ", ".join([profile.getName() for profile in profile_or_list])
  140. return { "status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profiles {0}", profile_names) }
  141. #If it hasn't returned by now, none of the plugins loaded the profile successfully.
  142. return { "status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type.", file_name)}
  143. def _configureProfile(self, profile, id_seed):
  144. profile.setReadOnly(False)
  145. new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile"))
  146. profile._id = new_id
  147. if self._machineHasOwnQualities():
  148. profile.setDefinition(self._activeDefinition())
  149. if self._machineHasOwnMaterials():
  150. profile.addMetaDataEntry("material", self._activeMaterialId())
  151. else:
  152. profile.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0])
  153. ContainerRegistry.getInstance().addContainer(profile)
  154. ## Gets a list of profile writer plugins
  155. # \return List of tuples of (plugin_id, meta_data).
  156. def _getIOPlugins(self, io_type):
  157. plugin_registry = PluginRegistry.getInstance()
  158. active_plugin_ids = plugin_registry.getActivePlugins()
  159. result = []
  160. for plugin_id in active_plugin_ids:
  161. meta_data = plugin_registry.getMetaData(plugin_id)
  162. if io_type in meta_data:
  163. result.append( (plugin_id, meta_data) )
  164. return result
  165. ## Gets the active definition
  166. # \return the active definition object or None if there is no definition
  167. def _activeDefinition(self):
  168. global_container_stack = Application.getInstance().getGlobalContainerStack()
  169. if global_container_stack:
  170. definition = global_container_stack.getBottom()
  171. if definition:
  172. return definition
  173. return None
  174. ## Returns true if the current machine requires its own materials
  175. # \return True if the current machine requires its own materials
  176. def _machineHasOwnMaterials(self):
  177. global_container_stack = Application.getInstance().getGlobalContainerStack()
  178. if global_container_stack:
  179. return global_container_stack.getMetaDataEntry("has_materials", False)
  180. return False
  181. ## Gets the ID of the active material
  182. # \return the ID of the active material or the empty string
  183. def _activeMaterialId(self):
  184. global_container_stack = Application.getInstance().getGlobalContainerStack()
  185. if global_container_stack:
  186. material = global_container_stack.findContainer({"type": "material"})
  187. if material:
  188. return material.getId()
  189. return ""
  190. ## Returns true if the current machien requires its own quality profiles
  191. # \return true if the current machien requires its own quality profiles
  192. def _machineHasOwnQualities(self):
  193. global_container_stack = Application.getInstance().getGlobalContainerStack()
  194. if global_container_stack:
  195. return parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", False))
  196. return False