Browse Source

Merge pull request #18371 from Ultimaker/CURA-11403_save-PAP

Cura 11403 save pap
Casper Lamboo 1 year ago
parent
commit
58eb62d976

+ 12 - 0
cura/CuraApplication.py

@@ -601,7 +601,9 @@ class CuraApplication(QtApplication):
         preferences.addPreference("mesh/scale_to_fit", False)
         preferences.addPreference("mesh/scale_tiny_meshes", True)
         preferences.addPreference("cura/dialog_on_project_save", True)
+        preferences.addPreference("cura/dialog_on_ucp_project_save", True)
         preferences.addPreference("cura/asked_dialog_on_project_save", False)
+        preferences.addPreference("cura/asked_dialog_on_ucp_project_save", False)
         preferences.addPreference("cura/choice_on_profile_override", "always_ask")
         preferences.addPreference("cura/choice_on_open_project", "always_ask")
         preferences.addPreference("cura/use_multi_build_plate", False)
@@ -1142,6 +1144,16 @@ class CuraApplication(QtApplication):
             self._build_plate_model = BuildPlateModel(self)
         return self._build_plate_model
 
+    @pyqtSlot()
+    def exportUcp(self):
+        writer = self.getMeshFileHandler().getWriter("3MFWriter")
+
+        if writer is None:
+            Logger.warning("3mf writer is not enabled")
+            return
+
+        writer.exportUcp()
+
     def getCuraSceneController(self, *args) -> CuraSceneController:
         if self._cura_scene_controller is None:
             self._cura_scene_controller = CuraSceneController.createCuraSceneController()

+ 38 - 0
plugins/3MFReader/SpecificSettingsModel.py

@@ -0,0 +1,38 @@
+# Copyright (c) 2024 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from PyQt6.QtCore import Qt
+
+from UM.Settings.SettingDefinition import SettingDefinition
+from UM.Qt.ListModel import ListModel
+
+
+class SpecificSettingsModel(ListModel):
+    CategoryRole = Qt.ItemDataRole.UserRole + 1
+    LabelRole = Qt.ItemDataRole.UserRole + 2
+    ValueRole = Qt.ItemDataRole.UserRole + 3
+
+    def __init__(self, parent = None):
+        super().__init__(parent = parent)
+        self.addRoleName(self.CategoryRole, "category")
+        self.addRoleName(self.LabelRole, "label")
+        self.addRoleName(self.ValueRole, "value")
+
+        self._i18n_catalog = None
+
+    def addSettingsFromStack(self, stack, category, settings):
+        for setting, value in settings.items():
+            unit = stack.getProperty(setting, "unit")
+
+            setting_type = stack.getProperty(setting, "type")
+            if setting_type is not None:
+                # This is not very good looking, but will do for now
+                value = SettingDefinition.settingValueToString(setting_type, value) + " " + unit
+            else:
+                value = str(value)
+
+            self.appendItem({
+                "category": category,
+                "label": stack.getProperty(setting, "label"),
+                "value": value
+            })

+ 1 - 1
plugins/3MFReader/ThreeMFReader.py

