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:
Remco Burema 2023-08-04 10:43:12 +02:00 committed by GitHub
commit 401e1eca6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 176 additions and 33 deletions

View File

@ -408,24 +408,25 @@ class WorkspaceDialog(QObject):
@pyqtSlot() @pyqtSlot()
def showMissingMaterialsWarning(self) -> None: def showMissingMaterialsWarning(self) -> None:
result_message = Message( 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, 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 message_type=Message.MessageType.WARNING
) )
result_message.addAction( result_message.addAction(
"learn_more", "learn_more",
name=i18n_catalog.i18nc("@action:button", "Learn more"), name=i18n_catalog.i18nc("@action:button", "Learn more"),
icon="", icon="",
description="Learn more about project materials.", description=i18n_catalog.i18nc("@label", "Learn more about project packages."),
button_align=Message.ActionButtonAlignment.ALIGN_LEFT, button_align=Message.ActionButtonAlignment.ALIGN_LEFT,
button_style=Message.ActionButtonStyle.LINK button_style=Message.ActionButtonStyle.LINK
) )
result_message.addAction( result_message.addAction(
"install_materials", "install_packages",
name=i18n_catalog.i18nc("@action:button", "Install Materials"), name=i18n_catalog.i18nc("@action:button", "Install Packages"),
icon="", icon="",
description="Install missing materials from project file.", description=i18n_catalog.i18nc("@label", "Install missing packages from project file."),
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT, button_align=Message.ActionButtonAlignment.ALIGN_RIGHT,
button_style=Message.ActionButtonStyle.DEFAULT button_style=Message.ActionButtonStyle.DEFAULT
) )

View File

@ -364,7 +364,7 @@ UM.Dialog
UM.Label UM.Label
{ {
id: warningText 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 Cura.PrimaryButton
{ {
visible: warning visible: warning
text: catalog.i18nc("@action:button", "Install missing material") text: catalog.i18nc("@action:button", "Install missing packages")
onClicked: manager.installMissingPackages() onClicked: manager.installMissingPackages()
} }
] ]

View File

@ -1,8 +1,9 @@
# Copyright (c) 2015-2022 Ultimaker B.V. # Copyright (c) 2015-2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import json 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.Mesh.MeshWriter import MeshWriter
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
@ -17,6 +18,7 @@ from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.CuraPackageManager import CuraPackageManager from cura.CuraPackageManager import CuraPackageManager
from cura.Settings import CuraContainerStack
from cura.Utils.Threading import call_on_qt_thread from cura.Utils.Threading import call_on_qt_thread
from cura.Snapshot import Snapshot from cura.Snapshot import Snapshot
@ -177,11 +179,13 @@ class ThreeMFWriter(MeshWriter):
# Add PNG to content types file # 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 # 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 # Write material metadata
material_metadata = self._getMaterialPackageMetadata() packages_metadata = self._getMaterialPackageMetadata() + self._getPluginPackageMetadata()
self._storeMetadataJson({"packages": material_metadata}, archive, PACKAGE_METADATA_PATH) self._storeMetadataJson({"packages": packages_metadata}, archive, PACKAGE_METADATA_PATH)
savitar_scene = Savitar.Scene() savitar_scene = Savitar.Scene()
@ -253,7 +257,64 @@ class ThreeMFWriter(MeshWriter):
metadata_file = zipfile.ZipInfo(path) 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) # 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 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 @staticmethod
def _getMaterialPackageMetadata() -> List[Dict[str, str]]: def _getMaterialPackageMetadata() -> List[Dict[str, str]]:
@ -278,7 +339,8 @@ class ThreeMFWriter(MeshWriter):
# Don't export bundled materials # Don't export bundled materials
continue 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) package_data = package_manager.getInstalledPackageInfo(package_id)
# We failed to find the package for this material # 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.") Logger.info(f"Could not find package for material in extruder {extruder.id}, skipping.")
continue continue
material_metadata = {"id": package_id, material_metadata = {
"id": package_id,
"display_name": package_data.get("display_name") if package_data.get("display_name") else "", "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 "", "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 ""} "sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get(
"sdk_version_semver") else "",
"type": "material",
}
metadata[package_id] = material_metadata metadata[package_id] = material_metadata

View 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"

View File

@ -20,7 +20,6 @@ class MissingPackageList(RemotePackageList):
def __init__(self, packages_metadata: List[Dict[str, str]], parent: Optional["QObject"] = None) -> None: def __init__(self, packages_metadata: List[Dict[str, str]], parent: Optional["QObject"] = None) -> None:
super().__init__(parent) super().__init__(parent)
self._packages_metadata: List[Dict[str, str]] = packages_metadata self._packages_metadata: List[Dict[str, str]] = packages_metadata
self._package_type_filter = "material"
self._search_type = "package_ids" self._search_type = "package_ids"
self._requested_search_string = ",".join(map(lambda package: package["id"], packages_metadata)) 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: for package_metadata in self._packages_metadata:
if package_metadata["id"] not in returned_packages_ids: 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.appendItem({"package": package})
self.itemsChanged.emit() self.itemsChanged.emit()

View File

@ -87,12 +87,22 @@ class PackageModel(QObject):
self._is_missing_package_information = False self._is_missing_package_information = False
@classmethod @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 = { package_data = {
"display_name": display_name, "display_name": display_name,
"package_version": package_version, "package_version": package_version,
"package_type": package_type, "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 = cls(package_data)
package_model.setIsMissingPackageInformation(True) package_model.setIsMissingPackageInformation(True)

View File

@ -12,7 +12,7 @@ import Cura 1.6 as Cura
Marketplace Marketplace
{ {
modality: Qt.ApplicationModal modality: Qt.ApplicationModal
title: catalog.i18nc("@title", "Install missing Materials") title: catalog.i18nc("@title", "Install missing packages")
pageContentsSource: "MissingPackages.qml" pageContentsSource: "MissingPackages.qml"
showSearchHeader: false showSearchHeader: false
showOnboadBanner: false showOnboadBanner: false

View File

@ -5,7 +5,7 @@ import UM 1.4 as UM
Packages Packages
{ {
pageTitle: catalog.i18nc("@header", "Install Materials") pageTitle: catalog.i18nc("@header", "Install Packages")
bannerVisible: false bannerVisible: false
showUpdateButton: false showUpdateButton: false