123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- # Copyright (c) 2021 Ultimaker B.V.
- # Cura is released under the terms of the LGPLv3 or higher.
- import json
- import math
- import os
- import tempfile
- import threading
- from enum import IntEnum
- from pathlib import Path
- from typing import Optional, List, Dict, Any, cast
- from PyQt6.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, pyqtEnum, QTimer, QUrl, QMetaObject
- from PyQt6.QtNetwork import QNetworkReply
- from PyQt6.QtQml import qmlRegisterType, qmlRegisterUncreatableMetaObject
- from UM.FileHandler.FileHandler import FileHandler
- from UM.Logger import Logger
- from UM.Message import Message
- from UM.Scene.SceneNode import SceneNode
- from UM.Signal import Signal
- from UM.TaskManagement.HttpRequestManager import HttpRequestManager
- from cura.API import Account
- from cura.CuraApplication import CuraApplication
- from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
- from .BackwardsCompatibleMessage import getBackwardsCompatibleMessage
- from .DFFileExportAndUploadManager import DFFileExportAndUploadManager
- from .DigitalFactoryApiClient import DigitalFactoryApiClient
- from .DigitalFactoryFileModel import DigitalFactoryFileModel
- from .DigitalFactoryFileResponse import DigitalFactoryFileResponse
- from .DigitalFactoryProjectModel import DigitalFactoryProjectModel
- from .DigitalFactoryProjectResponse import DigitalFactoryProjectResponse
- class DigitalFactoryController(QObject):
- DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB
- selectedProjectIndexChanged = pyqtSignal(int, arguments = ["newProjectIndex"])
- """Signal emitted whenever the selected project is changed in the projects dropdown menu"""
- selectedFileIndicesChanged = pyqtSignal("QList<int>", arguments = ["newFileIndices"])
- """Signal emitted whenever the selected file is changed in the files table"""
- retrievingProjectsStatusChanged = pyqtSignal(int, arguments = ["status"])
- """Signal emitted whenever the status of the 'retrieving projects' http get request is changed"""
- retrievingFilesStatusChanged = pyqtSignal(int, arguments = ["status"])
- """Signal emitted whenever the status of the 'retrieving files in project' http get request is changed"""
- creatingNewProjectStatusChanged = pyqtSignal(int, arguments = ["status"])
- """Signal emitted whenever the status of the 'create new library project' http get request is changed"""
- hasMoreProjectsToLoadChanged = pyqtSignal()
- """Signal emitted whenever the variable hasMoreProjectsToLoad is changed. This variable is used to determine if
- the paginated list of projects has more pages to show"""
- preselectedProjectChanged = pyqtSignal()
- """Signal emitted whenever a preselected project is set. Whenever there is a preselected project, it means that it is
- the only project in the ProjectModel. When the preselected project is invalidated, the ProjectsModel needs to be
- retrieved again."""
- projectCreationErrorTextChanged = pyqtSignal()
- """Signal emitted whenever the creation of a new project fails and a specific error message is returned from the
- server."""
- """Signals to inform about the process of the file upload"""
- uploadStarted = Signal()
- uploadFileProgress = Signal()
- uploadFileSuccess = Signal()
- uploadFileError = Signal()
- uploadFileFinished = Signal()
- """Signal to inform about the state of user access."""
- userAccessStateChanged = pyqtSignal(bool)
- """Signal to inform whether the user is allowed to create more Library projects."""
- userCanCreateNewLibraryProjectChanged = pyqtSignal(bool)
- class RetrievalStatus(IntEnum):
- """
- The status of an http get request.
- This is not an enum, because we want to use it in QML and QML doesn't recognize Python enums.
- """
- Idle = 0
- InProgress = 1
- Success = 2
- Failed = 3
- pyqtEnum(RetrievalStatus)
- def __init__(self, application: CuraApplication) -> None:
- super().__init__(parent = None)
- self._application = application
- self._dialog = None # type: Optional["QObject"]
- self.file_handlers = {} # type: Dict[str, FileHandler]
- self.nodes = None # type: Optional[List[SceneNode]]
- self.file_upload_manager = None # type: Optional[DFFileExportAndUploadManager]
- self._has_preselected_project = False # type: bool
- self._api = DigitalFactoryApiClient(self._application, on_error = lambda error: Logger.log("e", str(error)), projects_limit_per_page = 20)
- # Indicates whether there are more pages of projects that can be loaded from the API
- self._has_more_projects_to_load = False
- self._account = self._application.getInstance().getCuraAPI().account # type: Account
- self._account.loginStateChanged.connect(self._onLoginStateChanged)
- self._current_workspace_information = CuraApplication.getInstance().getCurrentWorkspaceInformation()
- # Initialize the project model
- self._project_model = DigitalFactoryProjectModel()
- self._selected_project_idx = -1
- self._project_creation_error_text = "Something went wrong while creating a new project. Please try again."
- self._project_filter = ""
- self._project_filter_change_timer = QTimer()
- self._project_filter_change_timer.setInterval(200)
- self._project_filter_change_timer.setSingleShot(True)
- self._project_filter_change_timer.timeout.connect(self._applyProjectFilter)
- # Initialize the file model
- self._file_model = DigitalFactoryFileModel()
- self._selected_file_indices = [] # type: List[int]
- # Filled after the application has been initialized
- self._supported_file_types = {} # type: Dict[str, str]
- # For cleaning up the files afterwards:
- self._erase_temp_files_lock = threading.Lock()
- # The statuses which indicate whether Cura is waiting for a response from the DigitalFactory API
- self.retrieving_files_status = self.RetrievalStatus.Idle
- self.retrieving_projects_status = self.RetrievalStatus.Idle
- self.creating_new_project_status = self.RetrievalStatus.Idle
- self._application.engineCreatedSignal.connect(self._onEngineCreated)
- self._application.initializationFinished.connect(self._applicationInitializationFinished)
- self._user_has_access = False
- self._user_account_can_create_new_project = False
- def clear(self) -> None:
- self._project_model.clearProjects()
- self._api.clear()
- self._has_preselected_project = False
- self.preselectedProjectChanged.emit()
- self.setRetrievingFilesStatus(self.RetrievalStatus.Idle)
- self.setRetrievingProjectsStatus(self.RetrievalStatus.Idle)
- self.setCreatingNewProjectStatus(self.RetrievalStatus.Idle)
- self.setSelectedProjectIndex(-1)
- def _onLoginStateChanged(self, logged_in: bool) -> None:
- def callback(has_access, **kwargs):
- self._user_has_access = has_access
- self.userAccessStateChanged.emit(logged_in)
- self._api.checkUserHasAccess(callback)
- def userAccountHasLibraryAccess(self) -> bool:
- """
- Checks whether the currently logged in user account has access to the Digital Library
- :return: True if the user account has Digital Library access, else False
- """
- if self._user_has_access:
- self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject)
- return self._user_has_access
- def initialize(self, preselected_project_id: Optional[str] = None) -> None:
- self.clear()
- if self._account.isLoggedIn and self.userAccountHasLibraryAccess():
- self.setRetrievingProjectsStatus(self.RetrievalStatus.InProgress)
- if preselected_project_id:
- self._api.getProject(preselected_project_id, on_finished = self.setProjectAsPreselected, failed = self._onGetProjectFailed)
- else:
- self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
- def setProjectAsPreselected(self, df_project: DigitalFactoryProjectResponse) -> None:
- """
- Sets the received df_project as the preselected one. When a project is preselected, it should be the only
- project inside the model, so this function first makes sure to clear the projects model.
- :param df_project: The library project intended to be set as preselected
- """
- self._project_model.clearProjects()
- self._project_model.setProjects([df_project])
- self.setSelectedProjectIndex(0)
- self.setHasPreselectedProject(True)
- self.setRetrievingProjectsStatus(self.RetrievalStatus.Success)
- self.setCreatingNewProjectStatus(self.RetrievalStatus.Success)
- def _onGetProjectFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
- reply_string = bytes(reply.readAll()).decode()
- self.setHasPreselectedProject(False)
- Logger.log("w", "Something went wrong while trying to retrieve a the preselected Digital Library project. Error: {}".format(reply_string))
- def _onGetProjectsFirstPageFinished(self, df_projects: List[DigitalFactoryProjectResponse]) -> None:
- """
- Set the first page of projects received from the digital factory library in the project model. Called whenever
- the retrieval of the first page of projects is successful.
- :param df_projects: A list of all the Digital Factory Library projects linked to the user's account
- """
- self.setHasMoreProjectsToLoad(self._api.hasMoreProjectsToLoad())
- self._project_model.setProjects(df_projects)
- self.setRetrievingProjectsStatus(self.RetrievalStatus.Success)
- @pyqtSlot()
- def loadMoreProjects(self) -> None:
- """
- Initiates the process of retrieving the next page of the projects list from the API.
- """
- self._api.getMoreProjects(on_finished = self.loadMoreProjectsFinished, failed = self._onGetProjectsFailed)
- self.setRetrievingProjectsStatus(self.RetrievalStatus.InProgress)
- def loadMoreProjectsFinished(self, df_projects: List[DigitalFactoryProjectResponse]) -> None:
- """
- Set the projects received from the digital factory library in the project model. Called whenever the retrieval
- of the projects is successful.
- :param df_projects: A list of all the Digital Factory Library projects linked to the user's account
- """
- self.setHasMoreProjectsToLoad(self._api.hasMoreProjectsToLoad())
- self._project_model.extendProjects(df_projects)
- self.setRetrievingProjectsStatus(self.RetrievalStatus.Success)
- def _onGetProjectsFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
- """
- Error function, called whenever the retrieval of projects fails.
- """
- self.setRetrievingProjectsStatus(self.RetrievalStatus.Failed)
- Logger.log("w", "Failed to retrieve the list of projects from the Digital Library. Error encountered: {}".format(error))
- def getProjectFilesFinished(self, df_files_in_project: List[DigitalFactoryFileResponse]) -> None:
- """
- Set the files received from the digital factory library in the file model. The files are filtered to only
- contain the files which can be opened by Cura.
- Called whenever the retrieval of the files is successful.
- :param df_files_in_project: A list of all the Digital Factory Library files that exist in a library project
- """
- # Filter to show only the files that can be opened in Cura
- self._file_model.setFilters({"file_name": lambda x: Path(x).suffix[1:].lower() in self._supported_file_types}) # the suffix is in format '.xyz', so omit the dot at the start
- self._file_model.setFiles(df_files_in_project)
- self.setRetrievingFilesStatus(self.RetrievalStatus.Success)
- def getProjectFilesFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
- """
- Error function, called whenever the retrieval of the files in a library project fails.
- """
- try:
- Logger.warning(f"Failed to retrieve the list of files in project '{self._project_model._projects[self._selected_project_idx]}' from the Digital Library")
- except IndexError:
- Logger.warning(f"Failed to retrieve the list of files in a project from the Digital Library. And failed to get the project too.")
- self.setRetrievingFilesStatus(self.RetrievalStatus.Failed)
- @pyqtSlot()
- def clearProjectSelection(self) -> None:
- """
- Clear the selected project.
- """
- if self._has_preselected_project:
- self.setHasPreselectedProject(False)
- else:
- self.setSelectedProjectIndex(-1)
- @pyqtSlot(int)
- def setSelectedProjectIndex(self, project_idx: int) -> None:
- """
- Sets the index of the project which is currently selected in the dropdown menu. Then, it uses the project_id of
- that project to retrieve the list of files included in that project and display it in the interface.
- :param project_idx: The index of the currently selected project
- """
- if project_idx < -1 or project_idx >= len(self._project_model.items):
- Logger.log("w", "The selected project index is invalid.")
- project_idx = -1 # -1 is a valid index for the combobox and it is handled as "nothing is selected"
- self._selected_project_idx = project_idx
- self.selectedProjectIndexChanged.emit(project_idx)
- # Clear the files from the previously-selected project and refresh the files model with the newly-selected-
- # project's files
- self._file_model.clearFiles()
- self.selectedFileIndicesChanged.emit([])
- if 0 <= project_idx < len(self._project_model.items):
- library_project_id = self._project_model.items[project_idx]["libraryProjectId"]
- self.setRetrievingFilesStatus(self.RetrievalStatus.InProgress)
- self._api.getListOfFilesInProject(library_project_id, on_finished = self.getProjectFilesFinished, failed = self.getProjectFilesFailed)
- @pyqtProperty(int, fset = setSelectedProjectIndex, notify = selectedProjectIndexChanged)
- def selectedProjectIndex(self) -> int:
- return self._selected_project_idx
- @pyqtSlot("QList<int>")
- def setSelectedFileIndices(self, file_indices: List[int]) -> None:
- """
- Sets the index of the file which is currently selected in the list of files.
- :param file_indices: The index of the currently selected file
- """
- if file_indices != self._selected_file_indices:
- self._selected_file_indices = file_indices
- self.selectedFileIndicesChanged.emit(file_indices)
- def setProjectFilter(self, new_filter: str) -> None:
- """
- Called when the user wants to change the search filter for projects.
- The filter is not immediately applied. There is some delay to allow the user to finish typing.
- :param new_filter: The new filter that the user wants to apply.
- """
- self._project_filter = new_filter
- self._project_filter_change_timer.start()
- """
- Signal to notify Qt that the applied filter has changed.
- """
- projectFilterChanged = pyqtSignal()
- @pyqtProperty(str, notify = projectFilterChanged, fset = setProjectFilter)
- def projectFilter(self) -> str:
- """
- The current search filter being applied to the project list.
- :return: The current search filter being applied to the project list.
- """
- return self._project_filter
- def _applyProjectFilter(self) -> None:
- """
- Actually apply the current filter to search for projects with the user-defined search string.
- :return:
- """
- self.clear()
- self.projectFilterChanged.emit()
- self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
- @pyqtProperty(QObject, constant = True)
- def digitalFactoryProjectModel(self) -> "DigitalFactoryProjectModel":
- return self._project_model
- @pyqtProperty(QObject, constant = True)
- def digitalFactoryFileModel(self) -> "DigitalFactoryFileModel":
- return self._file_model
- def setHasMoreProjectsToLoad(self, has_more_projects_to_load: bool) -> None:
- """
- Set the value that indicates whether there are more pages of projects that can be loaded from the API
- :param has_more_projects_to_load: Whether there are more pages of projects
- """
- if has_more_projects_to_load != self._has_more_projects_to_load:
- self._has_more_projects_to_load = has_more_projects_to_load
- self.hasMoreProjectsToLoadChanged.emit()
- @pyqtProperty(bool, fset = setHasMoreProjectsToLoad, notify = hasMoreProjectsToLoadChanged)
- def hasMoreProjectsToLoad(self) -> bool:
- """
- :return: whether there are more pages for projects that can be loaded from the API
- """
- return self._has_more_projects_to_load
- @pyqtSlot(str)
- def createLibraryProjectAndSetAsPreselected(self, project_name: Optional[str]) -> None:
- """
- Creates a new project with the given name in the Digital Library.
- :param project_name: The name that will be used for the new project
- """
- if project_name:
- self._api.createNewProject(project_name, self.setProjectAsPreselected, self._createNewLibraryProjectFailed)
- self.setCreatingNewProjectStatus(self.RetrievalStatus.InProgress)
- else:
- Logger.log("w", "No project name provided while attempting to create a new project. Aborting the project creation.")
- def _createNewLibraryProjectFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
- reply_string = bytes(reply.readAll()).decode()
- self._project_creation_error_text = "Something went wrong while creating the new project. Please try again."
- if reply_string:
- reply_dict = json.loads(reply_string)
- if "errors" in reply_dict and len(reply_dict["errors"]) >= 1 and "title" in reply_dict["errors"][0]:
- self._project_creation_error_text = "Error while creating the new project: {}".format(reply_dict["errors"][0]["title"])
- self.projectCreationErrorTextChanged.emit()
- self.setCreatingNewProjectStatus(self.RetrievalStatus.Failed)
- Logger.log("e", "Something went wrong while trying to create a new a project. Error: {}".format(reply_string))
- def setRetrievingProjectsStatus(self, new_status: RetrievalStatus) -> None:
- """
- Sets the status of the "retrieving library projects" http call.
- :param new_status: The new status
- """
- self.retrieving_projects_status = new_status
- self.retrievingProjectsStatusChanged.emit(int(new_status))
- @pyqtProperty(int, fset = setRetrievingProjectsStatus, notify = retrievingProjectsStatusChanged)
- def retrievingProjectsStatus(self) -> int:
- return int(self.retrieving_projects_status)
- def setRetrievingFilesStatus(self, new_status: RetrievalStatus) -> None:
- """
- Sets the status of the "retrieving files list in the selected library project" http call.
- :param new_status: The new status
- """
- self.retrieving_files_status = new_status
- self.retrievingFilesStatusChanged.emit(int(new_status))
- @pyqtProperty(int, fset = setRetrievingFilesStatus, notify = retrievingFilesStatusChanged)
- def retrievingFilesStatus(self) -> int:
- return int(self.retrieving_files_status)
- def setCreatingNewProjectStatus(self, new_status: RetrievalStatus) -> None:
- """
- Sets the status of the "creating new library project" http call.
- :param new_status: The new status
- """
- self.creating_new_project_status = new_status
- self.creatingNewProjectStatusChanged.emit(int(new_status))
- @pyqtProperty(int, fset = setCreatingNewProjectStatus, notify = creatingNewProjectStatusChanged)
- def creatingNewProjectStatus(self) -> int:
- return int(self.creating_new_project_status)
- @staticmethod
- def _onEngineCreated() -> None:
- qmlRegisterUncreatableMetaObject(DigitalFactoryController.staticMetaObject, "DigitalFactory", 1, 0, "RetrievalStatus", "RetrievalStatus is an Enum-only type")
- def _applicationInitializationFinished(self) -> None:
- self._supported_file_types = self._application.getInstance().getMeshFileHandler().getSupportedFileTypesRead()
- # Although Cura supports these, it's super confusing in this context to show them.
- for extension in ["jpg", "jpeg", "png", "bmp", "gif"]:
- if extension in self._supported_file_types:
- del self._supported_file_types[extension]
- @pyqtSlot()
- def openSelectedFiles(self) -> None:
- """ Downloads, then opens all files selected in the Qt frontend open dialog.
- """
- temp_dir = tempfile.mkdtemp()
- if temp_dir is None or temp_dir == "":
- Logger.error("Digital Library: Couldn't create temporary directory to store to-be downloaded files.")
- return
- if self._selected_project_idx < 0 or len(self._selected_file_indices) < 1:
- Logger.error("Digital Library: No project or no file selected on open action.")
- return
- to_erase_on_done_set = {
- os.path.join(temp_dir, self._file_model.getItem(i)["fileName"]).replace('\\', '/')
- for i in self._selected_file_indices}
- def onLoadedCallback(filename_done: str) -> None:
- filename_done = os.path.join(temp_dir, filename_done).replace('\\', '/')
- with self._erase_temp_files_lock:
- if filename_done in to_erase_on_done_set:
- try:
- os.remove(filename_done)
- to_erase_on_done_set.remove(filename_done)
- if len(to_erase_on_done_set) < 1 and os.path.exists(temp_dir):
- os.rmdir(temp_dir)
- except (IOError, OSError) as ex:
- Logger.error("Can't erase temporary (in) {0} because {1}.", temp_dir, str(ex))
- # Save the project id to make sure it will be preselected the next time the user opens the save dialog
- CuraApplication.getInstance().getCurrentWorkspaceInformation().setEntryToStore("digital_factory", "library_project_id", library_project_id)
- # Disconnect the signals so that they are not fired every time another (project) file is loaded
- app.fileLoaded.disconnect(onLoadedCallback)
- app.workspaceLoaded.disconnect(onLoadedCallback)
- app = CuraApplication.getInstance()
- app.fileLoaded.connect(onLoadedCallback) # fired when non-project files are loaded
- app.workspaceLoaded.connect(onLoadedCallback) # fired when project files are loaded
- project_name = self._project_model.getItem(self._selected_project_idx)["displayName"]
- for file_index in self._selected_file_indices:
- file_item = self._file_model.getItem(file_index)
- file_name = file_item["fileName"]
- download_url = file_item["downloadUrl"]
- library_project_id = file_item["libraryProjectId"]
- self._openSelectedFile(temp_dir, project_name, file_name, download_url)
- def _openSelectedFile(self, temp_dir: str, project_name: str, file_name: str, download_url: str) -> None:
- """ Downloads, then opens, the single specified file.
- :param temp_dir: The already created temporary directory where the files will be stored.
- :param project_name: Name of the project the file belongs to (used for error reporting).
- :param file_name: Name of the file to be downloaded and opened (used for error reporting).
- :param download_url: This url will be downloaded, then the downloaded file will be opened in Cura.
- """
- if not download_url:
- Logger.log("e", "No download url for file '{}'".format(file_name))
- return
- progress_message = Message(text = "{0}/{1}".format(project_name, file_name), dismissable = False, lifetime = 0,
- progress = 0, title = "Downloading...")
- progress_message.setProgress(0)
- progress_message.show()
- def progressCallback(rx: int, rt: int) -> None:
- progress_message.setProgress(math.floor(rx * 100.0 / rt))
- def finishedCallback(reply: QNetworkReply) -> None:
- progress_message.hide()
- try:
- with open(os.path.join(temp_dir, file_name), "wb+") as temp_file:
- bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
- while bytes_read:
- temp_file.write(bytes_read)
- bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
- CuraApplication.getInstance().processEvents()
- temp_file_name = temp_file.name
- except IOError as ex:
- Logger.logException("e", "Can't write Digital Library file {0}/{1} download to temp-directory {2}.",
- ex, project_name, file_name, temp_dir)
- getBackwardsCompatibleMessage(
- text = "Failed to write to temporary file for '{}'.".format(file_name),
- title = "File-system error",
- message_type_str="ERROR",
- lifetime = 10
- ).show()
- return
- CuraApplication.getInstance().readLocalFile(
- QUrl.fromLocalFile(temp_file_name), add_to_recent_files = False)
- def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, p = project_name,
- f = file_name) -> None:
- progress_message.hide()
- Logger.error("An error {0} {1} occurred while downloading {2}/{3}".format(str(error), str(reply), p, f))
- getBackwardsCompatibleMessage(
- text = "Failed Digital Library download for '{}'.".format(f),
- title = "Network error {}".format(error),
- message_type_str="ERROR",
- lifetime = 10
- ).show()
- download_manager = HttpRequestManager.getInstance()
- download_manager.get(download_url, callback = finishedCallback, download_progress_callback = progressCallback,
- error_callback = errorCallback, scope = UltimakerCloudScope(CuraApplication.getInstance()))
- def setHasPreselectedProject(self, new_has_preselected_project: bool) -> None:
- if not new_has_preselected_project:
- # The preselected project was the only one in the model, at index 0, so when we set the has_preselected_project to
- # false, we also need to clean it from the projects model
- self._project_model.clearProjects()
- self.setSelectedProjectIndex(-1)
- self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
- self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject)
- self.setRetrievingProjectsStatus(self.RetrievalStatus.InProgress)
- self._has_preselected_project = new_has_preselected_project
- self.preselectedProjectChanged.emit()
- @pyqtProperty(bool, fset = setHasPreselectedProject, notify = preselectedProjectChanged)
- def hasPreselectedProject(self) -> bool:
- return self._has_preselected_project
- def setCanCreateNewLibraryProject(self, can_create_new_library_project: bool) -> None:
- self._user_account_can_create_new_project = can_create_new_library_project
- self.userCanCreateNewLibraryProjectChanged.emit(self._user_account_can_create_new_project)
- @pyqtProperty(bool, fset = setCanCreateNewLibraryProject, notify = userCanCreateNewLibraryProjectChanged)
- def userAccountCanCreateNewLibraryProject(self) -> bool:
- return self._user_account_can_create_new_project
- @pyqtSlot(str, "QStringList")
- def saveFileToSelectedProject(self, filename: str, formats: List[str]) -> None:
- """
- Function triggered whenever the Save button is pressed.
- :param filename: The name (without the extension) that will be used for the files
- :param formats: List of the formats the scene will be exported to. Can include 3mf, ufp, or both
- """
- if self._selected_project_idx == -1:
- Logger.log("e", "No DF Library project is selected.")
- return
- if filename == "":
- Logger.log("w", "The file name cannot be empty.")
- getBackwardsCompatibleMessage(
- text = "Cannot upload file with an empty name to the Digital Library",
- title = "Empty file name provided",
- message_type_str = "ERROR",
- lifetime = 0
- ).show()
- return
- self._saveFileToSelectedProjectHelper(filename, formats)
- def _saveFileToSelectedProjectHelper(self, filename: str, formats: List[str]) -> None:
- # Indicate we have started sending a job (and propagate any user file name changes back to the open project)
- self.uploadStarted.emit(filename if "3mf" in formats else None)
- library_project_id = self._project_model.items[self._selected_project_idx]["libraryProjectId"]
- library_project_name = self._project_model.items[self._selected_project_idx]["displayName"]
- # Use the file upload manager to export and upload the 3mf and/or ufp files to the DF Library project
- self.file_upload_manager = DFFileExportAndUploadManager(file_handlers = self.file_handlers, nodes = cast(List[SceneNode], self.nodes),
- library_project_id = library_project_id,
- library_project_name = library_project_name,
- file_name = filename, formats = formats,
- on_upload_error = self.uploadFileError.emit,
- on_upload_success = self.uploadFileSuccess.emit,
- on_upload_finished = self.uploadFileFinished.emit,
- on_upload_progress = self.uploadFileProgress.emit)
- self.file_upload_manager.start()
- # Save the project id to make sure it will be preselected the next time the user opens the save dialog
- self._current_workspace_information.setEntryToStore("digital_factory", "library_project_id", library_project_id)
- @pyqtProperty(str, notify = projectCreationErrorTextChanged)
- def projectCreationErrorText(self) -> str:
- return self._project_creation_error_text
|