ContainerTree.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. # Copyright (c) 2019 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from UM.Job import Job # For our background task of loading MachineNodes lazily.
  4. from UM.JobQueue import JobQueue # For our background task of loading MachineNodes lazily.
  5. from UM.Logger import Logger
  6. from UM.Settings.ContainerRegistry import ContainerRegistry # To listen to containers being added.
  7. from UM.Signal import Signal
  8. import cura.CuraApplication # Imported like this to prevent circular dependencies.
  9. from cura.Machines.MachineNode import MachineNode
  10. from cura.Settings.GlobalStack import GlobalStack # To listen only to global stacks being added.
  11. from typing import Dict, List, Optional, TYPE_CHECKING
  12. import time
  13. if TYPE_CHECKING:
  14. from cura.Machines.QualityGroup import QualityGroup
  15. from cura.Machines.QualityChangesGroup import QualityChangesGroup
  16. from UM.Settings.ContainerStack import ContainerStack
  17. ## This class contains a look-up tree for which containers are available at
  18. # which stages of configuration.
  19. #
  20. # The tree starts at the machine definitions. For every distinct definition
  21. # there will be one machine node here.
  22. #
  23. # All of the fallbacks for material choices, quality choices, etc. should be
  24. # encoded in this tree. There must always be at least one child node (for
  25. # nodes that have children) but that child node may be a node representing the
  26. # empty instance container.
  27. class ContainerTree:
  28. __instance = None # type: Optional["ContainerTree"]
  29. @classmethod
  30. def getInstance(cls):
  31. if cls.__instance is None:
  32. cls.__instance = ContainerTree()
  33. return cls.__instance
  34. def __init__(self) -> None:
  35. self.machines = self._MachineNodeMap() # Mapping from definition ID to machine nodes with lazy loading.
  36. self.materialsChanged = Signal() # Emitted when any of the material nodes in the tree got changed.
  37. cura.CuraApplication.CuraApplication.getInstance().initializationFinished.connect(self._onStartupFinished) # Start the background task to load more machine nodes after start-up is completed.
  38. ## Get the quality groups available for the currently activated printer.
  39. #
  40. # This contains all quality groups, enabled or disabled. To check whether
  41. # the quality group can be activated, test for the
  42. # ``QualityGroup.is_available`` property.
  43. # \return For every quality type, one quality group.
  44. def getCurrentQualityGroups(self) -> Dict[str, "QualityGroup"]:
  45. global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
  46. if global_stack is None:
  47. return {}
  48. variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList]
  49. material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList]
  50. extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
  51. return self.machines[global_stack.definition.getId()].getQualityGroups(variant_names, material_bases, extruder_enabled)
  52. ## Get the quality changes groups available for the currently activated
  53. # printer.
  54. #
  55. # This contains all quality changes groups, enabled or disabled. To check
  56. # whether the quality changes group can be activated, test for the
  57. # ``QualityChangesGroup.is_available`` property.
  58. # \return A list of all quality changes groups.
  59. def getCurrentQualityChangesGroups(self) -> List["QualityChangesGroup"]:
  60. global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
  61. if global_stack is None:
  62. return []
  63. variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList]
  64. material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList]
  65. extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
  66. return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
  67. ## Ran after completely starting up the application.
  68. def _onStartupFinished(self) -> None:
  69. currently_added = ContainerRegistry.getInstance().findContainerStacks() # Find all currently added global stacks.
  70. JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added))
  71. ## Dictionary-like object that contains the machines.
  72. #
  73. # This handles the lazy loading of MachineNodes.
  74. class _MachineNodeMap:
  75. def __init__(self) -> None:
  76. self._machines = {} # type: Dict[str, MachineNode]
  77. ## Returns whether a printer with a certain definition ID exists. This
  78. # is regardless of whether or not the printer is loaded yet.
  79. # \param definition_id The definition to look for.
  80. # \return Whether or not a printer definition exists with that name.
  81. def __contains__(self, definition_id: str) -> bool:
  82. return len(ContainerRegistry.getInstance().findContainersMetadata(id = definition_id)) > 0
  83. ## Returns a machine node for the specified definition ID.
  84. #
  85. # If the machine node wasn't loaded yet, this will load it lazily.
  86. # \param definition_id The definition to look for.
  87. # \return A machine node for that definition.
  88. def __getitem__(self, definition_id: str) -> MachineNode:
  89. if definition_id not in self._machines:
  90. start_time = time.time()
  91. self._machines[definition_id] = MachineNode(definition_id)
  92. self._machines[definition_id].materialsChanged.connect(ContainerTree.getInstance().materialsChanged)
  93. Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - start_time))
  94. return self._machines[definition_id]
  95. ## Gets a machine node for the specified definition ID, with default.
  96. #
  97. # The default is returned if there is no definition with the specified
  98. # ID. If the machine node wasn't loaded yet, this will load it lazily.
  99. # \param definition_id The definition to look for.
  100. # \param default The machine node to return if there is no machine
  101. # with that definition (can be ``None`` optionally or if not
  102. # provided).
  103. # \return A machine node for that definition, or the default if there
  104. # is no definition with the provided definition_id.
  105. def get(self, definition_id: str, default: Optional[MachineNode] = None) -> Optional[MachineNode]:
  106. if definition_id not in self:
  107. return default
  108. return self[definition_id]
  109. ## Returns whether we've already cached this definition's node.
  110. # \param definition_id The definition that we may have cached.
  111. # \return ``True`` if it's cached.
  112. def is_loaded(self, definition_id: str) -> bool:
  113. return definition_id in self._machines
  114. ## Pre-loads all currently added printers as a background task so that
  115. # switching printers in the interface is faster.
  116. class _MachineNodeLoadJob(Job):
  117. ## Creates a new background task.
  118. # \param tree_root The container tree instance. This cannot be
  119. # obtained through the singleton static function since the instance
  120. # may not yet be constructed completely.
  121. # \param container_stacks All of the stacks to pre-load the container
  122. # trees for. This needs to be provided from here because the stacks
  123. # need to be constructed on the main thread because they are QObject.
  124. def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]) -> None:
  125. self.tree_root = tree_root
  126. self.container_stacks = container_stacks
  127. super().__init__()
  128. ## Starts the background task.
  129. #
  130. # The ``JobQueue`` will schedule this on a different thread.
  131. def run(self) -> None:
  132. for stack in self.container_stacks: # Load all currently-added containers.
  133. if not isinstance(stack, GlobalStack):
  134. continue
  135. # Allow a thread switch after every container.
  136. # Experimentally, sleep(0) didn't allow switching. sleep(0.1) or sleep(0.2) neither.
  137. # We're in no hurry though. Half a second is fine.
  138. time.sleep(0.5)
  139. definition_id = stack.definition.getId()
  140. if not self.tree_root.machines.is_loaded(definition_id):
  141. _ = self.tree_root.machines[definition_id]