mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-15 12:35:56 +08:00
Merge pull request #16296 from Ultimaker/CURA-10719_show_missing_plugins
Show install missing packages button in open workspace dialog for plugins
This commit is contained in:
commit
401e1eca6a
@ -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()
|
||||
|
@ -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.<br/>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.<br/>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()
|
||||
}
|
||||
]
|
||||
|
@ -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<plugin_id>\w+)@(?P<version>\d+.\d+.\d+)::(?P<value>\w+)")
|
||||
# This regex parses enum values to find if they contain custom
|
||||
# backend engine values. These custom enum values are in the format
|
||||
# PLUGIN::<plugin_id>@<version>::<value>
|
||||
# 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
|
||||
|
||||
|
60
plugins/3MFWriter/tests/TestMFWriter.py
Normal file
60
plugins/3MFWriter/tests/TestMFWriter.py
Normal file
@ -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"
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user