@@ -41,7 +41,7 @@ class ThreeMFReader(MeshReader):
 
         MimeTypeDatabase.addMimeType(
             MimeType(
-                name = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
+                name="application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
                 comment="3MF",
                 suffixes=["3mf"]
             )

+ 163 - 83
plugins/3MFReader/ThreeMFWorkspaceReader.py

@@ -5,6 +5,7 @@ from configparser import ConfigParser
 import zipfile
 import os
 import json
+import re
 from typing import cast, Dict, List, Optional, Tuple, Any, Set
 
 import xml.etree.ElementTree as ET
@@ -57,6 +58,7 @@ _ignored_machine_network_metadata: Set[str] = {
     "is_abstract_machine"
 }
 
+USER_SETTINGS_PATH = "Cura/user-settings.json"
 
 class ContainerInfo:
     def __init__(self, file_name: Optional[str], serialized: Optional[str], parser: Optional[ConfigParser]) -> None:
@@ -141,10 +143,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
         self._old_new_materials: Dict[str, str] = {}
         self._machine_info = None
 
+        self._load_profile = False
+        self._user_settings: Dict[str, Dict[str, Any]] = {}
+
     def _clearState(self):
         self._id_mapping = {}
         self._old_new_materials = {}
         self._machine_info = None
+        self._load_profile = False
+        self._user_settings = {}
 
     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.
@@ -228,11 +235,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
         self._resolve_strategies = {k: None for k in resolve_strategy_keys}
         containers_found_dict = {k: False for k in resolve_strategy_keys}
 
+        # Check whether the file is a UCP, which changes some import options
+        is_ucp = USER_SETTINGS_PATH in cura_file_names
+
         #
         # Read definition containers
         #
         machine_definition_id = None
-        updatable_machines = []
+        updatable_machines = None if 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)]
@@ -250,7 +260,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
             if definition_container_type == "machine":
                 machine_definition_id = container_id
                 machine_definition_containers = self._container_registry.findDefinitionContainers(id = machine_definition_id)
-                if machine_definition_containers:
+                if machine_definition_containers and updatable_machines is not None:
                     updatable_machines = [machine for machine in self._container_registry.findContainerStacks(type = "machine") if machine.definition == machine_definition_containers[0]]
                 machine_type = definition_container["name"]
                 variant_type_name = definition_container.get("variants_name", variant_type_name)
@@ -597,6 +607,37 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
         package_metadata = self._parse_packages_metadata(archive)
         missing_package_metadata = self._filter_missing_package_metadata(package_metadata)
 
+        # Load the user specifically exported settings
+        self._dialog.exportedSettingModel.clear()
+        if 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)
+                actual_global_stack = CuraApplication.getInstance().getGlobalContainerStack()
+
+                for stack_name, settings in self._user_settings.items():
+                    if stack_name == 'global':
+                        self._dialog.exportedSettingModel.addSettingsFromStack(actual_global_stack, i18n_catalog.i18nc("@label", "Global"), settings)
+                    else:
+                        extruder_match = re.fullmatch('extruder_([0-9]+)', stack_name)
+                        if extruder_match is not None:
+                            extruder_nr = int(extruder_match.group(1))
+                            self._dialog.exportedSettingModel.addSettingsFromStack(any_extruder_stack,
+                                                                                   i18n_catalog.i18nc("@label",
+                                                                                                      "Extruder {0}", extruder_nr + 1),
+                                                                                   settings)
+            except KeyError as e:
+                # If there is no user settings file, it's not a UCP, so notify user of failure.
+                Logger.log("w", "File %s is not a valid UCP.", file_name)
+                message = Message(
+                    i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
+                                       "Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.",
+                                       file_name, str(e)),
+                    title=i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
+                    message_type=Message.MessageType.ERROR)
+                message.show()
+                return WorkspaceReader.PreReadResult.failed
+
         # Show the dialog, informing the user what is about to happen.
         self._dialog.setMachineConflict(machine_conflict)
         self._dialog.setIsPrinterGroup(is_printer_group)
@@ -617,8 +658,11 @@ 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.show()
 
+
         # Choosing the initially selected printer in MachineSelector
         is_networked_machine = False
         is_abstract_machine = False
@@ -648,6 +692,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
         self._dialog.setIsNetworkedMachine(is_networked_machine)
         self._dialog.setIsAbstractMachine(is_abstract_machine)
         self._dialog.setMachineName(machine_name)
+        self._dialog.updateCompatibleMachine()
+        self._dialog.setSelectSameProfileChecked(self._dialog.isCompatibleMachine)
 
         # Block until the dialog is closed.
         self._dialog.waitForClose()
@@ -655,6 +701,8 @@ 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._resolve_strategies = self._dialog.getResult()
         #
         # There can be 3 resolve strategies coming from the dialog:
