WelcomePagesModel.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. # Copyright (c) 2019 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from collections import deque
  4. import os
  5. from typing import TYPE_CHECKING, Optional, List, Dict, Any, Deque
  6. from PyQt5.QtCore import QUrl, Qt, pyqtSlot, pyqtProperty, pyqtSignal
  7. from UM.Logger import Logger
  8. from UM.Qt.ListModel import ListModel
  9. from UM.Resources import Resources
  10. if TYPE_CHECKING:
  11. from PyQt5.QtCore import QObject
  12. from cura.CuraApplication import CuraApplication
  13. #
  14. # This is the Qt ListModel that contains all welcome pages data. Each page is a page that can be shown as a step in the
  15. # welcome wizard dialog. Each item in this ListModel represents a page, which contains the following fields:
  16. #
  17. # - id : A unique page_id which can be used in function goToPage(page_id)
  18. # - page_url : The QUrl to the QML file that contains the content of this page
  19. # - next_page_id : (OPTIONAL) The next page ID to go to when this page finished. This is optional. If this is not
  20. # provided, it will go to the page with the current index + 1
  21. # - should_show_function : (OPTIONAL) An optional function that returns True/False indicating if this page should be
  22. # shown. By default all pages should be shown. If a function returns False, that page will
  23. # be skipped and its next page will be shown.
  24. #
  25. # Note that in any case, a page that has its "should_show_function" == False will ALWAYS be skipped.
  26. #
  27. class WelcomePagesModel(ListModel):
  28. IdRole = Qt.UserRole + 1 # Page ID
  29. PageUrlRole = Qt.UserRole + 2 # URL to the page's QML file
  30. NextPageIdRole = Qt.UserRole + 3 # The next page ID it should go to
  31. def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
  32. super().__init__(parent)
  33. self.addRoleName(self.IdRole, "id")
  34. self.addRoleName(self.PageUrlRole, "page_url")
  35. self.addRoleName(self.NextPageIdRole, "next_page_id")
  36. self._application = application
  37. self._pages = [] # type: List[Dict[str, Any]]
  38. self._current_page_index = 0
  39. # Store all the previous page indices so it can go back.
  40. self._previous_page_indices_stack = deque() # type: Deque[int]
  41. allFinished = pyqtSignal() # emitted when all steps have been finished
  42. currentPageIndexChanged = pyqtSignal()
  43. @pyqtProperty(int, notify = currentPageIndexChanged)
  44. def currentPageIndex(self) -> int:
  45. return self._current_page_index
  46. # Returns a float number in [0, 1] which indicates the current progress.
  47. @pyqtProperty(float, notify = currentPageIndexChanged)
  48. def currentProgress(self) -> float:
  49. if len(self._items) == 0:
  50. return 0
  51. else:
  52. return self._current_page_index / len(self._items)
  53. # Indicates if the current page is the last page.
  54. @pyqtProperty(bool, notify = currentPageIndexChanged)
  55. def isCurrentPageLast(self) -> bool:
  56. return self._current_page_index == len(self._items) - 1
  57. def _setCurrentPageIndex(self, page_index: int) -> None:
  58. if page_index != self._current_page_index:
  59. self._previous_page_indices_stack.append(self._current_page_index)
  60. self._current_page_index = page_index
  61. self.currentPageIndexChanged.emit()
  62. # Ends the Welcome-Pages. Put as a separate function for cases like the 'decline' in the User-Agreement.
  63. @pyqtSlot()
  64. def atEnd(self) -> None:
  65. self.allFinished.emit()
  66. self.resetState()
  67. # Goes to the next page.
  68. # If "from_index" is given, it will look for the next page to show starting from the "from_index" page instead of
  69. # the "self._current_page_index".
  70. @pyqtSlot()
  71. def goToNextPage(self, from_index: Optional[int] = None) -> None:
  72. # Look for the next page that should be shown
  73. current_index = self._current_page_index if from_index is None else from_index
  74. while True:
  75. page_item = self._items[current_index]
  76. # Check if there's a "next_page_id" assigned. If so, go to that page. Otherwise, go to the page with the
  77. # current index + 1.
  78. next_page_id = page_item.get("next_page_id")
  79. next_page_index = current_index + 1
  80. if next_page_id:
  81. idx = self.getPageIndexById(next_page_id)
  82. if idx is None:
  83. # FIXME: If we cannot find the next page, we cannot do anything here.
  84. Logger.log("e", "Cannot find page with ID [%s]", next_page_id)
  85. return
  86. next_page_index = idx
  87. # If we have reached the last page, emit allFinished signal and reset.
  88. if next_page_index == len(self._items):
  89. self.atEnd()
  90. return
  91. # Check if the this page should be shown (default yes), if not, keep looking for the next one.
  92. next_page_item = self.getItem(next_page_index)
  93. if self._shouldPageBeShown(next_page_index):
  94. break
  95. Logger.log("d", "Page [%s] should not be displayed, look for the next page.", next_page_item["id"])
  96. current_index = next_page_index
  97. # Move to the next page
  98. self._setCurrentPageIndex(next_page_index)
  99. # Goes to the previous page. If there's no previous page, do nothing.
  100. @pyqtSlot()
  101. def goToPreviousPage(self) -> None:
  102. if len(self._previous_page_indices_stack) == 0:
  103. Logger.log("i", "No previous page, do nothing")
  104. return
  105. previous_page_index = self._previous_page_indices_stack.pop()
  106. self._current_page_index = previous_page_index
  107. self.currentPageIndexChanged.emit()
  108. # Sets the current page to the given page ID. If the page ID is not found, do nothing.
  109. @pyqtSlot(str)
  110. def goToPage(self, page_id: str) -> None:
  111. page_index = self.getPageIndexById(page_id)
  112. if page_index is None:
  113. # FIXME: If we cannot find the next page, we cannot do anything here.
  114. Logger.log("e", "Cannot find page with ID [%s]", page_index)
  115. return
  116. if self._shouldPageBeShown(page_index):
  117. # Move to that page if it should be shown
  118. self._setCurrentPageIndex(page_index)
  119. else:
  120. # Find the next page to show starting from the "page_index"
  121. self.goToNextPage(from_index = page_index)
  122. # Checks if the page with the given index should be shown by calling the "should_show_function" associated with it.
  123. # If the function is not present, returns True (show page by default).
  124. def _shouldPageBeShown(self, page_index: int) -> bool:
  125. next_page_item = self.getItem(page_index)
  126. should_show_function = next_page_item.get("should_show_function", lambda: True)
  127. return should_show_function()
  128. # Resets the state of the WelcomePagesModel. This functions does the following:
  129. # - Resets current_page_index to 0
  130. # - Clears the previous page indices stack
  131. @pyqtSlot()
  132. def resetState(self) -> None:
  133. self._current_page_index = 0
  134. self._previous_page_indices_stack.clear()
  135. self.currentPageIndexChanged.emit()
  136. # Gets the page index with the given page ID. If the page ID doesn't exist, returns None.
  137. def getPageIndexById(self, page_id: str) -> Optional[int]:
  138. page_idx = None
  139. for idx, page_item in enumerate(self._items):
  140. if page_item["id"] == page_id:
  141. page_idx = idx
  142. break
  143. return page_idx
  144. # Convenience function to get QUrl path to pages that's located in "resources/qml/WelcomePages".
  145. def _getBuiltinWelcomePagePath(self, page_filename: str) -> "QUrl":
  146. from cura.CuraApplication import CuraApplication
  147. return QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles,
  148. os.path.join("WelcomePages", page_filename)))
  149. def initialize(self) -> None:
  150. # Add default welcome pages
  151. self._pages.append({"id": "welcome",
  152. "page_url": self._getBuiltinWelcomePagePath("WelcomeContent.qml"),
  153. })
  154. self._pages.append({"id": "user_agreement",
  155. "page_url": self._getBuiltinWelcomePagePath("UserAgreementContent.qml"),
  156. })
  157. self._pages.append({"id": "whats_new",
  158. "page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"),
  159. })
  160. self._pages.append({"id": "data_collections",
  161. "page_url": self._getBuiltinWelcomePagePath("DataCollectionsContent.qml"),
  162. })
  163. self._pages.append({"id": "add_network_or_local_printer",
  164. "page_url": self._getBuiltinWelcomePagePath("AddNetworkOrLocalPrinterContent.qml"),
  165. "next_page_id": "machine_actions",
  166. })
  167. self._pages.append({"id": "add_printer_by_ip",
  168. "page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"),
  169. "next_page_id": "machine_actions",
  170. })
  171. self._pages.append({"id": "machine_actions",
  172. "page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"),
  173. "next_page_id": "cloud",
  174. "should_show_function": self.shouldShowMachineActions,
  175. })
  176. self._pages.append({"id": "cloud",
  177. "page_url": self._getBuiltinWelcomePagePath("CloudContent.qml"),
  178. })
  179. self.setItems(self._pages)
  180. # Indicates if the machine action panel should be shown by checking if there's any first start machine actions
  181. # available.
  182. def shouldShowMachineActions(self) -> bool:
  183. global_stack = self._application.getMachineManager().activeMachine
  184. if global_stack is None:
  185. return False
  186. definition_id = global_stack.definition.getId()
  187. first_start_actions = self._application.getMachineActionManager().getFirstStartActions(definition_id)
  188. return len(first_start_actions) > 0
  189. def addPage(self) -> None:
  190. pass
  191. __all__ = ["WelcomePagesModel"]