diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py
index afa1deecfd..ed42485691 100644
--- a/plugins/3MFReader/WorkspaceDialog.py
+++ b/plugins/3MFReader/WorkspaceDialog.py
@@ -408,26 +408,27 @@ class WorkspaceDialog(QObject):
@pyqtSlot()
def showMissingMaterialsWarning(self) -> None:
result_message = Message(
- i18n_catalog.i18nc("@info:status", "The material used in this project relies on some material definitions not available in Cura, this might produce undesirable print results. We highly recommend installing the full material package from the Marketplace."),
+ i18n_catalog.i18nc("@info:status",
+ "Some of the packages used in the project file are currently not installed in Cura, this might produce undesirable print results. We highly recommend installing the all required packages from the Marketplace."),
lifetime=0,
- title=i18n_catalog.i18nc("@info:title", "Material profiles not installed"),
+ title=i18n_catalog.i18nc("@info:title", "Some required packages are not installed"),
message_type=Message.MessageType.WARNING
)
result_message.addAction(
- "learn_more",
- name=i18n_catalog.i18nc("@action:button", "Learn more"),
- icon="",
- description="Learn more about project materials.",
- button_align=Message.ActionButtonAlignment.ALIGN_LEFT,
- button_style=Message.ActionButtonStyle.LINK
+ "learn_more",
+ name=i18n_catalog.i18nc("@action:button", "Learn more"),
+ icon="",
+ description=i18n_catalog.i18nc("@label", "Learn more about project packages."),
+ button_align=Message.ActionButtonAlignment.ALIGN_LEFT,
+ button_style=Message.ActionButtonStyle.LINK
)
result_message.addAction(
- "install_materials",
- name=i18n_catalog.i18nc("@action:button", "Install Materials"),
- icon="",
- description="Install missing materials from project file.",
- button_align=Message.ActionButtonAlignment.ALIGN_RIGHT,
- button_style=Message.ActionButtonStyle.DEFAULT
+ "install_packages",
+ name=i18n_catalog.i18nc("@action:button", "Install Packages"),
+ icon="",
+ description=i18n_catalog.i18nc("@label", "Install missing packages from project file."),
+ button_align=Message.ActionButtonAlignment.ALIGN_RIGHT,
+ button_style=Message.ActionButtonStyle.DEFAULT
)
result_message.actionTriggered.connect(self._onMessageActionTriggered)
result_message.show()
diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml
index d1a000bae4..d5f9b1817d 100644
--- a/plugins/3MFReader/WorkspaceDialog.qml
+++ b/plugins/3MFReader/WorkspaceDialog.qml
@@ -364,7 +364,7 @@ UM.Dialog
UM.Label
{
id: warningText
- text: catalog.i18nc("@label", "The material used in this project is currently not installed in Cura.
Install the material profile and reopen the project.")
+ text: catalog.i18nc("@label", "This project contains materials or plugins that are currently not installed in Cura.
Install the missing packages and reopen the project.")
}
}
@@ -404,7 +404,7 @@ UM.Dialog
Cura.PrimaryButton
{
visible: warning
- text: catalog.i18nc("@action:button", "Install missing material")
+ text: catalog.i18nc("@action:button", "Install missing packages")
onClicked: manager.installMissingPackages()
}
]
diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py
index 57c667145e..10b040e01f 100644
--- a/plugins/3MFWriter/ThreeMFWriter.py
+++ b/plugins/3MFWriter/ThreeMFWriter.py
@@ -1,8 +1,9 @@
# Copyright (c) 2015-2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
+import re
-from typing import Optional, cast, List, Dict
+from typing import Optional, cast, List, Dict, Pattern, Set
from UM.Mesh.MeshWriter import MeshWriter
from UM.Math.Vector import Vector
@@ -17,6 +18,7 @@ from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
from cura.CuraApplication import CuraApplication
from cura.CuraPackageManager import CuraPackageManager
+from cura.Settings import CuraContainerStack
from cura.Utils.Threading import call_on_qt_thread
from cura.Snapshot import Snapshot
@@ -175,13 +177,15 @@ class ThreeMFWriter(MeshWriter):
archive.writestr(thumbnail_file, thumbnail_buffer.data())
# Add PNG to content types file
- thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png")
+ thumbnail_type = ET.SubElement(content_types, "Default", Extension="png", ContentType="image/png")
# Add thumbnail relation to _rels/.rels file
- thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/" + THUMBNAIL_PATH, Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
+ thumbnail_relation_element = ET.SubElement(relations_element, "Relationship",
+ Target="/" + THUMBNAIL_PATH, Id="rel1",
+ Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
# Write material metadata
- material_metadata = self._getMaterialPackageMetadata()
- self._storeMetadataJson({"packages": material_metadata}, archive, PACKAGE_METADATA_PATH)
+ packages_metadata = self._getMaterialPackageMetadata() + self._getPluginPackageMetadata()
+ self._storeMetadataJson({"packages": packages_metadata}, archive, PACKAGE_METADATA_PATH)
savitar_scene = Savitar.Scene()
@@ -253,7 +257,64 @@ class ThreeMFWriter(MeshWriter):
metadata_file = zipfile.ZipInfo(path)
# We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
metadata_file.compress_type = zipfile.ZIP_DEFLATED
- archive.writestr(metadata_file, json.dumps(metadata, separators=(", ", ": "), indent=4, skipkeys=True, ensure_ascii=False))
+ archive.writestr(metadata_file,
+ json.dumps(metadata, separators=(", ", ": "), indent=4, skipkeys=True, ensure_ascii=False))
+
+ @staticmethod
+ def _getPluginPackageMetadata() -> List[Dict[str, str]]:
+ """Get metadata for all backend plugins that are used in the project.
+
+ :return: List of material metadata dictionaries.
+ """
+
+ backend_plugin_enum_value_regex = re.compile(
+ r"PLUGIN::(?P\w+)@(?P\d+.\d+.\d+)::(?P\w+)")
+ # This regex parses enum values to find if they contain custom
+ # backend engine values. These custom enum values are in the format
+ # PLUGIN::@::
+ # where
+ # - plugin_id is the id of the plugin
+ # - version is in the semver format
+ # - value is the value of the enum
+
+ plugin_ids = set()
+
+ def addPluginIdsInStack(stack: CuraContainerStack) -> None:
+ for key in stack.getAllKeys():
+ value = str(stack.getProperty(key, "value"))
+ for plugin_id, _version, _value in backend_plugin_enum_value_regex.findall(value):
+ plugin_ids.add(plugin_id)
+
+ # Go through all stacks and find all the plugin id contained in the project
+ global_stack = CuraApplication.getInstance().getMachineManager().activeMachine
+ addPluginIdsInStack(global_stack)
+
+ for container in global_stack.getContainers():
+ addPluginIdsInStack(container)
+
+ for extruder_stack in global_stack.extruderList:
+ addPluginIdsInStack(extruder_stack)
+
+ for container in extruder_stack.getContainers():
+ addPluginIdsInStack(container)
+
+ metadata = {}
+
+ package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
+ for plugin_id in plugin_ids:
+ package_data = package_manager.getInstalledPackageInfo(plugin_id)
+
+ metadata[plugin_id] = {
+ "id": plugin_id,
+ "display_name": package_data.get("display_name") if package_data.get("display_name") else "",
+ "package_version": package_data.get("package_version") if package_data.get("package_version") else "",
+ "sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get(
+ "sdk_version_semver") else "",
+ "type": "plugin",
+ }
+
+ # Storing in a dict and fetching values to avoid duplicates
+ return list(metadata.values())
@staticmethod
def _getMaterialPackageMetadata() -> List[Dict[str, str]]:
@@ -278,7 +339,8 @@ class ThreeMFWriter(MeshWriter):
# Don't export bundled materials
continue
- package_id = package_manager.getMaterialFilePackageId(extruder.material.getFileName(), extruder.material.getMetaDataEntry("GUID"))
+ package_id = package_manager.getMaterialFilePackageId(extruder.material.getFileName(),
+ extruder.material.getMetaDataEntry("GUID"))
package_data = package_manager.getInstalledPackageInfo(package_id)
# We failed to find the package for this material
@@ -286,10 +348,14 @@ class ThreeMFWriter(MeshWriter):
Logger.info(f"Could not find package for material in extruder {extruder.id}, skipping.")
continue
- material_metadata = {"id": package_id,
- "display_name": package_data.get("display_name") if package_data.get("display_name") else "",
- "package_version": package_data.get("package_version") if package_data.get("package_version") else "",
- "sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get("sdk_version_semver") else ""}
+ material_metadata = {
+ "id": package_id,
+ "display_name": package_data.get("display_name") if package_data.get("display_name") else "",
+ "package_version": package_data.get("package_version") if package_data.get("package_version") else "",
+ "sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get(
+ "sdk_version_semver") else "",
+ "type": "material",
+ }
metadata[package_id] = material_metadata
diff --git a/plugins/3MFWriter/tests/TestMFWriter.py b/plugins/3MFWriter/tests/TestMFWriter.py
new file mode 100644
index 0000000000..7bee581105
--- /dev/null
+++ b/plugins/3MFWriter/tests/TestMFWriter.py
@@ -0,0 +1,60 @@
+import sys
+import os.path
+from typing import Dict, Optional
+import pytest
+
+from unittest.mock import patch, MagicMock, PropertyMock
+
+from UM.PackageManager import PackageManager
+from cura.CuraApplication import CuraApplication
+
+sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
+
+import ThreeMFWriter
+
+PLUGIN_ID = "my_plugin"
+DISPLAY_NAME = "MyPlugin"
+PACKAGE_VERSION = "0.0.1"
+SDK_VERSION = "8.0.0"
+
+
+@pytest.fixture
+def package_manager() -> MagicMock:
+ pm = MagicMock(spec=PackageManager)
+ pm.getInstalledPackageInfo.return_value = {
+ "display_name": DISPLAY_NAME,
+ "package_version": PACKAGE_VERSION,
+ "sdk_version_semver": SDK_VERSION
+ }
+ return pm
+
+
+@pytest.fixture
+def machine_manager() -> MagicMock:
+ mm = MagicMock(spec=PackageManager)
+ active_machine = MagicMock()
+ active_machine.getAllKeys.return_value = ["infill_pattern", "layer_height", "material_bed_temperature"]
+ active_machine.getProperty.return_value = f"PLUGIN::{PLUGIN_ID}@{PACKAGE_VERSION}::custom_value"
+ active_machine.getContainers.return_value = []
+ active_machine.extruderList = []
+ mm.activeMachine = active_machine
+ return mm
+
+
+@pytest.fixture
+def application(package_manager, machine_manager):
+ app = MagicMock()
+ app.getPackageManager.return_value = package_manager
+ app.getMachineManager.return_value = machine_manager
+ return app
+
+
+def test_enumParsing(application):
+ with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)):
+ packages_metadata = ThreeMFWriter.ThreeMFWriter._getPluginPackageMetadata()[0]
+
+ assert packages_metadata.get("id") == PLUGIN_ID
+ assert packages_metadata.get("display_name") == DISPLAY_NAME
+ assert packages_metadata.get("package_version") == PACKAGE_VERSION
+ assert packages_metadata.get("sdk_version_semver") == SDK_VERSION
+ assert packages_metadata.get("type") == "plugin"
diff --git a/plugins/Marketplace/MissingPackageList.py b/plugins/Marketplace/MissingPackageList.py
index 385e78b95f..018e977823 100644
--- a/plugins/Marketplace/MissingPackageList.py
+++ b/plugins/Marketplace/MissingPackageList.py
@@ -20,7 +20,6 @@ class MissingPackageList(RemotePackageList):
def __init__(self, packages_metadata: List[Dict[str, str]], parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._packages_metadata: List[Dict[str, str]] = packages_metadata
- self._package_type_filter = "material"
self._search_type = "package_ids"
self._requested_search_string = ",".join(map(lambda package: package["id"], packages_metadata))
@@ -38,7 +37,14 @@ class MissingPackageList(RemotePackageList):
for package_metadata in self._packages_metadata:
if package_metadata["id"] not in returned_packages_ids:
- package = PackageModel.fromIncompletePackageInformation(package_metadata["display_name"], package_metadata["package_version"], self._package_type_filter)
+ package_type = package_metadata["type"] if "type" in package_metadata else "material"
+ # When this feature was originally introduced only missing materials were detected. With the inclusion
+ # of backend plugins this system was extended to also detect missing plugins. With that change the type
+ # of the package was added to the metadata. Project files before this change do not have this type. So
+ # if the type is not present we assume it is a material.
+ package = PackageModel.fromIncompletePackageInformation(package_metadata["display_name"],
+ package_metadata["package_version"],
+ package_type)
self.appendItem({"package": package})
self.itemsChanged.emit()
diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py
index fa909b4120..afc6e0ce73 100644
--- a/plugins/Marketplace/PackageModel.py
+++ b/plugins/Marketplace/PackageModel.py
@@ -87,12 +87,22 @@ class PackageModel(QObject):
self._is_missing_package_information = False
@classmethod
- def fromIncompletePackageInformation(cls, display_name: str, package_version: str, package_type: str) -> "PackageModel":
+ def fromIncompletePackageInformation(cls, display_name: str, package_version: str,
+ package_type: str) -> "PackageModel":
+ description = ""
+ match package_type:
+ case "material":
+ description = catalog.i18nc("@label:label Ultimaker Marketplace is a brand name, don't translate",
+ "The material package associated with the Cura project could not be found on the Ultimaker Marketplace. Use the partial material profile definition stored in the Cura project file at your own risk.")
+ case "plugin":
+ description = catalog.i18nc("@label:label Ultimaker Marketplace is a brand name, don't translate",
+ "The plugin associated with the Cura project could not be found on the Ultimaker Marketplace. As the plugin may be required to slice the project it might not be possible to correctly slice the file.")
+
package_data = {
"display_name": display_name,
"package_version": package_version,
"package_type": package_type,
- "description": catalog.i18nc("@label:label Ultimaker Marketplace is a brand name, don't translate", "The material package associated with the Cura project could not be found on the Ultimaker Marketplace. Use the partial material profile definition stored in the Cura project file at your own risk.")
+ "description": description,
}
package_model = cls(package_data)
package_model.setIsMissingPackageInformation(True)
diff --git a/plugins/Marketplace/resources/qml/InstallMissingPackagesDialog.qml b/plugins/Marketplace/resources/qml/InstallMissingPackagesDialog.qml
index edad18f1a8..7ac0a241e6 100644
--- a/plugins/Marketplace/resources/qml/InstallMissingPackagesDialog.qml
+++ b/plugins/Marketplace/resources/qml/InstallMissingPackagesDialog.qml
@@ -12,7 +12,7 @@ import Cura 1.6 as Cura
Marketplace
{
modality: Qt.ApplicationModal
- title: catalog.i18nc("@title", "Install missing Materials")
+ title: catalog.i18nc("@title", "Install missing packages")
pageContentsSource: "MissingPackages.qml"
showSearchHeader: false
showOnboadBanner: false
diff --git a/plugins/Marketplace/resources/qml/MissingPackages.qml b/plugins/Marketplace/resources/qml/MissingPackages.qml
index 316d048317..a1d29addf9 100644
--- a/plugins/Marketplace/resources/qml/MissingPackages.qml
+++ b/plugins/Marketplace/resources/qml/MissingPackages.qml
@@ -5,7 +5,7 @@ import UM 1.4 as UM
Packages
{
- pageTitle: catalog.i18nc("@header", "Install Materials")
+ pageTitle: catalog.i18nc("@header", "Install Packages")
bannerVisible: false
showUpdateButton: false