123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- # Copyright (c) 2021 Ultimaker B.V.
- # Cura is released under the terms of the LGPLv3 or higher.
- import os
- from collections import deque
- from typing import TYPE_CHECKING, Optional, List, Dict, Any
- from PyQt6.QtCore import QUrl, Qt, pyqtSlot, pyqtProperty, pyqtSignal
- from UM.i18n import i18nCatalog
- from UM.Logger import Logger
- from UM.Qt.ListModel import ListModel
- from UM.Resources import Resources
- if TYPE_CHECKING:
- from PyQt6.QtCore import QObject
- from cura.CuraApplication import CuraApplication
- class WelcomePagesModel(ListModel):
- """
- 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 welcome wizard dialog. Each item in this ListModel represents a page, which contains the following fields:
- - id : A unique page_id which can be used in function goToPage(page_id)
- - page_url : The QUrl to the QML file that contains the content of this page
- - next_page_id : (OPTIONAL) The next page ID to go to when this page finished. This is optional. If this is
- not provided, it will go to the page with the current index + 1
- - next_page_button_text : (OPTIONAL) The text to show for the "next" button, by default it's the translated text of
- "Next". Note that each step QML can decide whether to use this text or not, so it's not
- mandatory.
- - should_show_function : (OPTIONAL) An optional function that returns True/False indicating if this page should be
- shown. By default all pages should be shown. If a function returns False, that page will
- be skipped and its next page will be shown.
- Note that in any case, a page that has its "should_show_function" == False will ALWAYS be skipped.
- """
- IdRole = Qt.ItemDataRole.UserRole + 1 # Page ID
- PageUrlRole = Qt.ItemDataRole.UserRole + 2 # URL to the page's QML file
- NextPageIdRole = Qt.ItemDataRole.UserRole + 3 # The next page ID it should go to
- NextPageButtonTextRole = Qt.ItemDataRole.UserRole + 4 # The text for the next page button
- PreviousPageButtonTextRole = Qt.ItemDataRole.UserRole + 5 # The text for the previous page button
- def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
- super().__init__(parent)
- self.addRoleName(self.IdRole, "id")
- self.addRoleName(self.PageUrlRole, "page_url")
- self.addRoleName(self.NextPageIdRole, "next_page_id")
- self.addRoleName(self.NextPageButtonTextRole, "next_page_button_text")
- self.addRoleName(self.PreviousPageButtonTextRole, "previous_page_button_text")
- self._application = application
- self._catalog = i18nCatalog("cura")
- self._default_next_button_text = self._catalog.i18nc("@action:button", "Next")
- self._pages: List[Dict[str, Any]] = []
- self._current_page_index = 0
- # Store all the previous page indices so it can go back.
- self._previous_page_indices_stack: deque = deque()
- # If the welcome flow should be shown. It can show the complete flow or just the changelog depending on the
- # specific case. See initialize() for how this variable is set.
- self._should_show_welcome_flow = False
- allFinished = pyqtSignal() # emitted when all steps have been finished
- currentPageIndexChanged = pyqtSignal()
- @pyqtProperty(int, notify = currentPageIndexChanged)
- def currentPageIndex(self) -> int:
- return self._current_page_index
- @pyqtProperty(float, notify = currentPageIndexChanged)
- def currentProgress(self) -> float:
- """
- Returns a float number in [0, 1] which indicates the current progress.
- """
- if len(self._items) == 0:
- return 0
- else:
- return self._current_page_index / len(self._items)
- @pyqtProperty(bool, notify = currentPageIndexChanged)
- def isCurrentPageLast(self) -> bool:
- """
- Indicates if the current page is the last page.
- """
- return self._current_page_index == len(self._items) - 1
- def _setCurrentPageIndex(self, page_index: int) -> None:
- if page_index != self._current_page_index:
- self._previous_page_indices_stack.append(self._current_page_index)
- self._current_page_index = page_index
- self.currentPageIndexChanged.emit()
- @pyqtSlot()
- def atEnd(self) -> None:
- """
- Ends the Welcome-Pages. Put as a separate function for cases like the 'decline' in the User-Agreement.
- """
- self.allFinished.emit()
- self.resetState()
- @pyqtSlot()
- def goToNextPage(self, from_index: Optional[int] = None) -> None:
- """
- Goes to the next page.
- If "from_index" is given, it will look for the next page to show starting from the "from_index" page instead of
- the "self._current_page_index".
- """
- # Look for the next page that should be shown
- current_index = self._current_page_index if from_index is None else from_index
- while True:
- page_item = self._items[current_index]
- # Check if there's a "next_page_id" assigned. If so, go to that page. Otherwise, go to the page with the
- # current index + 1.
- next_page_id = page_item.get("next_page_id")
- next_page_index = current_index + 1
- if next_page_id:
- idx = self.getPageIndexById(next_page_id)
- if idx is None:
- # FIXME: If we cannot find the next page, we cannot do anything here.
- Logger.log("e", "Cannot find page with ID [%s]", next_page_id)
- return
- next_page_index = idx
- is_final_page = page_item.get("is_final_page")
- # If we have reached the last page, emit allFinished signal and reset.
- if next_page_index == len(self._items) or is_final_page:
- self.atEnd()
- return
- # Check if the this page should be shown (default yes), if not, keep looking for the next one.
- next_page_item = self.getItem(next_page_index)
- if self._shouldPageBeShown(next_page_index):
- break
- Logger.log("d", "Page [%s] should not be displayed, look for the next page.", next_page_item["id"])
- current_index = next_page_index
- # Move to the next page
- self._setCurrentPageIndex(next_page_index)
- @pyqtSlot()
- def goToPreviousPage(self) -> None:
- """
- Goes to the previous page. If there's no previous page, do nothing.
- """
- if len(self._previous_page_indices_stack) == 0:
- Logger.log("i", "No previous page, do nothing")
- return
- previous_page_index = self._previous_page_indices_stack.pop()
- self._current_page_index = previous_page_index
- self.currentPageIndexChanged.emit()
- @pyqtSlot(str)
- def goToPage(self, page_id: str) -> None:
- """Sets the current page to the given page ID. If the page ID is not found, do nothing."""
- page_index = self.getPageIndexById(page_id)
- if page_index is None:
- # FIXME: If we cannot find the next page, we cannot do anything here.
- Logger.log("e", "Cannot find page with ID [%s], go to the next page by default", page_index)
- self.goToNextPage()
- return
- if self._shouldPageBeShown(page_index):
- # Move to that page if it should be shown
- self._setCurrentPageIndex(page_index)
- else:
- # Find the next page to show starting from the "page_index"
- self.goToNextPage(from_index = page_index)
- def _shouldPageBeShown(self, page_index: int) -> bool:
- """
- Checks if the page with the given index should be shown by calling the "should_show_function" associated with
- it. If the function is not present, returns True (show page by default).
- """
- next_page_item = self.getItem(page_index)
- should_show_function = next_page_item.get("should_show_function", lambda: True)
- return should_show_function()
- @pyqtSlot()
- def resetState(self) -> None:
- """
- Resets the state of the WelcomePagesModel. This functions does the following:
- - Resets current_page_index to 0
- - Clears the previous page indices stack
- """
- self._current_page_index = 0
- self._previous_page_indices_stack.clear()
- self.currentPageIndexChanged.emit()
- shouldShowWelcomeFlowChanged = pyqtSignal()
- @pyqtProperty(bool, notify = shouldShowWelcomeFlowChanged)
- def shouldShowWelcomeFlow(self) -> bool:
- return self._should_show_welcome_flow
- def getPageIndexById(self, page_id: str) -> Optional[int]:
- """Gets the page index with the given page ID. If the page ID doesn't exist, returns None."""
- page_idx = None
- for idx, page_item in enumerate(self._items):
- if page_item["id"] == page_id:
- page_idx = idx
- break
- return page_idx
- @staticmethod
- def _getBuiltinWelcomePagePath(page_filename: str) -> QUrl:
- """Convenience function to get QUrl path to pages that's located in "resources/qml/WelcomePages"."""
- from cura.CuraApplication import CuraApplication
- return QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles,
- os.path.join("WelcomePages", page_filename)))
- # FIXME: HACKs for optimization that we don't update the model every time the active machine gets changed.
- def _onActiveMachineChanged(self) -> None:
- self._application.getMachineManager().globalContainerChanged.disconnect(self._onActiveMachineChanged)
- self._initialize(update_should_show_flag = False)
- def initialize(self) -> None:
- self._application.getMachineManager().globalContainerChanged.connect(self._onActiveMachineChanged)
- self._initialize()
- def _initialize(self, update_should_show_flag: bool = True) -> None:
- show_whats_new_only = False
- if update_should_show_flag:
- has_active_machine = self._application.getMachineManager().activeMachine is not None
- has_app_just_upgraded = self._application.hasJustUpdatedFromOldVersion()
- # Only show the what's new dialog if there's no machine and we have just upgraded
- show_complete_flow = not has_active_machine
- show_whats_new_only = has_active_machine and has_app_just_upgraded
- # FIXME: This is a hack. Because of the circular dependency between MachineManager, ExtruderManager, and
- # possibly some others, setting the initial active machine is not done when the MachineManager gets
- # initialized. So at this point, we don't know if there will be an active machine or not. It could be that
- # the active machine files are corrupted so we cannot rely on Preferences either. This makes sure that once
- # the active machine gets changed, this model updates the flags, so it can decide whether to show the
- # welcome flow or not.
- should_show_welcome_flow = show_complete_flow or show_whats_new_only
- if should_show_welcome_flow != self._should_show_welcome_flow:
- self._should_show_welcome_flow = should_show_welcome_flow
- self.shouldShowWelcomeFlowChanged.emit()
- # All pages
- all_pages_list = [{"id": "welcome",
- "page_url": self._getBuiltinWelcomePagePath("WelcomeContent.qml"),
- },
- {"id": "user_agreement",
- "page_url": self._getBuiltinWelcomePagePath("UserAgreementContent.qml"),
- },
- {"id": "data_collections",
- "page_url": self._getBuiltinWelcomePagePath("DataCollectionsContent.qml"),
- },
- {"id": "cloud",
- "page_url": self._getBuiltinWelcomePagePath("CloudContent.qml"),
- "should_show_function": self.shouldShowCloudPage,
- },
- {"id": "add_network_or_local_printer",
- "page_url": self._getBuiltinWelcomePagePath("AddNetworkOrLocalPrinterContent.qml"),
- "next_page_id": "machine_actions",
- },
- {"id": "add_printer_by_ip",
- "page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"),
- "next_page_id": "machine_actions",
- },
- {"id": "add_cloud_printers",
- "page_url": self._getBuiltinWelcomePagePath("AddCloudPrintersView.qml"),
- "next_page_button_text": self._catalog.i18nc("@action:button", "Next"),
- "next_page_id": "whats_new",
- },
- {"id": "machine_actions",
- "page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"),
- "should_show_function": self.shouldShowMachineActions,
- },
- {"id": "whats_new",
- "page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"),
- "next_page_button_text": self._catalog.i18nc("@action:button", "Skip"),
- },
- {"id": "changelog",
- "page_url": self._getBuiltinWelcomePagePath("ChangelogContent.qml"),
- "next_page_button_text": self._catalog.i18nc("@action:button", "Finish"),
- },
- ]
- pages_to_show = all_pages_list
- if show_whats_new_only:
- pages_to_show = list(filter(lambda x: x["id"] == "whats_new", all_pages_list))
- self._pages = pages_to_show
- self.setItems(self._pages)
- def setItems(self, items: List[Dict[str, Any]]) -> None:
- # For convenience, inject the default "next" button text to each item if it's not present.
- for item in items:
- if "next_page_button_text" not in item:
- item["next_page_button_text"] = self._default_next_button_text
- super().setItems(items)
- def shouldShowMachineActions(self) -> bool:
- """
- Indicates if the machine action panel should be shown by checking if there's any first start machine actions
- available.
- """
- global_stack = self._application.getMachineManager().activeMachine
- if global_stack is None:
- return False
- definition_id = global_stack.definition.getId()
- first_start_actions = self._application.getMachineActionManager().getFirstStartActions(definition_id)
- return len([action for action in first_start_actions if action.needsUserInteraction()]) > 0
- def shouldShowCloudPage(self) -> bool:
- """
- The cloud page should be shown only if the user is not logged in
- :return: True if the user is not logged in, False if he/she is
- """
- # Import CuraApplication locally or else it fails
- from cura.CuraApplication import CuraApplication
- api = CuraApplication.getInstance().getCuraAPI()
- return not api.account.isLoggedIn
- def addPage(self) -> None:
- pass
|