Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/Ultimaker/Cura.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/DigitalLibrary/src/DigitalFactoryController.py')
-rw-r--r--plugins/DigitalLibrary/src/DigitalFactoryController.py563
1 files changed, 563 insertions, 0 deletions
diff --git a/plugins/DigitalLibrary/src/DigitalFactoryController.py b/plugins/DigitalLibrary/src/DigitalFactoryController.py
new file mode 100644
index 0000000000..cd20479611
--- /dev/null
+++ b/plugins/DigitalLibrary/src/DigitalFactoryController.py
@@ -0,0 +1,563 @@
+# Copyright (c) 2021 Ultimaker B.V.
+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
+
+from PyQt5.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, Q_ENUMS, QUrl
+from PyQt5.QtNetwork import QNetworkReply
+from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType
+
+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 .DFFileExportAndUploadManager import DFFileExportAndUploadManager
+from .DigitalFactoryApiClient import DigitalFactoryApiClient
+from .DigitalFactoryFileModel import DigitalFactoryFileModel
+from .DigitalFactoryFileResponse import DigitalFactoryFileResponse
+from .DigitalFactoryProjectModel import DigitalFactoryProjectModel
+from .DigitalFactoryProjectResponse import DigitalFactoryProjectResponse
+
+
+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
+
+
+class DFRetrievalStatus(QObject):
+ """
+ Used as an intermediate QObject that registers the RetrievalStatus as a recognizable enum in QML, so that it can
+ be used within QML objects as DigitalFactory.RetrievalStatus.<status>
+ """
+
+ Q_ENUMS(RetrievalStatus)
+
+
+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()
+
+ 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
+ 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._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."
+
+ # 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 = RetrievalStatus.Idle
+ self.retrieving_projects_status = RetrievalStatus.Idle
+ self.creating_new_project_status = RetrievalStatus.Idle
+
+ self._application.engineCreatedSignal.connect(self._onEngineCreated)
+ self._application.initializationFinished.connect(self._applicationInitializationFinished)
+
+ def clear(self) -> None:
+ self._project_model.clearProjects()
+ self._api.clear()
+ self._has_preselected_project = False
+ self.preselectedProjectChanged.emit()
+
+ self.setRetrievingFilesStatus(RetrievalStatus.Idle)
+ self.setRetrievingProjectsStatus(RetrievalStatus.Idle)
+ self.setCreatingNewProjectStatus(RetrievalStatus.Idle)
+
+ self.setSelectedProjectIndex(-1)
+
+ 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
+ """
+ subscriptions = [] # type: Optional[List[Dict[str, Any]]]
+ if self._account.userProfile:
+ subscriptions = self._account.userProfile.get("subscriptions", [])
+ return len(subscriptions) > 0
+
+ def initialize(self, preselected_project_id: Optional[str] = None) -> None:
+ self.clear()
+
+ if self._account.isLoggedIn and self.userAccountHasLibraryAccess():
+ self.setRetrievingProjectsStatus(RetrievalStatus.InProgress)
+ if preselected_project_id:
+ self._api.getProject(preselected_project_id, on_finished = self.setProjectAsPreselected, failed = self._onGetProjectFailed)
+ else:
+ self._api.getProjectsFirstPage(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(RetrievalStatus.Success)
+ self.setCreatingNewProjectStatus(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(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(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(RetrievalStatus.Success)
+
+ def _onGetProjectsFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
+ """
+ Error function, called whenever the retrieval of projects fails.
+ """
+ self.setRetrievingProjectsStatus(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(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.
+ """
+ Logger.log("w", "Failed to retrieve the list of files in project '{}' from the Digital Library".format(self._project_model._projects[self._selected_project_idx]))
+ self.setRetrievingFilesStatus(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(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)
+
+ @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(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(RetrievalStatus.Failed)
+ Logger.log("e", "Something went wrong while trying to create a new a project. Error: {}".format(reply_string))
+
+ # The new_status type is actually "RetrievalStatus" but since the RetrievalStatus cannot be an enum, we leave it as int
+ def setRetrievingProjectsStatus(self, new_status: int) -> 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(new_status)
+
+ @pyqtProperty(int, fset = setRetrievingProjectsStatus, notify = retrievingProjectsStatusChanged)
+ def retrievingProjectsStatus(self) -> int:
+ return self.retrieving_projects_status
+
+ # The new_status type is actually "RetrievalStatus" but since the RetrievalStatus cannot be an enum, we leave it as int
+ def setRetrievingFilesStatus(self, new_status: int) -> 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(new_status)
+
+ @pyqtProperty(int, fset = setRetrievingFilesStatus, notify = retrievingFilesStatusChanged)
+ def retrievingFilesStatus(self) -> int:
+ return self.retrieving_files_status
+
+ # The new_status type is actually "RetrievalStatus" but since the RetrievalStatus cannot be an enum, we leave it as int
+ def setCreatingNewProjectStatus(self, new_status: int) -> 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(new_status)
+
+ @pyqtProperty(int, fset = setCreatingNewProjectStatus, notify = creatingNewProjectStatusChanged)
+ def creatingNewProjectStatus(self) -> int:
+ return self.creating_new_project_status
+
+ @staticmethod
+ def _onEngineCreated() -> None:
+ qmlRegisterUncreatableType(DFRetrievalStatus, "DigitalFactory", 1, 0, "RetrievalStatus", "Could not create RetrievalStatus enum type")
+
+ def _applicationInitializationFinished(self) -> None:
+ self._supported_file_types = self._application.getInstance().getMeshFileHandler().getSupportedFileTypesRead()
+
+ @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: A list of the indices of the currently selected files
+ """
+ self._selected_file_indices = file_indices
+ self.selectedFileIndicesChanged.emit(file_indices)
+
+ @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)
+ Message(
+ text = "Failed to write to temporary file for '{}'.".format(file_name),
+ title = "File-system 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))
+ Message(
+ text = "Failed Digital Library download for '{}'.".format(f),
+ title = "Network error {}".format(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(on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
+ self.setRetrievingProjectsStatus(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
+
+ @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.")
+ Message(text = "Cannot upload file with an empty name to the Digital Library", title = "Empty file name provided", lifetime = 0).show()
+ return
+
+ self._saveFileToSelectedProjectHelper(filename, formats)
+
+ def _saveFileToSelectedProjectHelper(self, filename: str, formats: List[str]) -> None:
+ # Indicate we have started sending a job.
+ self.uploadStarted.emit()
+
+ 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 = 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)
+
+ # 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