@@ -690,16 +738,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
         except EnvironmentError as e:
             message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
                                                  "Project file <filename>{0}</filename> is suddenly inaccessible: <message>{1}</message>.", file_name, str(e)),
-                                                 title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
-                                                 message_type = Message.MessageType.ERROR)
+                              title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
+                              message_type = Message.MessageType.ERROR)
             message.show()
             self.setWorkspaceName("")
             return [], {}
         except zipfile.BadZipFile as e:
             message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
                                                  "Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.", file_name, str(e)),
-                                                 title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
-                                                 message_type = Message.MessageType.ERROR)
+                              title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
+                              message_type = Message.MessageType.ERROR)
             message.show()
             self.setWorkspaceName("")
             return [], {}
@@ -761,9 +809,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
             # Find the machine which will be overridden
             global_stacks = self._container_registry.findContainerStacks(id = self._dialog.getMachineToOverride(), type = "machine")
             if not global_stacks:
-                message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!", 
+                message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!",
                                                      "Project file <filename>{0}</filename> is made using profiles that are unknown to this version of UltiMaker Cura.", file_name),
-                                                     message_type = Message.MessageType.ERROR)
+                                  message_type = Message.MessageType.ERROR)
                 message.show()
                 self.setWorkspaceName("")
                 return [], {}
@@ -777,84 +825,89 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
             for stack in extruder_stacks:
                 stack.setNextStack(global_stack, connect_signals = False)
 
-        Logger.log("d", "Workspace loading is checking definitions...")
-        # Get all the definition files & check if they exist. If not, add them.
-        definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
-        for definition_container_file in definition_container_files:
-            container_id = self._stripFileToId(definition_container_file)
+        if self._load_profile:
+            Logger.log("d", "Workspace loading is checking definitions...")
+            # Get all the definition files & check if they exist. If not, add them.
+            definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
+            for definition_container_file in definition_container_files:
+                container_id = self._stripFileToId(definition_container_file)
 
-            definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
-            if not definitions:
-                definition_container = DefinitionContainer(container_id)
-                try:
-                    definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
-                                                     file_name = definition_container_file)
-                except ContainerFormatError:
-                    # We cannot just skip the definition file because everything else later will just break if the
-                    # machine definition cannot be found.
-                    Logger.logException("e", "Failed to deserialize definition file %s in project file %s",
-                                        definition_container_file, file_name)
-                    definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
-                self._container_registry.addContainer(definition_container)
-            Job.yieldThread()
-            QCoreApplication.processEvents()  # Ensure that the GUI does not freeze.
-
-        Logger.log("d", "Workspace loading is checking materials...")
-        # Get all the material files and check if they exist. If not, add them.
-        xml_material_profile = self._getXmlProfileClass()
-        if self._material_container_suffix is None:
-            self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
-        if xml_material_profile:
-            material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
-            for material_container_file in material_container_files:
-                to_deserialize_material = False
-                container_id = self._stripFileToId(material_container_file)
-                need_new_name = False
-                materials = self._container_registry.findInstanceContainers(id = container_id)
-
-                if not materials:
-                    # No material found, deserialize this material later and add it
-                    to_deserialize_material = True
-                else:
-                    material_container = materials[0]
-                    old_material_root_id = material_container.getMetaDataEntry("base_file")
-                    if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id):  # Only create new materials if they are not read only.
-                        to_deserialize_material = True
-
-                        if self._resolve_strategies["material"] == "override":
-                            # Remove the old materials and then deserialize the one from the project
-                            root_material_id = material_container.getMetaDataEntry("base_file")
-                            application.getContainerRegistry().removeContainer(root_material_id)
-                        elif self._resolve_strategies["material"] == "new":
-                            # Note that we *must* deserialize it with a new ID, as multiple containers will be
-                            # auto created & added.
-                            container_id = self.getNewId(container_id)
-                            self._old_new_materials[old_material_root_id] = container_id
-                            need_new_name = True
-
-                if to_deserialize_material:
-                    material_container = xml_material_profile(container_id)
+                definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
+                if not definitions:
+                    definition_container = DefinitionContainer(container_id)
                     try:
