From f3c49e494ebb74163c4a2c502d1b981bdcc5d6e3 Mon Sep 17 00:00:00 2001 From: Saumya Jain Date: Thu, 29 Feb 2024 15:45:13 +0100 Subject: [PATCH] adding option of opening model as UCP or normal project file CURA-11403 --- cura/CuraActions.py | 10 +- cura/CuraApplication.py | 12 ++ plugins/3MFReader/ThreeMFWorkspaceReader.py | 16 ++- plugins/3MFWriter/ThreeMFWriter.py | 17 ++- resources/qml/Cura.qml | 63 ++++++++--- .../AskOpenAsProjectOrUcpOrImportModel.qml | 104 ++++++++++++++++++ 6 files changed, 187 insertions(+), 35 deletions(-) create mode 100644 resources/qml/Dialogs/AskOpenAsProjectOrUcpOrImportModel.qml diff --git a/cura/CuraActions.py b/cura/CuraActions.py index e33ce8123d..9612e473b8 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -1,6 +1,6 @@ # Copyright (c) 2023 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. - +import zipfile from typing import List, cast from PyQt6.QtCore import QObject, QUrl, pyqtSignal, pyqtProperty @@ -33,6 +33,7 @@ from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOper from UM.Logger import Logger from UM.Scene.SceneNode import SceneNode +USER_SETTINGS_PATH = "Cura/user-settings.json" class CuraActions(QObject): def __init__(self, parent: QObject = None) -> None: @@ -195,6 +196,13 @@ class CuraActions(QObject): operation.addOperation(SetObjectExtruderOperation(node, extruder_id)) operation.push() + @pyqtSlot(str, result = bool) + def isProjectUcp(self, file_url) -> bool: + file_name = QUrl(file_url).toLocalFile() + archive = zipfile.ZipFile(file_name, "r") + cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] + return USER_SETTINGS_PATH in cura_file_names + @pyqtSlot(int) def setBuildPlateForSelection(self, build_plate_nr: int) -> None: Logger.log("d", "Setting build plate number... %d" % build_plate_nr) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index c32017371f..00e6304c0a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1979,6 +1979,18 @@ class CuraApplication(QtApplication): openProjectFile = pyqtSignal(QUrl, bool, arguments = ["project_file", "add_to_recent_files"]) # Emitted when a project file is about to open. + @pyqtSlot(QUrl, bool) + def readLocalUcpFile(self, file: QUrl, add_to_recent_files: bool = True): + + file_name = QUrl(file).toLocalFile() + workspace_reader = self.getWorkspaceFileHandler() + if workspace_reader is None: + Logger.log("w", "Workspace reader not found") + return + + workspace_reader.getReaderForFile(file_name).setOpenAsUcp(True) + workspace_reader.readLocalFile(file, add_to_recent_files) + @pyqtSlot(QUrl, str, bool) @pyqtSlot(QUrl, str) @pyqtSlot(QUrl) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 25e2afa8bd..0e527590f5 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -117,6 +117,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._supported_extensions = [".3mf"] self._dialog = WorkspaceDialog() self._3mf_mesh_reader = None + self._is_ucp = False self._container_registry = ContainerRegistry.getInstance() # suffixes registered with the MimeTypes don't start with a dot '.' @@ -153,6 +154,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._load_profile = False self._user_settings = {} + def setOpenAsUcp(self, openAsUcp: bool): + self._is_ucp = openAsUcp + def getNewId(self, old_id: str): """Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results. @@ -242,7 +246,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Read definition containers # machine_definition_id = None - updatable_machines = None if is_ucp else [] + updatable_machines = None if self._is_ucp else [] machine_definition_container_count = 0 extruder_definition_container_count = 0 definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] @@ -609,7 +613,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Load the user specifically exported settings self._dialog.exportedSettingModel.clear() - if is_ucp: + if self._is_ucp: try: self._user_settings = json.loads(archive.open("Cura/user-settings.json").read().decode("utf-8")) any_extruder_stack = ExtruderManager.getInstance().getExtruderStack(0) @@ -658,8 +662,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setVariantType(variant_type_name) self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity) self._dialog.setMissingPackagesMetadata(missing_package_metadata) - self._dialog.setHasVisibleSelectSameProfileChanged(is_ucp) - self._dialog.setAllowCreatemachine(not is_ucp) + self._dialog.setHasVisibleSelectSameProfileChanged(self._is_ucp) + self._dialog.setAllowCreatemachine(not self._is_ucp) self._dialog.show() @@ -701,7 +705,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled - self._load_profile = not is_ucp or (self._dialog.selectSameProfileChecked and self._dialog.isCompatibleMachine) + self._load_profile = not self._is_ucp self._resolve_strategies = self._dialog.getResult() # @@ -717,7 +721,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if key not in containers_found_dict or strategy is not None: continue self._resolve_strategies[key] = "override" if containers_found_dict[key] else "new" - + self._is_ucp = False return WorkspaceReader.PreReadResult.accepted @call_on_qt_thread diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 5583059a2f..3389941ed8 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -135,16 +135,13 @@ class ThreeMFWriter(MeshWriter): stack = um_node.callDecoration("getStack") if stack is not None: changed_setting_keys = stack.getTop().getAllKeys() - - if exported_settings is None: - # Ensure that we save the extruder used for this object in a multi-extrusion setup - if stack.getProperty("machine_extruder_count", "value") > 1: - changed_setting_keys.add("extruder_nr") - - # Get values for all changed settings & save them. - for key in changed_setting_keys: - savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value"))) - else: + # Ensure that we save the extruder used for this object in a multi-extrusion setup + if stack.getProperty("machine_extruder_count", "value") > 1: + changed_setting_keys.add("extruder_nr") + # Get values for all changed settings & save them. + for key in changed_setting_keys: + savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value"))) + if exported_settings is not None: # We want to export only the specified settings if um_node.getName() in exported_settings: model_exported_settings = exported_settings[um_node.getName()] diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 4983363946..b01cd192c3 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -701,24 +701,34 @@ UM.MainWindow if (hasProjectFile) { - var projectFile = projectFileUrlList[0]; - - // check preference - var choice = UM.Preferences.getValue("cura/choice_on_open_project"); - if (choice == "open_as_project") + var projectFile = projectFileUrlList[0] + var is_ucp = CuraActions.isProjectUcp(projectFile); + print("this file is ucp", is_ucp); + if (is_ucp) { - openFilesIncludingProjectsDialog.loadProjectFile(projectFile); + askOpenAsProjectOrUcpOrImportModelsDialog.fileUrl = projectFile; + askOpenAsProjectOrUcpOrImportModelsDialog.addToRecent = true; + askOpenAsProjectOrUcpOrImportModelsDialog.show(); } - else if (choice == "open_as_model") + else { - openFilesIncludingProjectsDialog.loadModelFiles([projectFile].slice()); - } - else // always ask - { - // ask whether to open as project or as models - askOpenAsProjectOrModelsDialog.fileUrl = projectFile; - askOpenAsProjectOrModelsDialog.addToRecent = true; - askOpenAsProjectOrModelsDialog.show(); + // check preference + var choice = UM.Preferences.getValue("cura/choice_on_open_project"); + if (choice == "open_as_project") + { + openFilesIncludingProjectsDialog.loadProjectFile(projectFile); + } + else if (choice == "open_as_model") + { + openFilesIncludingProjectsDialog.loadModelFiles([projectFile].slice()); + } + else // always ask + { + // ask whether to open as project or as models + askOpenAsProjectOrModelsDialog.fileUrl = projectFile; + askOpenAsProjectOrModelsDialog.addToRecent = true; + askOpenAsProjectOrModelsDialog.show(); + } } } else @@ -769,14 +779,31 @@ UM.MainWindow id: askOpenAsProjectOrModelsDialog } + AskOpenAsProjectOrUcpOrImportModel + { + id: askOpenAsProjectOrUcpOrImportModelsDialog + } + Connections { target: CuraApplication function onOpenProjectFile(project_file, add_to_recent_files) { - askOpenAsProjectOrModelsDialog.fileUrl = project_file; - askOpenAsProjectOrModelsDialog.addToRecent = add_to_recent_files; - askOpenAsProjectOrModelsDialog.show(); + var is_ucp = CuraActions.isProjectUcp(project_file); + print("this file is ucp", is_ucp); + if (is_ucp) + { + + askOpenAsProjectOrUcpOrImportModelsDialog.fileUrl = project_file; + askOpenAsProjectOrUcpOrImportModelsDialog.addToRecent = add_to_recent_files; + askOpenAsProjectOrUcpOrImportModelsDialog.show(); + } + else + { + askOpenAsProjectOrModelsDialog.fileUrl = project_file; + askOpenAsProjectOrModelsDialog.addToRecent = add_to_recent_files; + askOpenAsProjectOrModelsDialog.show(); + } } } diff --git a/resources/qml/Dialogs/AskOpenAsProjectOrUcpOrImportModel.qml b/resources/qml/Dialogs/AskOpenAsProjectOrUcpOrImportModel.qml new file mode 100644 index 0000000000..9791a3e451 --- /dev/null +++ b/resources/qml/Dialogs/AskOpenAsProjectOrUcpOrImportModel.qml @@ -0,0 +1,104 @@ +// Copyright (c) 2022 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 + +import UM 1.5 as UM +import Cura 1.0 as Cura + + +UM.Dialog +{ + // This dialog asks the user whether he/she wants to open a project file as a project or import models. + id: base + + title: catalog.i18nc("@title:window", "Open Universal Cura Project (UCP) file") + width: UM.Theme.getSize("small_popup_dialog").width + height: UM.Theme.getSize("small_popup_dialog").height + backgroundColor: UM.Theme.getColor("main_background") + + maximumHeight: height + maximumWidth: width + minimumHeight: maximumHeight + minimumWidth: maximumWidth + + modality: Qt.WindowModal + + property var fileUrl + property var addToRecent: true //Whether to add this file to the recent files list after reading it. + + // load the entire project + function loadProjectFile() { + + UM.WorkspaceFileHandler.readLocalFile(base.fileUrl, base.addToRecent); + + base.hide() + } + + // load the project file as separated models + function loadModelFiles() { + CuraApplication.readLocalFile(base.fileUrl, "open_as_model", base.addToRecent) + + base.hide() + } + + // load the project file as Universal cura project + function loadUcpFiles() { + CuraApplication.readLocalUcpFile(base.fileUrl, base.addToRecent) + + base.hide() + } + + // override UM.Dialog accept + function accept () { + + // when hitting 'enter', we always open as project unless open_as_model was explicitly stored as preference + if (openAsPreference == "open_as_model") { + loadModelFiles() + } else if (openAsPreference == "open_as_ucp"){ + loadUcpFiles() + }else { + loadProjectFile() + } + } + + Column + { + anchors.fill: parent + spacing: UM.Theme.getSize("default_margin").height + + UM.Label + { + id: questionText + width: parent.width + text: catalog.i18nc("@text:window", "This is a Cura Universal project file. Would you like to open it as a Cura project or Cura Universal Project or import the models from it?") + wrapMode: Text.WordWrap + } + } + + onAccepted: loadProjectFile() + onRejected: loadModelFiles() + + buttonSpacing: UM.Theme.getSize("thin_margin").width + + rightButtons: + [ + Cura.SecondaryButton + { + text: catalog.i18nc("@action:button", "Open as project") + onClicked: loadProjectFile() + }, + Cura.PrimaryButton + { + text: catalog.i18nc("@action:button", "Open as UCP") + onClicked: loadUcpFiles() + }, + Cura.SecondaryButton + { + text: catalog.i18nc("@action:button", "Import models") + onClicked: loadModelFiles() + } + ] +}