WelcomePagesModel.py 11 KB

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