-                        material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
-                                                       file_name = container_id + "." + self._material_container_suffix)
+                        definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
+                                                         file_name = definition_container_file)
                     except ContainerFormatError:
-                        Logger.logException("e", "Failed to deserialize material file %s in project file %s",
-                                            material_container_file, file_name)
-                        continue
-                    if need_new_name:
-                        new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName())
-                        material_container.setName(new_name)
-                    material_container.setDirty(True)
-                    self._container_registry.addContainer(material_container)
+                        # We cannot just skip the definition file because everything else later will just break if the
+                        # machine definition cannot be found.
+                        Logger.logException("e", "Failed to deserialize definition file %s in project file %s",
+                                            definition_container_file, file_name)
+                        definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
+                    self._container_registry.addContainer(definition_container)
                 Job.yieldThread()
                 QCoreApplication.processEvents()  # Ensure that the GUI does not freeze.
 
+            Logger.log("d", "Workspace loading is checking materials...")
+            # Get all the material files and check if they exist. If not, add them.
+            xml_material_profile = self._getXmlProfileClass()
+            if self._material_container_suffix is None:
+                self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
+            if xml_material_profile:
+                material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
+                for material_container_file in material_container_files:
+                    to_deserialize_material = False
+                    container_id = self._stripFileToId(material_container_file)
+                    need_new_name = False
+                    materials = self._container_registry.findInstanceContainers(id = container_id)
+
+                    if not materials:
+                        # No material found, deserialize this material later and add it
+                        to_deserialize_material = True
+                    else:
+                        material_container = materials[0]
+                        old_material_root_id = material_container.getMetaDataEntry("base_file")
+                        if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id):  # Only create new materials if they are not read only.
+                            to_deserialize_material = True
+
+                            if self._resolve_strategies["material"] == "override":
+                                # Remove the old materials and then deserialize the one from the project
+                                root_material_id = material_container.getMetaDataEntry("base_file")
+                                application.getContainerRegistry().removeContainer(root_material_id)
+                            elif self._resolve_strategies["material"] == "new":
+                                # Note that we *must* deserialize it with a new ID, as multiple containers will be
+                                # auto created & added.
+                                container_id = self.getNewId(container_id)
+                                self._old_new_materials[old_material_root_id] = container_id
+                                need_new_name = True
+
+                    if to_deserialize_material:
+                        material_container = xml_material_profile(container_id)
+                        try:
+                            material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
+                                                           file_name = container_id + "." + self._material_container_suffix)
+                        except ContainerFormatError:
+                            Logger.logException("e", "Failed to deserialize material file %s in project file %s",
+                                                material_container_file, file_name)
+                            continue
+                        if need_new_name:
+                            new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName())
+                            material_container.setName(new_name)
+                        material_container.setDirty(True)
+                        self._container_registry.addContainer(material_container)
+                    Job.yieldThread()
+                    QCoreApplication.processEvents()  # Ensure that the GUI does not freeze.
+
         if global_stack:
-            # Handle quality changes if any
-            self._processQualityChanges(global_stack)
+            if self._load_profile:
+                # Handle quality changes if any
+                self._processQualityChanges(global_stack)
 
-            # Prepare the machine
-            self._applyChangesToMachine(global_stack, extruder_stack_dict)
+                # Prepare the machine
+                self._applyChangesToMachine(global_stack, extruder_stack_dict)
+            else:
+                # Just clear the settings now, so that we can change the active machine without conflicts
+                self._clearMachineSettings(global_stack, extruder_stack_dict)
 
             Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
             # Actually change the active machine.
