QualityManagementModel.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. # Copyright (c) 2019 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from typing import Any, cast, Dict, Optional, TYPE_CHECKING
  4. from PyQt5.QtCore import pyqtSlot, QObject, Qt, QTimer
  5. from UM.Logger import Logger
  6. from UM.Qt.ListModel import ListModel
  7. from UM.Settings.InstanceContainer import InstanceContainer # To create new profiles.
  8. import cura.CuraApplication # Imported this way to prevent circular imports.
  9. from cura.Settings.ContainerManager import ContainerManager
  10. from cura.Machines.ContainerTree import ContainerTree
  11. from cura.Settings.cura_empty_instance_containers import empty_quality_changes_container
  12. from cura.Settings.IntentManager import IntentManager
  13. from cura.Machines.Models.MachineModelUtils import fetchLayerHeight
  14. from UM.i18n import i18nCatalog
  15. catalog = i18nCatalog("cura")
  16. if TYPE_CHECKING:
  17. from UM.Settings.Interfaces import ContainerInterface
  18. from cura.Machines.QualityChangesGroup import QualityChangesGroup
  19. from cura.Settings.ExtruderStack import ExtruderStack
  20. from cura.Settings.GlobalStack import GlobalStack
  21. #
  22. # This the QML model for the quality management page.
  23. #
  24. class QualityManagementModel(ListModel):
  25. NameRole = Qt.UserRole + 1
  26. IsReadOnlyRole = Qt.UserRole + 2
  27. QualityGroupRole = Qt.UserRole + 3
  28. QualityTypeRole = Qt.UserRole + 4
  29. QualityChangesGroupRole = Qt.UserRole + 5
  30. IntentCategoryRole = Qt.UserRole + 6
  31. SectionNameRole = Qt.UserRole + 7
  32. def __init__(self, parent: Optional["QObject"] = None) -> None:
  33. super().__init__(parent)
  34. self.addRoleName(self.NameRole, "name")
  35. self.addRoleName(self.IsReadOnlyRole, "is_read_only")
  36. self.addRoleName(self.QualityGroupRole, "quality_group")
  37. self.addRoleName(self.QualityTypeRole, "quality_type")
  38. self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
  39. self.addRoleName(self.IntentCategoryRole, "intent_category")
  40. self.addRoleName(self.SectionNameRole, "section_name")
  41. application = cura.CuraApplication.CuraApplication.getInstance()
  42. container_registry = application.getContainerRegistry()
  43. self._machine_manager = application.getMachineManager()
  44. self._machine_manager.activeQualityGroupChanged.connect(self._onChange)
  45. self._machine_manager.activeStackChanged.connect(self._onChange)
  46. self._machine_manager.extruderChanged.connect(self._onChange)
  47. self._machine_manager.globalContainerChanged.connect(self._onChange)
  48. self._extruder_manager = application.getExtruderManager()
  49. self._extruder_manager.extrudersChanged.connect(self._onChange)
  50. container_registry.containerAdded.connect(self._qualityChangesListChanged)
  51. container_registry.containerRemoved.connect(self._qualityChangesListChanged)
  52. container_registry.containerMetaDataChanged.connect(self._qualityChangesListChanged)
  53. self._update_timer = QTimer()
  54. self._update_timer.setInterval(100)
  55. self._update_timer.setSingleShot(True)
  56. self._update_timer.timeout.connect(self._update)
  57. self._onChange()
  58. def _onChange(self) -> None:
  59. self._update_timer.start()
  60. ## Deletes a custom profile. It will be gone forever.
  61. # \param quality_changes_group The quality changes group representing the
  62. # profile to delete.
  63. @pyqtSlot(QObject)
  64. def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
  65. Logger.log("i", "Removing quality changes group {group_name}".format(group_name = quality_changes_group.name))
  66. removed_quality_changes_ids = set()
  67. container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
  68. for metadata in [quality_changes_group.metadata_for_global] + list(quality_changes_group.metadata_per_extruder.values()):
  69. container_id = metadata["id"]
  70. container_registry.removeContainer(container_id)
  71. removed_quality_changes_ids.add(container_id)
  72. # Reset all machines that have activated this custom profile.
  73. for global_stack in container_registry.findContainerStacks(type = "machine"):
  74. if global_stack.qualityChanges.getId() in removed_quality_changes_ids:
  75. global_stack.qualityChanges = empty_quality_changes_container
  76. for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"):
  77. if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids:
  78. extruder_stack.qualityChanges = empty_quality_changes_container
  79. ## Rename a custom profile.
  80. #
  81. # Because the names must be unique, the new name may not actually become
  82. # the name that was given. The actual name is returned by this function.
  83. # \param quality_changes_group The custom profile that must be renamed.
  84. # \param new_name The desired name for the profile.
  85. # \return The actual new name of the profile, after making the name
  86. # unique.
  87. @pyqtSlot(QObject, str, result = str)
  88. def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str:
  89. Logger.log("i", "Renaming QualityChangesGroup {old_name} to {new_name}.".format(old_name = quality_changes_group.name, new_name = new_name))
  90. if new_name == quality_changes_group.name:
  91. Logger.log("i", "QualityChangesGroup name {name} unchanged.".format(name = quality_changes_group.name))
  92. return new_name
  93. application = cura.CuraApplication.CuraApplication.getInstance()
  94. container_registry = application.getContainerRegistry()
  95. new_name = container_registry.uniqueName(new_name)
  96. # CURA-6842
  97. # FIXME: setName() will trigger metaDataChanged signal that are connected with type Qt.AutoConnection. In this
  98. # case, setName() will trigger direct connections which in turn causes the quality changes group and the models
  99. # to update. Because multiple containers need to be renamed, and every time a container gets renamed, updates
  100. # gets triggered and this results in partial updates. For example, if we rename the global quality changes
  101. # container first, the rest of the system still thinks that I have selected "my_profile" instead of
  102. # "my_new_profile", but an update already gets triggered, and the quality changes group that's selected will
  103. # have no container for the global stack, because "my_profile" just got renamed to "my_new_profile". This results
  104. # in crashes because the rest of the system assumes that all data in a QualityChangesGroup will be correct.
  105. #
  106. # Renaming the container for the global stack in the end seems to be ok, because the assumption is mostly based
  107. # on the quality changes container for the global stack.
  108. for metadata in quality_changes_group.metadata_per_extruder.values():
  109. extruder_container = cast(InstanceContainer, container_registry.findContainers(id = metadata["id"])[0])
  110. extruder_container.setName(new_name)
  111. global_container = cast(InstanceContainer, container_registry.findContainers(id=quality_changes_group.metadata_for_global["id"])[0])
  112. global_container.setName(new_name)
  113. quality_changes_group.name = new_name
  114. application.getMachineManager().activeQualityChanged.emit()
  115. application.getMachineManager().activeQualityGroupChanged.emit()
  116. return new_name
  117. ## Duplicates a given quality profile OR quality changes profile.
  118. # \param new_name The desired name of the new profile. This will be made
  119. # unique, so it might end up with a different name.
  120. # \param quality_model_item The item of this model to duplicate, as
  121. # dictionary. See the descriptions of the roles of this list model.
  122. @pyqtSlot(str, "QVariantMap")
  123. def duplicateQualityChanges(self, new_name: str, quality_model_item: Dict[str, Any]) -> None:
  124. global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
  125. if not global_stack:
  126. Logger.log("i", "No active global stack, cannot duplicate quality (changes) profile.")
  127. return
  128. container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
  129. new_name = container_registry.uniqueName(new_name)
  130. intent_category = quality_model_item["intent_category"]
  131. quality_group = quality_model_item["quality_group"]
  132. quality_changes_group = quality_model_item["quality_changes_group"]
  133. if quality_changes_group is None:
  134. # Create global quality changes only.
  135. new_quality_changes = self._createQualityChanges(quality_group.quality_type, intent_category, new_name,
  136. global_stack, extruder_stack = None)
  137. container_registry.addContainer(new_quality_changes)
  138. else:
  139. for metadata in [quality_changes_group.metadata_for_global] + list(quality_changes_group.metadata_per_extruder.values()):
  140. containers = container_registry.findContainers(id = metadata["id"])
  141. if not containers:
  142. continue
  143. container = containers[0]
  144. new_id = container_registry.uniqueName(container.getId())
  145. container_registry.addContainer(container.duplicate(new_id, new_name))
  146. ## Create quality changes containers from the user containers in the active
  147. # stacks.
  148. #
  149. # This will go through the global and extruder stacks and create
  150. # quality_changes containers from the user containers in each stack. These
  151. # then replace the quality_changes containers in the stack and clear the
  152. # user settings.
  153. # \param base_name The new name for the quality changes profile. The final
  154. # name of the profile might be different from this, because it needs to be
  155. # made unique.
  156. @pyqtSlot(str)
  157. def createQualityChanges(self, base_name: str) -> None:
  158. machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
  159. global_stack = machine_manager.activeMachine
  160. if not global_stack:
  161. return
  162. active_quality_name = machine_manager.activeQualityOrQualityChangesName
  163. if active_quality_name == "":
  164. Logger.log("w", "No quality container found in stack %s, cannot create profile", global_stack.getId())
  165. return
  166. machine_manager.blurSettings.emit()
  167. if base_name is None or base_name == "":
  168. base_name = active_quality_name
  169. container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
  170. unique_name = container_registry.uniqueName(base_name)
  171. # Go through the active stacks and create quality_changes containers from the user containers.
  172. container_manager = ContainerManager.getInstance()
  173. stack_list = [global_stack] + list(global_stack.extruders.values())
  174. for stack in stack_list:
  175. quality_container = stack.quality
  176. quality_changes_container = stack.qualityChanges
  177. if not quality_container or not quality_changes_container:
  178. Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
  179. continue
  180. extruder_stack = None
  181. intent_category = None
  182. if stack.getMetaDataEntry("position") is not None:
  183. extruder_stack = stack
  184. intent_category = stack.intent.getMetaDataEntry("intent_category")
  185. new_changes = self._createQualityChanges(quality_container.getMetaDataEntry("quality_type"), intent_category, unique_name, global_stack, extruder_stack)
  186. container_manager._performMerge(new_changes, quality_changes_container, clear_settings = False)
  187. container_manager._performMerge(new_changes, stack.userChanges)
  188. container_registry.addContainer(new_changes)
  189. ## Create a quality changes container with the given set-up.
  190. # \param quality_type The quality type of the new container.
  191. # \param intent_category The intent category of the new container.
  192. # \param new_name The name of the container. This name must be unique.
  193. # \param machine The global stack to create the profile for.
  194. # \param extruder_stack The extruder stack to create the profile for. If
  195. # not provided, only a global container will be created.
  196. def _createQualityChanges(self, quality_type: str, intent_category: Optional[str], new_name: str, machine: "GlobalStack", extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer":
  197. container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
  198. base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId()
  199. new_id = base_id + "_" + new_name
  200. new_id = new_id.lower().replace(" ", "_")
  201. new_id = container_registry.uniqueName(new_id)
  202. # Create a new quality_changes container for the quality.
  203. quality_changes = InstanceContainer(new_id)
  204. quality_changes.setName(new_name)
  205. quality_changes.setMetaDataEntry("type", "quality_changes")
  206. quality_changes.setMetaDataEntry("quality_type", quality_type)
  207. if intent_category is not None:
  208. quality_changes.setMetaDataEntry("intent_category", intent_category)
  209. # If we are creating a container for an extruder, ensure we add that to the container.
  210. if extruder_stack is not None:
  211. quality_changes.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
  212. # If the machine specifies qualities should be filtered, ensure we match the current criteria.
  213. machine_definition_id = ContainerTree.getInstance().machines[machine.definition.getId()].quality_definition
  214. quality_changes.setDefinition(machine_definition_id)
  215. quality_changes.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.getInstance().SettingVersion)
  216. return quality_changes
  217. ## Triggered when any container changed.
  218. #
  219. # This filters the updates to the container manager: When it applies to
  220. # the list of quality changes, we need to update our list.
  221. def _qualityChangesListChanged(self, container: "ContainerInterface") -> None:
  222. if container.getMetaDataEntry("type") == "quality_changes":
  223. self._update()
  224. @pyqtSlot("QVariantMap", result = str)
  225. def getQualityItemDisplayName(self, quality_model_item: Dict[str, Any]) -> str:
  226. quality_group = quality_model_item["quality_group"]
  227. is_read_only = quality_model_item["is_read_only"]
  228. intent_category = quality_model_item["intent_category"]
  229. quality_level_name = "Not Supported"
  230. if quality_group is not None:
  231. quality_level_name = quality_group.name
  232. display_name = quality_level_name
  233. if intent_category != "default":
  234. intent_display_name = catalog.i18nc("@label", intent_category.capitalize())
  235. display_name = "{intent_name} - {the_rest}".format(intent_name = intent_display_name,
  236. the_rest = display_name)
  237. # A custom quality
  238. if not is_read_only:
  239. display_name = "{custom_profile_name} - {the_rest}".format(custom_profile_name = quality_model_item["name"],
  240. the_rest = display_name)
  241. return display_name
  242. def _update(self):
  243. Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
  244. global_stack = self._machine_manager.activeMachine
  245. if not global_stack:
  246. self.setItems([])
  247. return
  248. container_tree = ContainerTree.getInstance()
  249. quality_group_dict = container_tree.getCurrentQualityGroups()
  250. quality_changes_group_list = container_tree.getCurrentQualityChangesGroups()
  251. available_quality_types = set(quality_type for quality_type, quality_group in quality_group_dict.items()
  252. if quality_group.is_available)
  253. if not available_quality_types and not quality_changes_group_list:
  254. # Nothing to show
  255. self.setItems([])
  256. return
  257. item_list = []
  258. # Create quality group items (intent category = "default")
  259. for quality_group in quality_group_dict.values():
  260. if not quality_group.is_available:
  261. continue
  262. layer_height = fetchLayerHeight(quality_group)
  263. item = {"name": quality_group.name,
  264. "is_read_only": True,
  265. "quality_group": quality_group,
  266. "quality_type": quality_group.quality_type,
  267. "quality_changes_group": None,
  268. "intent_category": "default",
  269. "section_name": catalog.i18nc("@label", "Default"),
  270. "layer_height": layer_height, # layer_height is only used for sorting
  271. }
  272. item_list.append(item)
  273. # Sort by layer_height for built-in qualities
  274. item_list = sorted(item_list, key = lambda x: x["layer_height"])
  275. # Create intent items (non-default)
  276. available_intent_list = IntentManager.getInstance().getCurrentAvailableIntents()
  277. available_intent_list = [i for i in available_intent_list if i[0] != "default"]
  278. result = []
  279. for intent_category, quality_type in available_intent_list:
  280. result.append({
  281. "name": quality_group_dict[quality_type].name, # Use the quality name as the display name
  282. "is_read_only": True,
  283. "quality_group": quality_group_dict[quality_type],
  284. "quality_type": quality_type,
  285. "quality_changes_group": None,
  286. "intent_category": intent_category,
  287. "section_name": catalog.i18nc("@label", intent_category.capitalize()),
  288. })
  289. # Sort by quality_type for each intent category
  290. result = sorted(result, key = lambda x: (x["intent_category"], x["quality_type"]))
  291. item_list += result
  292. # Create quality_changes group items
  293. quality_changes_item_list = []
  294. for quality_changes_group in quality_changes_group_list:
  295. # CURA-6913 Note that custom qualities can be based on "not supported", so the quality group can be None.
  296. quality_group = quality_group_dict.get(quality_changes_group.quality_type)
  297. quality_type = quality_changes_group.quality_type
  298. item = {"name": quality_changes_group.name,
  299. "is_read_only": False,
  300. "quality_group": quality_group,
  301. "quality_type": quality_type,
  302. "quality_changes_group": quality_changes_group,
  303. "intent_category": quality_changes_group.intent_category,
  304. "section_name": catalog.i18nc("@label", "Custom profiles"),
  305. }
  306. quality_changes_item_list.append(item)
  307. # Sort quality_changes items by names and append to the item list
  308. quality_changes_item_list = sorted(quality_changes_item_list, key = lambda x: x["name"].upper())
  309. item_list += quality_changes_item_list
  310. self.setItems(item_list)
  311. # TODO: Duplicated code here from InstanceContainersModel. Refactor and remove this later.
  312. #
  313. ## Gets a list of the possible file filters that the plugins have
  314. # registered they can read or write. The convenience meta-filters
  315. # "All Supported Types" and "All Files" are added when listing
  316. # readers, but not when listing writers.
  317. #
  318. # \param io_type \type{str} name of the needed IO type
  319. # \return A list of strings indicating file name filters for a file
  320. # dialog.
  321. @pyqtSlot(str, result = "QVariantList")
  322. def getFileNameFilters(self, io_type):
  323. from UM.i18n import i18nCatalog
  324. catalog = i18nCatalog("uranium")
  325. #TODO: This function should be in UM.Resources!
  326. filters = []
  327. all_types = []
  328. for plugin_id, meta_data in self._getIOPlugins(io_type):
  329. for io_plugin in meta_data[io_type]:
  330. filters.append(io_plugin["description"] + " (*." + io_plugin["extension"] + ")")
  331. all_types.append("*.{0}".format(io_plugin["extension"]))
  332. if "_reader" in io_type:
  333. # if we're listing readers, add the option to show all supported files as the default option
  334. filters.insert(0, catalog.i18nc("@item:inlistbox", "All Supported Types ({0})", " ".join(all_types)))
  335. filters.append(catalog.i18nc("@item:inlistbox", "All Files (*)")) # Also allow arbitrary files, if the user so prefers.
  336. return filters
  337. ## Gets a list of profile reader or writer plugins
  338. # \return List of tuples of (plugin_id, meta_data).
  339. def _getIOPlugins(self, io_type):
  340. from UM.PluginRegistry import PluginRegistry
  341. pr = PluginRegistry.getInstance()
  342. active_plugin_ids = pr.getActivePlugins()
  343. result = []
  344. for plugin_id in active_plugin_ids:
  345. meta_data = pr.getMetaData(plugin_id)
  346. if io_type in meta_data:
  347. result.append( (plugin_id, meta_data) )
  348. return result