@@ -866,6 +919,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
             # To solve this, we schedule _updateActiveMachine() for later so it will have the latest data.
             self._updateActiveMachine(global_stack)
 
+            if not self._load_profile:
+                # Now we have switched, apply the user settings
+                self._applyUserSettings(global_stack, extruder_stack_dict, self._user_settings)
+
         # Load all the nodes / mesh data of the workspace
         nodes = self._3mf_mesh_reader.read(file_name)
         if nodes is None:
@@ -1177,21 +1234,44 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
             material_node = machine_node.variants[extruder_stack.variant.getName()].materials[root_material_id]
             extruder_stack.material = material_node.container
 
-    def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
-        # Clear all first
+    def _clearMachineSettings(self, global_stack, extruder_stack_dict):
         self._clearStack(global_stack)
         for extruder_stack in extruder_stack_dict.values():
             self._clearStack(extruder_stack)
 
+        self._quality_changes_to_apply = None
+        self._quality_type_to_apply = None
+        self._intent_category_to_apply = None
+        self._user_settings_to_apply = None
+
+    def _applyUserSettings(self, global_stack, extruder_stack_dict, user_settings):
+        for stack_name, settings in user_settings.items():
+            if stack_name == 'global':
+                ThreeMFWorkspaceReader._applyUserSettingsOnStack(global_stack, settings)
+            else:
+                extruder_match = re.fullmatch('extruder_([0-9]+)', stack_name)
+                if extruder_match is not None:
+                    extruder_nr = extruder_match.group(1)
+                    if extruder_nr in extruder_stack_dict:
+                        ThreeMFWorkspaceReader._applyUserSettingsOnStack(extruder_stack_dict[extruder_nr], settings)
+
+    @staticmethod
+    def _applyUserSettingsOnStack(stack, user_settings):
+        user_settings_container = stack.userChanges
+
+        for setting_to_import, setting_value in user_settings.items():
+            user_settings_container.setProperty(setting_to_import, 'value', setting_value)
+
+    def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
+        # Clear all first
+        self._clearMachineSettings(global_stack, extruder_stack_dict)
+
         self._applyDefinitionChanges(global_stack, extruder_stack_dict)
         self._applyUserChanges(global_stack, extruder_stack_dict)
         self._applyVariants(global_stack, extruder_stack_dict)
         self._applyMaterials(global_stack, extruder_stack_dict)
 
         # prepare the quality to select
-        self._quality_changes_to_apply = None
-        self._quality_type_to_apply = None
-        self._intent_category_to_apply = None
         if self._machine_info.quality_changes_info is not None:
             self._quality_changes_to_apply = self._machine_info.quality_changes_info.name
         else:

+ 54 - 0
plugins/3MFReader/WorkspaceDialog.py

@@ -22,6 +22,8 @@ import time
 
 from cura.CuraApplication import CuraApplication
 
+from .SpecificSettingsModel import SpecificSettingsModel
+
 i18n_catalog = i18nCatalog("cura")
 
 
@@ -71,6 +73,11 @@ class WorkspaceDialog(QObject):
         self._install_missing_package_dialog: Optional[QObject] = None
         self._is_abstract_machine = False
         self._is_networked_machine = False
+        self._is_compatible_machine = False
+        self._has_visible_select_same_profile = False
+        self._select_same_profile_checked = True
+        self._allow_create_machine = True
+        self._exported_settings_model = SpecificSettingsModel()
 
     machineConflictChanged = pyqtSignal()
     qualityChangesConflictChanged = pyqtSignal()
@@ -94,6 +101,9 @@ class WorkspaceDialog(QObject):
     extrudersChanged = pyqtSignal()
     isPrinterGroupChanged = pyqtSignal()
     missingPackagesChanged = pyqtSignal()
+    isCompatibleMachineChanged = pyqtSignal()
+    hasVisibleSelectSameProfileChanged = pyqtSignal()
+    selectSameProfileCheckedChanged = pyqtSignal()
 
     @pyqtProperty(bool, notify = isPrinterGroupChanged)
     def isPrinterGroup(self) -> bool:
@@ -292,6 +302,50 @@ class WorkspaceDialog(QObject):
     @pyqtSlot(str)
     def setMachineToOverride(self, machine_name: str) -> None:
         self._override_machine = machine_name
+        self.updateCompatibleMachine()
+
+    def updateCompatibleMachine(self):
+        registry = ContainerRegistry.getInstance()
+        containers_expected = registry.findDefinitionContainers(name=self._machine_type)
+        containers_selected = registry.findContainerStacks(id=self._override_machine)
+        if len(containers_expected) == 1 and len(containers_selected) == 1:
+            new_compatible_machine = (containers_expected[0] == containers_selected[0].definition)
+            if new_compatible_machine != self._is_compatible_machine:
+                self._is_compatible_machine = new_compatible_machine
+                self.isCompatibleMachineChanged.emit()
+
+    @pyqtProperty(bool, notify = isCompatibleMachineChanged)
+    def isCompatibleMachine(self) -> bool:
+        return self._is_compatible_machine
+
+    def setHasVisibleSelectSameProfileChanged(self, has_visible_select_same_profile):
+        if has_visible_select_same_profile != self._has_visible_select_same_profile:
+            self._has_visible_select_same_profile = has_visible_select_same_profile
+            self.hasVisibleSelectSameProfileChanged.emit()
+
+    @pyqtProperty(bool, notify = hasVisibleSelectSameProfileChanged)
+    def hasVisibleSelectSameProfile(self):
+        return self._has_visible_select_same_profile
+
+    def setSelectSameProfileChecked(self, select_same_profile_checked):
+        if select_same_profile_checked != self._select_same_profile_checked:
+            self._select_same_profile_checked = select_same_profile_checked
+            self.selectSameProfileCheckedChanged.emit()
+
+    @pyqtProperty(bool, notify = selectSameProfileCheckedChanged, fset = setSelectSameProfileChecked)
+    def selectSameProfileChecked(self):
+        return self._select_same_profile_checked
+
+    def setAllowCreatemachine(self, allow_create_machine):
+        self._allow_create_machine = allow_create_machine
+
+    @pyqtProperty(bool, constant = True)
+    def allowCreateMachine(self):
+        return self._allow_create_machine
+
+    @pyqtProperty(QObject, constant = True)
+    def exportedSettingModel(self):
+        return self._exported_settings_model
 
     @pyqtSlot()
     def closeBackend(self) -> None:

+ 56 - 5
plugins/3MFReader/WorkspaceDialog.qml

@@ -6,7 +6,7 @@ import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.3
 import QtQuick.Window 2.2
 
-import UM 1.5 as UM
+import UM 1.6 as UM
 import Cura 1.1 as Cura
 
 UM.Dialog
@@ -120,13 +120,17 @@ UM.Dialog
 
                         minDropDownWidth: machineSelector.width
 
-                        buttons: [
+                        Component
+                        {
+                            id: componentNewPrinter
+
                             Cura.SecondaryButton
                             {
                                 id: createNewPrinter
                                 text: catalog.i18nc("@button", "Create new")
                                 fixedWidthMode: true
                                 width: parent.width - leftPadding * 1.5
+                                visible: manager.allowCreateMachine
                                 onClicked:
                                 {
                                     toggleContent()
@@ -136,7 +140,9 @@ UM.Dialog
                                     manager.setIsNetworkedMachine(false)
                                 }
                             }
-                        ]
+                        }
+
+                        buttons: manager.allowCreateMachine ? [componentNewPrinter.createObject()] : []
 
                         onSelectPrinter: function(machine)
                         {
@@ -165,26 +171,71 @@ UM.Dialog
                         {
                             leftLabelText: catalog.i18nc("@action:label", "Name")
                             rightLabelText: manager.qualityName
+                            visible: manager.isCompatibleMachine
                         }
 
                         WorkspaceRow
                         {
                             leftLabelText: catalog.i18nc("@action:label", "Intent")
                             rightLabelText: manager.intentName
+                            visible: manager.isCompatibleMachine
                         }
 
                         WorkspaceRow
                         {
                             leftLabelText: catalog.i18nc("@action:label", "Not in profile")
                             rightLabelText: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings)
-                            visible: manager.numUserSettings != 0
+                            visible: manager.numUserSettings != 0 && manager.selectSameProfileChecked && manager.isCompatibleMachine
                         }
 
                         WorkspaceRow
                         {
                             leftLabelText: catalog.i18nc("@action:label", "Derivative from")
                             rightLabelText: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
-                            visible: manager.numSettingsOverridenByQualityChanges != 0
+                            visible: manager.numSettingsOverridenByQualityChanges != 0 && manager.selectSameProfileChecked && manager.isCompatibleMachine
+                        }
+
+                        WorkspaceRow
+                        {
+                            leftLabelText: catalog.i18nc("@action:label", "Specific settings")
+                            rightLabelText: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.exportedSettingModel.rowCount()).arg(manager.exportedSettingModel.rowCount())
+                            buttonText: tableViewSpecificSettings.shouldBeVisible ? catalog.i18nc("@action:button", "Hide settings") : catalog.i18nc("@action:button", "Show settings")
+                            visible: !manager.selectSameProfileChecked || !manager.isCompatibleMachine
+                            onButtonClicked: tableViewSpecificSettings.shouldBeVisible = !tableViewSpecificSettings.shouldBeVisible
+                        }
+
+                        Cura.TableView
+                        {
+                            id: tableViewSpecificSettings
+                            width: parent.width - parent.leftPadding - UM.Theme.getSize("default_margin").width
+                            height: UM.Theme.getSize("card").height
+                            visible: shouldBeVisible && (!manager.selectSameProfileChecked || !manager.isCompatibleMachine)
+                            property bool shouldBeVisible: false
+
+                            columnHeaders:
+                            [
+                                catalog.i18nc("@title:column", "Applies on"),
+                                catalog.i18nc("@title:column", "Setting"),
+                                catalog.i18nc("@title:column", "Value")
+                            ]
+
+                            model: UM.TableModel
+                            {
+                                id: tableModel
+                                headers: ["category", "label", "value"]
+                                rows: manager.exportedSettingModel.items
+                            }
+                        }
+
+                        UM.CheckBox
+                        {
+                            text: catalog.i18nc("@action:checkbox", "Select the same profile")
+                            onEnabledChanged: manager.selectSameProfileChecked = enabled
+                            tooltip: enabled ? "" : catalog.i18nc("@tooltip", "You can use the same profile only if you have the same printer as the project was published with")
+                            visible: manager.hasVisibleSelectSameProfile && manager.isCompatibleMachine
+
+                            checked: manager.selectSameProfileChecked
+                            onCheckedChanged: manager.selectSameProfileChecked = checked
                         }
                     }
 

+ 16 - 4
plugins/3MFReader/WorkspaceRow.qml

@@ -9,26 +9,38 @@ import QtQuick.Window 2.2
 import UM 1.5 as UM
 import Cura 1.1 as Cura
 
-Row
+RowLayout
 {
+    id: root
+
     property alias leftLabelText: leftLabel.text
     property alias rightLabelText: rightLabel.text
+    property alias buttonText: button.text
+    signal buttonClicked
 
     width: parent.width
-    height: visible ? childrenRect.height : 0
 
     UM.Label
     {
         id: leftLabel
         text: catalog.i18nc("@action:label", "Type")
-        width: Math.round(parent.width / 4)
+        Layout.preferredWidth: Math.round(parent.width / 4)
         wrapMode: Text.WordWrap
     }
+
     UM.Label
     {
         id: rightLabel
         text: manager.machineType
-        width: Math.round(parent.width / 3)
         wrapMode: Text.WordWrap
     }
+
+    Cura.TertiaryButton
+    {
+        id: button
+        visible: !text.isEmpty
+        Layout.maximumHeight: leftLabel.implicitHeight
+        Layout.fillWidth: true
+        onClicked: root.buttonClicked()
+    }
 }

+ 5 - 26
plugins/3MFReader/WorkspaceSection.qml

@@ -5,7 +5,7 @@ import QtQuick 2.10
 import QtQuick.Controls 2.3
 
 
-import UM 1.5 as UM
+import UM 1.8 as UM
 
 
 Item
@@ -80,34 +80,13 @@ Item
             sourceComponent: combobox
         }
 
-        MouseArea
+        UM.HelpIcon
         {
-            id: helpIconMouseArea
             anchors.right: parent.right
             anchors.verticalCenter: comboboxLabel.verticalCenter
-            width: childrenRect.width
-            height: childrenRect.height
-            hoverEnabled: true
-
-            UM.ColorImage
-            {
-                width: UM.Theme.getSize("section_icon").width
-                height: width
-
-                visible: comboboxTooltipText != ""
-                source: UM.Theme.getIcon("Help")
-                color: UM.Theme.getColor("text")
-
-                UM.ToolTip
-                {
-                    text: comboboxTooltipText
-                    visible: helpIconMouseArea.containsMouse
-                    targetPoint: Qt.point(parent.x + Math.round(parent.width / 2), parent.y)
-                    x: 0
-                    y: parent.y + parent.height + UM.Theme.getSize("default_margin").height
-                    width: UM.Theme.getSize("tooltip").width
-                }
-            }
+
+            text: comboboxTooltipText
+            visible: comboboxTooltipText != ""
         }
     }
 

+ 38 - 0
plugins/3MFWriter/SettingExport.py

@@ -0,0 +1,38 @@
+#  Copyright (c) 2024 Ultimaker B.V.
+#  Cura is released under the terms of the LGPLv3 or higher.
+
+from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal
+
+
+class SettingExport(QObject):
+
+    def __init__(self, id, name, value, selectable):
+        super().__init__()
+        self.id = id
+        self._name = name
+        self._value = value
+        self._selected = selectable
+        self._selectable = selectable
+
+    @pyqtProperty(str, constant=True)
+    def name(self):
+        return self._name
+
+    @pyqtProperty(str, constant=True)
+    def value(self):
+        return self._value
+
+    selectedChanged = pyqtSignal(bool)
+
+    def setSelected(self, selected):
+        if selected != self._selected:
+            self._selected = selected
+            self.selectedChanged.emit(self._selected)
+
+    @pyqtProperty(bool, fset = setSelected, notify = selectedChanged)
+    def selected(self):
+        return self._selected
+
+    @pyqtProperty(bool, constant=True)
+    def selectable(self):
+        return self._selectable

+ 38 - 0
plugins/3MFWriter/SettingSelection.qml

@@ -0,0 +1,38 @@
+// Copyright (c) 2024 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.10
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.3
+import QtQuick.Window 2.2
+
+import UM 1.8 as UM
+import Cura 1.1 as Cura
+
+RowLayout
+{
+    id: settingSelection
+
+    UM.CheckBox
+    {
+        text: modelData.name
+        Layout.preferredWidth: UM.Theme.getSize("setting").width
+        checked: modelData.selected
+        onClicked: modelData.selected = checked
+        enabled: modelData.selectable
+    }
+
+    UM.Label
+    {
+        text: modelData.value
+    }
+
+    UM.HelpIcon
+    {
+        UM.I18nCatalog { id: catalog; name: "cura" }
+
+        text: catalog.i18nc("@tooltip",
+                            "This setting can't be exported because it depends on the used printer capacities")
+        visible: !modelData.selectable
+    }
+}

Some files were not shown because too many files changed in this diff