diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 720406fbc6..e23ed47ffa 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -5,13 +5,13 @@ import os from typing import Any, cast, Dict, List, Set, Tuple, TYPE_CHECKING, Optional from UM.Logger import Logger +from UM.PluginRegistry import PluginRegistry from cura.CuraApplication import CuraApplication # To find some resource types. from cura.Settings.GlobalStack import GlobalStack from UM.PackageManager import PackageManager # The class we're extending. from UM.Resources import Resources # To find storage paths for some resource types. from UM.i18n import i18nCatalog -from plugins.XmlMaterialProfile.XmlMaterialProfile import XmlMaterialProfile catalog = i18nCatalog("cura") @@ -68,7 +68,8 @@ class CuraPackageManager(PackageManager): with open(root + "/" + file_name, encoding="utf-8") as f: # Make sure the file we found has the same guid as our material # Parsing this xml would be better but the namespace is needed to search it. - parsed_guid = XmlMaterialProfile.getMetadataFromSerialized(f.read(), "GUID") + parsed_guid = PluginRegistry.getInstance().getPluginObject("XmlMaterialProfile").getMetadataFromSerialized( + f.read(), "GUID") if guid == parsed_guid: return package_id diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index ddc7922546..7456ce2b95 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -23,6 +23,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType from UM.Job import Job from UM.Preferences import Preferences +from cura.CuraPackageManager import CuraPackageManager from cura.Machines.ContainerTree import ContainerTree from cura.Settings.CuraStackBuilder import CuraStackBuilder @@ -579,6 +580,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): is_printer_group = True machine_name = group_name + # Getting missing required package ids + package_metadata = self._parse_packages_metadata(archive) + missing_package_metadata = self._filter_missing_package_metadata(package_metadata) + # Show the dialog, informing the user what is about to happen. self._dialog.setMachineConflict(machine_conflict) self._dialog.setIsPrinterGroup(is_printer_group) @@ -599,6 +604,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setExtruders(extruders) self._dialog.setVariantType(variant_type_name) self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity) + self._dialog.setMissingPackagesMetadata(missing_package_metadata) self._dialog.show() # Block until the dialog is closed. @@ -1243,3 +1249,26 @@ class ThreeMFWorkspaceReader(WorkspaceReader): metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"}) for entry in metadata: return entry.text + + @staticmethod + def _parse_packages_metadata(archive: zipfile.ZipFile) -> List[Dict[str, str]]: + try: + package_metadata = json.loads(archive.open("Metadata/packages.json").read().decode("utf-8")) + return package_metadata["packages"] + except Exception: + Logger.error("Failed to load packes metadata from .3mf file") + return [] + + + @staticmethod + def _filter_missing_package_metadata(package_metadata: List[Dict[str, str]]) -> List[Dict[str, str]]: + """Filters out installed packages from package_metadata""" + missing_packages = [] + package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager()) + + for package in package_metadata: + package_id = package["id"] + if not package_manager.isPackageInstalled(package_id): + missing_packages.append(package) + + return missing_packages diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index b63d8b6288..419f1fe69c 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -2,14 +2,18 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import List, Optional, Dict, cast -from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication +from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication, QUrl +from PyQt6.QtGui import QDesktopServices + from UM.FlameProfiler import pyqtSlot from UM.PluginRegistry import PluginRegistry from UM.Application import Application from UM.i18n import i18nCatalog from UM.Settings.ContainerRegistry import ContainerRegistry from cura.Settings.GlobalStack import GlobalStack +from plugins.Marketplace.InstallMissingPackagesDialog import InstallMissingPackageDialog from .UpdatableMachinesModel import UpdatableMachinesModel +from UM.Message import Message import os import threading @@ -23,7 +27,7 @@ i18n_catalog = i18nCatalog("cura") class WorkspaceDialog(QObject): showDialogSignal = pyqtSignal() - def __init__(self, parent = None): + def __init__(self, parent = None) -> None: super().__init__(parent) self._component = None self._context = None @@ -59,6 +63,9 @@ class WorkspaceDialog(QObject): self._objects_on_plate = False self._is_printer_group = False self._updatable_machines_model = UpdatableMachinesModel(self) + self._missing_package_metadata: List[Dict[str, str]] = [] + self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry() + self._install_missing_package_dialog: Optional[QObject] = None machineConflictChanged = pyqtSignal() qualityChangesConflictChanged = pyqtSignal() @@ -79,6 +86,7 @@ class WorkspaceDialog(QObject): variantTypeChanged = pyqtSignal() extrudersChanged = pyqtSignal() isPrinterGroupChanged = pyqtSignal() + missingPackagesChanged = pyqtSignal() @pyqtProperty(bool, notify = isPrinterGroupChanged) def isPrinterGroup(self) -> bool: @@ -274,6 +282,19 @@ class WorkspaceDialog(QObject): self._has_quality_changes_conflict = quality_changes_conflict self.qualityChangesConflictChanged.emit() + def setMissingPackagesMetadata(self, missing_package_metadata: List[Dict[str, str]]) -> None: + self._missing_package_metadata = missing_package_metadata + self.missingPackagesChanged.emit() + + @pyqtProperty("QVariantList", notify=missingPackagesChanged) + def missingPackages(self) -> List[Dict[str, str]]: + return self._missing_package_metadata + + @pyqtSlot() + def installMissingPackages(self) -> None: + self._install_missing_package_dialog = InstallMissingPackageDialog(self._missing_package_metadata, self.showMissingMaterialsWarning) + self._install_missing_package_dialog.show() + def getResult(self) -> Dict[str, Optional[str]]: if "machine" in self._result and self.updatableMachinesModel.count <= 1: self._result["machine"] = None @@ -360,6 +381,41 @@ class WorkspaceDialog(QObject): time.sleep(1 / 50) QCoreApplication.processEvents() # Ensure that the GUI does not freeze. + @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."), + lifetime=0, + title=i18n_catalog.i18nc("@info:title", "Material profiles 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 + ) + 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 + ) + result_message.actionTriggered.connect(self._onMessageActionTriggered) + result_message.show() + + def _onMessageActionTriggered(self, message: Message, sync_message_action: str) -> None: + if sync_message_action == "install_materials": + self.installMissingPackages() + message.hide() + elif sync_message_action == "learn_more": + QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360011968360-Using-the-Ultimaker-Marketplace")) + + def __show(self) -> None: if self._view is None: self._createViewFromQML() diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 4c384b306b..d0250abfd8 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -17,7 +17,7 @@ UM.Dialog minimumWidth: UM.Theme.getSize("popup_dialog").width minimumHeight: UM.Theme.getSize("popup_dialog").height width: minimumWidth - + margin: UM.Theme.getSize("default_margin").width property int comboboxHeight: UM.Theme.getSize("default_margin").height onClosing: manager.notifyClosed() @@ -31,337 +31,220 @@ UM.Dialog } } - Item + Flickable { - id: dialogSummaryItem + clip: true width: parent.width - height: childrenRect.height - anchors.margins: 10 * screenScaleFactor + height: parent.height + contentHeight: dialogSummaryItem.height + ScrollBar.vertical: UM.ScrollBar { id: verticalScrollBar } - UM.I18nCatalog + Item { - id: catalog - name: "cura" - } - - ListModel - { - id: resolveStrategiesModel - // Instead of directly adding the list elements, we add them afterwards. - // This is because it's impossible to use setting function results to be bound to listElement properties directly. - // See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties - Component.onCompleted: - { - append({"key": "override", "label": catalog.i18nc("@action:ComboBox Update/override existing profile", "Update existing")}); - append({"key": "new", "label": catalog.i18nc("@action:ComboBox Save settings in a new profile", "Create new")}); - } - } - - Column - { - width: parent.width + id: dialogSummaryItem + width: verticalScrollBar.visible ? parent.width - verticalScrollBar.width - UM.Theme.getSize("default_margin").width : parent.width height: childrenRect.height - spacing: UM.Theme.getSize("default_margin").height + anchors.margins: 10 * screenScaleFactor + + UM.I18nCatalog + { + id: catalog + name: "cura" + } + + ListModel + { + id: resolveStrategiesModel + // Instead of directly adding the list elements, we add them afterwards. + // This is because it's impossible to use setting function results to be bound to listElement properties directly. + // See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties + Component.onCompleted: + { + append({"key": "override", "label": catalog.i18nc("@action:ComboBox Update/override existing profile", "Update existing")}); + append({"key": "new", "label": catalog.i18nc("@action:ComboBox Save settings in a new profile", "Create new")}); + } + } Column { width: parent.width height: childrenRect.height + spacing: UM.Theme.getSize("default_margin").height - UM.Label + Column { - id: titleLabel - text: catalog.i18nc("@action:title", "Summary - Cura Project") - font: UM.Theme.getFont("large") - } - - Rectangle - { - id: separator - color: UM.Theme.getColor("text") width: parent.width - height: UM.Theme.getSize("default_lining").height - } - } + height: childrenRect.height - Item - { - width: parent.width - height: childrenRect.height - - UM.TooltipArea - { - id: machineResolveStrategyTooltip - anchors.top: parent.top - anchors.right: parent.right - width: (parent.width / 3) | 0 - height: visible ? comboboxHeight : 0 - visible: base.visible && machineResolveComboBox.model.count > 1 - text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?") - Cura.ComboBox + UM.Label { - id: machineResolveComboBox - model: manager.updatableMachinesModel - visible: machineResolveStrategyTooltip.visible - textRole: "displayName" + id: titleLabel + text: catalog.i18nc("@action:title", "Summary - Cura Project") + font: UM.Theme.getFont("large") + } + + Rectangle + { + id: separator + color: UM.Theme.getColor("text") width: parent.width - height: UM.Theme.getSize("button").height - onCurrentIndexChanged: - { - if (model.getItem(currentIndex).id == "new" - && model.getItem(currentIndex).type == "default_option") - { - manager.setResolveStrategy("machine", "new") - } - else - { - manager.setResolveStrategy("machine", "override") - manager.setMachineToOverride(model.getItem(currentIndex).id) - } - } + height: UM.Theme.getSize("default_lining").height + } + } - onVisibleChanged: - { - if (!visible) {return} + Item + { + width: parent.width + height: childrenRect.height - currentIndex = 0 - // If the project printer exists in Cura, set it as the default dropdown menu option. - // No need to check object 0, which is the "Create new" option - for (var i = 1; i < model.count; i++) + UM.TooltipArea + { + id: machineResolveStrategyTooltip + anchors.top: parent.top + anchors.right: parent.right + width: (parent.width / 3) | 0 + height: visible ? comboboxHeight : 0 + visible: base.visible && machineResolveComboBox.model.count > 1 + text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?") + Cura.ComboBox + { + id: machineResolveComboBox + model: manager.updatableMachinesModel + visible: machineResolveStrategyTooltip.visible + textRole: "displayName" + width: parent.width + height: UM.Theme.getSize("button").height + onCurrentIndexChanged: { - if (model.getItem(i).name == manager.machineName) + if (model.getItem(currentIndex).id == "new" + && model.getItem(currentIndex).type == "default_option") { - currentIndex = i - break + manager.setResolveStrategy("machine", "new") + } + else + { + manager.setResolveStrategy("machine", "override") + manager.setMachineToOverride(model.getItem(currentIndex).id) } } - // The project printer does not exist in Cura. If there is at least one printer of the same - // type, select the first one, else set the index to "Create new" - if (currentIndex == 0 && model.count > 1) + + onVisibleChanged: { - currentIndex = 1 + if (!visible) {return} + + currentIndex = 0 + // If the project printer exists in Cura, set it as the default dropdown menu option. + // No need to check object 0, which is the "Create new" option + for (var i = 1; i < model.count; i++) + { + if (model.getItem(i).name == manager.machineName) + { + currentIndex = i + break + } + } + // The project printer does not exist in Cura. If there is at least one printer of the same + // type, select the first one, else set the index to "Create new" + if (currentIndex == 0 && model.count > 1) + { + currentIndex = 1 + } } } } - } - Column - { - width: parent.width - height: childrenRect.height - - UM.Label - { - id: printer_settings_label - text: catalog.i18nc("@action:label", "Printer settings") - font: UM.Theme.getFont("default_bold") - } - - Row + Column { width: parent.width height: childrenRect.height UM.Label { - text: catalog.i18nc("@action:label", "Type") - width: (parent.width / 3) | 0 - } - UM.Label - { - text: manager.machineType - width: (parent.width / 3) | 0 - } - } - - Row - { - width: parent.width - height: childrenRect.height - - UM.Label - { - text: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name") - width: (parent.width / 3) | 0 - } - UM.Label - { - text: manager.machineName - width: (parent.width / 3) | 0 - wrapMode: Text.WordWrap - } - } - } - } - - Item - { - width: parent.width - height: childrenRect.height - - UM.TooltipArea - { - anchors.right: parent.right - anchors.top: parent.top - width: (parent.width / 3) | 0 - height: visible ? comboboxHeight : 0 - visible: manager.qualityChangesConflict - text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?") - Cura.ComboBox - { - model: resolveStrategiesModel - textRole: "label" - id: qualityChangesResolveComboBox - width: parent.width - height: UM.Theme.getSize("button").height - onActivated: - { - manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key) - } - } - } - - Column - { - width: parent.width - height: childrenRect.height - - UM.Label - { - text: catalog.i18nc("@action:label", "Profile settings") - font: UM.Theme.getFont("default_bold") - } - - Row - { - width: parent.width - height: childrenRect.height - - UM.Label - { - text: catalog.i18nc("@action:label", "Name") - width: (parent.width / 3) | 0 - } - UM.Label - { - text: manager.qualityName - width: (parent.width / 3) | 0 - wrapMode: Text.WordWrap - } - } - - Row - { - width: parent.width - height: childrenRect.height - - UM.Label - { - text: catalog.i18nc("@action:label", "Intent") - width: (parent.width / 3) | 0 - } - UM.Label - { - text: manager.intentName - width: (parent.width / 3) | 0 - wrapMode: Text.WordWrap - } - } - - Row - { - width: parent.width - height: childrenRect.height - - UM.Label - { - text: catalog.i18nc("@action:label", "Not in profile") - visible: manager.numUserSettings != 0 - width: (parent.width / 3) | 0 - } - UM.Label - { - text: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings) - visible: manager.numUserSettings != 0 - width: (parent.width / 3) | 0 - } - } - - Row - { - width: parent.width - height: childrenRect.height - - UM.Label - { - text: catalog.i18nc("@action:label", "Derivative from") - visible: manager.numSettingsOverridenByQualityChanges != 0 - width: (parent.width / 3) | 0 - } - UM.Label - { - text: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges) - width: (parent.width / 3) | 0 - visible: manager.numSettingsOverridenByQualityChanges != 0 - wrapMode: Text.WordWrap - } - } - } - } - - Item - { - width: parent.width - height: childrenRect.height - - UM.TooltipArea - { - id: materialResolveTooltip - anchors.right: parent.right - anchors.top: parent.top - width: (parent.width / 3) | 0 - height: visible ? comboboxHeight : 0 - visible: manager.materialConflict - text: catalog.i18nc("@info:tooltip", "How should the conflict in the material be resolved?") - Cura.ComboBox - { - model: resolveStrategiesModel - textRole: "label" - id: materialResolveComboBox - width: parent.width - height: UM.Theme.getSize("button").height - onActivated: - { - manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key) - } - } - } - - Column - { - width: parent.width - height: childrenRect.height - Row - { - height: childrenRect.height - width: parent.width - spacing: UM.Theme.getSize("narrow_margin").width - - UM.Label - { - text: catalog.i18nc("@action:label", "Material settings") + id: printer_settings_label + text: catalog.i18nc("@action:label", "Printer settings") font: UM.Theme.getFont("default_bold") - width: (parent.width / 3) | 0 } - } - Repeater - { - model: manager.materialLabels - delegate: Row + Row { width: parent.width height: childrenRect.height + + UM.Label + { + text: catalog.i18nc("@action:label", "Type") + width: (parent.width / 3) | 0 + } + UM.Label + { + text: manager.machineType + width: (parent.width / 3) | 0 + } + } + + Row + { + width: parent.width + height: childrenRect.height + + UM.Label + { + text: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name") + width: (parent.width / 3) | 0 + } + UM.Label + { + text: manager.machineName + width: (parent.width / 3) | 0 + wrapMode: Text.WordWrap + } + } + } + } + + Item + { + width: parent.width + height: childrenRect.height + + UM.TooltipArea + { + anchors.right: parent.right + anchors.top: parent.top + width: (parent.width / 3) | 0 + height: visible ? comboboxHeight : 0 + visible: manager.qualityChangesConflict + text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?") + Cura.ComboBox + { + model: resolveStrategiesModel + textRole: "label" + id: qualityChangesResolveComboBox + width: parent.width + height: UM.Theme.getSize("button").height + onActivated: + { + manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key) + } + } + } + + Column + { + width: parent.width + height: childrenRect.height + + UM.Label + { + text: catalog.i18nc("@action:label", "Profile settings") + font: UM.Theme.getFont("default_bold") + } + + Row + { + width: parent.width + height: childrenRect.height + UM.Label { text: catalog.i18nc("@action:label", "Name") @@ -369,76 +252,250 @@ UM.Dialog } UM.Label { - text: modelData + text: manager.qualityName width: (parent.width / 3) | 0 wrapMode: Text.WordWrap } } + + Row + { + width: parent.width + height: childrenRect.height + + UM.Label + { + text: catalog.i18nc("@action:label", "Intent") + width: (parent.width / 3) | 0 + } + UM.Label + { + text: manager.intentName + width: (parent.width / 3) | 0 + wrapMode: Text.WordWrap + } + } + + Row + { + width: parent.width + height: childrenRect.height + + UM.Label + { + text: catalog.i18nc("@action:label", "Not in profile") + visible: manager.numUserSettings != 0 + width: (parent.width / 3) | 0 + } + UM.Label + { + text: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings) + visible: manager.numUserSettings != 0 + width: (parent.width / 3) | 0 + } + } + + Row + { + width: parent.width + height: childrenRect.height + + UM.Label + { + text: catalog.i18nc("@action:label", "Derivative from") + visible: manager.numSettingsOverridenByQualityChanges != 0 + width: (parent.width / 3) | 0 + } + UM.Label + { + text: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges) + width: (parent.width / 3) | 0 + visible: manager.numSettingsOverridenByQualityChanges != 0 + wrapMode: Text.WordWrap + } + } } } - } - Column - { - width: parent.width - height: childrenRect.height - - UM.Label + Item { - text: catalog.i18nc("@action:label", "Setting visibility") - font: UM.Theme.getFont("default_bold") + width: parent.width + height: childrenRect.height + + UM.TooltipArea + { + id: materialResolveTooltip + anchors.right: parent.right + anchors.top: parent.top + width: (parent.width / 3) | 0 + height: visible ? comboboxHeight : 0 + visible: manager.materialConflict + text: catalog.i18nc("@info:tooltip", "How should the conflict in the material be resolved?") + Cura.ComboBox + { + model: resolveStrategiesModel + textRole: "label" + id: materialResolveComboBox + width: parent.width + height: UM.Theme.getSize("button").height + onActivated: + { + manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key) + } + } + } + + Column + { + width: parent.width + height: childrenRect.height + Row + { + height: childrenRect.height + width: parent.width + spacing: UM.Theme.getSize("narrow_margin").width + + UM.Label + { + text: catalog.i18nc("@action:label", "Material settings") + font: UM.Theme.getFont("default_bold") + width: (parent.width / 3) | 0 + } + } + + Repeater + { + model: manager.materialLabels + delegate: Row + { + width: parent.width + height: childrenRect.height + UM.Label + { + text: catalog.i18nc("@action:label", "Name") + width: (parent.width / 3) | 0 + } + UM.Label + { + text: modelData + width: (parent.width / 3) | 0 + wrapMode: Text.WordWrap + } + } + } + } } + + Column + { + width: parent.width + height: childrenRect.height + + UM.Label + { + text: catalog.i18nc("@action:label", "Setting visibility") + font: UM.Theme.getFont("default_bold") + } + Row + { + width: parent.width + height: childrenRect.height + UM.Label + { + text: catalog.i18nc("@action:label", "Mode") + width: (parent.width / 3) | 0 + } + UM.Label + { + text: manager.activeMode + width: (parent.width / 3) | 0 + } + } + Row + { + width: parent.width + height: childrenRect.height + visible: manager.hasVisibleSettingsField + UM.Label + { + text: catalog.i18nc("@action:label", "Visible settings:") + width: (parent.width / 3) | 0 + } + UM.Label + { + text: catalog.i18nc("@action:label", "%1 out of %2" ).arg(manager.numVisibleSettings).arg(manager.totalNumberOfSettings) + width: (parent.width / 3) | 0 + } + } + } + Row { width: parent.width height: childrenRect.height - UM.Label + visible: manager.hasObjectsOnPlate + UM.ColorImage { - text: catalog.i18nc("@action:label", "Mode") - width: (parent.width / 3) | 0 + width: warningLabel.height + height: width + source: UM.Theme.getIcon("Information") + color: UM.Theme.getColor("text") } UM.Label { - text: manager.activeMode - width: (parent.width / 3) | 0 - } - } - Row - { - width: parent.width - height: childrenRect.height - visible: manager.hasVisibleSettingsField - UM.Label - { - text: catalog.i18nc("@action:label", "Visible settings:") - width: (parent.width / 3) | 0 - } - UM.Label - { - text: catalog.i18nc("@action:label", "%1 out of %2" ).arg(manager.numVisibleSettings).arg(manager.totalNumberOfSettings) - width: (parent.width / 3) | 0 + id: warningLabel + text: catalog.i18nc("@action:warning", "Loading a project will clear all models on the build plate.") } } } + } + } - Row + property bool warning: manager.missingPackages.length > 0 + + footerComponent: Rectangle + { + color: warning ? UM.Theme.getColor("warning") : "transparent" + anchors.bottom: parent.bottom + width: parent.width + height: childrenRect.height + 2 * base.margin + + Column + { + height: childrenRect.height + spacing: base.margin + + anchors.margins: base.margin + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + + RowLayout { - width: parent.width + id: warningRow height: childrenRect.height - visible: manager.hasObjectsOnPlate + visible: warning + spacing: base.margin UM.ColorImage { - width: warningLabel.height - height: width - source: UM.Theme.getIcon("Information") - color: UM.Theme.getColor("text") + width: UM.Theme.getSize("extruder_icon").width + height: UM.Theme.getSize("extruder_icon").height + source: UM.Theme.getIcon("Warning") } + UM.Label { - id: warningLabel - text: catalog.i18nc("@action:warning", "Loading a project will clear all models on the build plate.") + id: warningText + text: "The material used in this project is currently not installed in Cura.
Install the material profile and reopen the project." } } + + Loader + { + width: parent.width + height: childrenRect.height + sourceComponent: buttonRow + } } } @@ -447,13 +504,30 @@ UM.Dialog rightButtons: [ Cura.TertiaryButton { + visible: !warning text: catalog.i18nc("@action:button", "Cancel") onClicked: reject() }, Cura.PrimaryButton { + visible: !warning text: catalog.i18nc("@action:button", "Open") onClicked: accept() + }, + Cura.TertiaryButton + { + visible: warning + text: catalog.i18nc("@action:button", "Open project anyway") + onClicked: { + manager.showMissingMaterialsWarning(); + accept(); + } + }, + Cura.PrimaryButton + { + visible: warning + text: catalog.i18nc("@action:button", "Install missing material") + onClicked: manager.installMissingPackages() } ] diff --git a/plugins/Marketplace/InstallMissingPackagesDialog.py b/plugins/Marketplace/InstallMissingPackagesDialog.py new file mode 100644 index 0000000000..282dc54492 --- /dev/null +++ b/plugins/Marketplace/InstallMissingPackagesDialog.py @@ -0,0 +1,66 @@ +import os + +from PyQt6.QtCore import QObject, pyqtSignal, pyqtProperty, QUrl +from PyQt6.QtGui import QDesktopServices +from typing import Optional, List, Dict, cast, Callable +from cura.CuraApplication import CuraApplication +from UM.PluginRegistry import PluginRegistry +from cura.CuraPackageManager import CuraPackageManager +from UM.Message import Message +from UM.i18n import i18nCatalog + +from UM.FlameProfiler import pyqtSlot +from plugins.Marketplace.MissingPackageList import MissingPackageList + +i18n_catalog = i18nCatalog("cura") + +class InstallMissingPackageDialog(QObject): + """Dialog used to display packages that need to be installed to load 3mf file materials""" + def __init__(self, packages_metadata: List[Dict[str, str]], show_missing_materials_warning: Callable[[], None]) -> None: + """Initialize + + :param packages_metadata: List of dictionaries containing information about missing packages. + """ + super().__init__() + + self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry() + self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager()) + self._package_manager.installedPackagesChanged.connect(self.checkIfRestartNeeded) + + self._dialog: Optional[QObject] = None + self._restart_needed = False + self._package_metadata: List[Dict[str, str]] = packages_metadata + + self._package_model: MissingPackageList = MissingPackageList(packages_metadata) + self._show_missing_materials_warning = show_missing_materials_warning + + def show(self) -> None: + plugin_path = self._plugin_registry.getPluginPath("Marketplace") + if plugin_path is None: + plugin_path = os.path.dirname(__file__) + + # create a QML component for the license dialog + license_dialog_component_path = os.path.join(plugin_path, "resources", "qml", "InstallMissingPackagesDialog.qml") + self._dialog = CuraApplication.getInstance().createQmlComponent(license_dialog_component_path, {"manager": self}) + self._dialog.show() + + def checkIfRestartNeeded(self) -> None: + if self._dialog is None: + return + + self._restart_needed = self._package_manager.hasPackagesToRemoveOrInstall + self.showRestartChanged.emit() + + showRestartChanged = pyqtSignal() + + @pyqtProperty(bool, notify=showRestartChanged) + def showRestartNotification(self) -> bool: + return self._restart_needed + + @pyqtProperty(QObject) + def model(self) -> MissingPackageList: + return self._package_model + + @pyqtSlot() + def showMissingMaterialsWarning(self) -> None: + self._show_missing_materials_warning() diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py index 171e3f915b..e856245c3d 100644 --- a/plugins/Marketplace/Marketplace.py +++ b/plugins/Marketplace/Marketplace.py @@ -103,6 +103,9 @@ class Marketplace(Extension, QObject): self.setTabShown(1) def checkIfRestartNeeded(self) -> None: + if self._window is None: + return + if self._package_manager.hasPackagesToRemoveOrInstall or \ cast(PluginRegistry, self._plugin_registry).getCurrentSessionActivationChangedPlugins(): self._restart_needed = True diff --git a/plugins/Marketplace/MissingPackageList.py b/plugins/Marketplace/MissingPackageList.py new file mode 100644 index 0000000000..385e78b95f --- /dev/null +++ b/plugins/Marketplace/MissingPackageList.py @@ -0,0 +1,46 @@ +# Copyright (c) 2022 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import Optional, TYPE_CHECKING, Dict, List + +from .Constants import PACKAGES_URL +from .PackageModel import PackageModel +from .RemotePackageList import RemotePackageList +from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication + +from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API. +from UM.i18n import i18nCatalog + +if TYPE_CHECKING: + from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal + +catalog = i18nCatalog("cura") + +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)) + + def _parseResponse(self, reply: "QNetworkReply") -> None: + super()._parseResponse(reply) + + # At the end of the list we want to show some information about packages the user is missing that can't be found + # This will add cards with some information about the missing packages + if not self.hasMore: + self._addPackagesMissingFromRequest() + + def _addPackagesMissingFromRequest(self) -> None: + """Create cards for packages the user needs to install that could not be found""" + returned_packages_ids = [item["package"].packageId for item in self._items] + + 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) + self.appendItem({"package": package}) + + self.itemsChanged.emit() + + diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index bd1b0681fc..078bfc879e 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -84,6 +84,20 @@ class PackageModel(QObject): self._is_busy = False + self._is_missing_package_information = False + + @classmethod + def fromIncompletePackageInformation(cls, display_name: str, package_version: str, package_type: str) -> "PackageModel": + package_data = { + "display_name": display_name, + "package_version": package_version, + "package_type": package_type, + "description": "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." + } + package_model = cls(package_data) + package_model.setIsMissingPackageInformation(True) + return package_model + @pyqtSlot() def _processUpdatedPackages(self): self.setCanUpdate(self._package_manager.checkIfPackageCanUpdate(self._package_id)) @@ -385,3 +399,14 @@ class PackageModel(QObject): def canUpdate(self) -> bool: """Flag indicating if the package can be updated""" return self._can_update + + isMissingPackageInformationChanged = pyqtSignal() + + def setIsMissingPackageInformation(self, isMissingPackageInformation: bool) -> None: + self._is_missing_package_information = isMissingPackageInformation + self.isMissingPackageInformationChanged.emit() + + @pyqtProperty(bool, notify=isMissingPackageInformationChanged) + def isMissingPackageInformation(self) -> bool: + """Flag indicating if the package can be updated""" + return self._is_missing_package_information diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py index c20efabbc2..d06d2c64c5 100644 --- a/plugins/Marketplace/RemotePackageList.py +++ b/plugins/Marketplace/RemotePackageList.py @@ -28,6 +28,7 @@ class RemotePackageList(PackageList): self._package_type_filter = "" self._requested_search_string = "" self._current_search_string = "" + self._search_type = "search" self._request_url = self._initialRequestUrl() self._ongoing_requests["get_packages"] = None self.isLoadingChanged.connect(self._onLoadingChanged) @@ -100,7 +101,7 @@ class RemotePackageList(PackageList): if self._package_type_filter != "": request_url += f"&package_type={self._package_type_filter}" if self._current_search_string != "": - request_url += f"&search={self._current_search_string}" + request_url += f"&{self._search_type}={self._current_search_string}" return request_url def _parseResponse(self, reply: "QNetworkReply") -> None: diff --git a/plugins/Marketplace/resources/qml/InstallMissingPackagesDialog.qml b/plugins/Marketplace/resources/qml/InstallMissingPackagesDialog.qml new file mode 100644 index 0000000000..edad18f1a8 --- /dev/null +++ b/plugins/Marketplace/resources/qml/InstallMissingPackagesDialog.qml @@ -0,0 +1,21 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Window 2.2 + +import UM 1.5 as UM +import Cura 1.6 as Cura + +Marketplace +{ + modality: Qt.ApplicationModal + title: catalog.i18nc("@title", "Install missing Materials") + pageContentsSource: "MissingPackages.qml" + showSearchHeader: false + showOnboadBanner: false + + onClosing: manager.showMissingMaterialsWarning() +} diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml index d925f265d9..2a3c5f69ce 100644 --- a/plugins/Marketplace/resources/qml/Marketplace.qml +++ b/plugins/Marketplace/resources/qml/Marketplace.qml @@ -16,6 +16,10 @@ Window signal searchStringChanged(string new_search) + property alias showOnboadBanner: onBoardBanner.visible + property alias showSearchHeader: searchHeader.visible + property alias pageContentsSource: content.source + minimumWidth: UM.Theme.getSize("modal_window_minimum").width minimumHeight: UM.Theme.getSize("modal_window_minimum").height width: minimumWidth @@ -86,6 +90,7 @@ Window OnboardBanner { + id: onBoardBanner visible: content.item && content.item.bannerVisible text: content.item && content.item.bannerText icon: content.item && content.item.bannerIcon @@ -100,6 +105,7 @@ Window // Search & Top-Level Tabs Item { + id: searchHeader implicitHeight: childrenRect.height implicitWidth: parent.width - 2 * UM.Theme.getSize("default_margin").width Layout.alignment: Qt.AlignHCenter @@ -186,7 +192,7 @@ Window { text: catalog.i18nc("@info", "Search in the browser") iconSource: UM.Theme.getIcon("LinkExternal") - visible: pageSelectionTabBar.currentItem.hasSearch + visible: pageSelectionTabBar.currentItem.hasSearch && searchHeader.visible isIconOnRightSide: true height: fontMetrics.height textFont: fontMetrics.font diff --git a/plugins/Marketplace/resources/qml/MissingPackages.qml b/plugins/Marketplace/resources/qml/MissingPackages.qml new file mode 100644 index 0000000000..316d048317 --- /dev/null +++ b/plugins/Marketplace/resources/qml/MissingPackages.qml @@ -0,0 +1,15 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import UM 1.4 as UM + +Packages +{ + pageTitle: catalog.i18nc("@header", "Install Materials") + + bannerVisible: false + showUpdateButton: false + showInstallButton: true + + model: manager.model +} diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml index 52254a478f..b74dad0712 100644 --- a/plugins/Marketplace/resources/qml/PackageCard.qml +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -18,6 +18,8 @@ Rectangle height: childrenRect.height color: UM.Theme.getColor("main_background") radius: UM.Theme.getSize("default_radius").width + border.color: packageData.isMissingPackageInformation ? UM.Theme.getColor("warning") : "transparent" + border.width: packageData.isMissingPackageInformation ? UM.Theme.getSize("default_lining").width : 0 PackageCardHeader { diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml index 00d107f4fc..6c9d533bb5 100644 --- a/plugins/Marketplace/resources/qml/PackageCardHeader.qml +++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml @@ -19,6 +19,8 @@ Item property bool showInstallButton: false property bool showUpdateButton: false + property string missingPackageReadMoreUrl: "https://ultimaker.atlassian.net/wiki/spaces/SD/pages/1231916580/Campaign+links+from+Cura+to+the+Ultimaker+domain" + width: parent.width height: UM.Theme.getSize("card").height @@ -87,6 +89,14 @@ Item Layout.preferredWidth: parent.width Layout.preferredHeight: childrenRect.height + UM.StatusIcon + { + width: UM.Theme.getSize("section_icon").width + UM.Theme.getSize("narrow_margin").width + height: UM.Theme.getSize("section_icon").height + status: UM.StatusIcon.Status.WARNING + visible: packageData.isMissingPackageInformation + } + UM.Label { text: packageData.displayName @@ -109,6 +119,7 @@ Item Button { id: externalLinkButton + visible: !packageData.isMissingPackageInformation // For some reason if i set padding, they don't match up. If i set all of them explicitly, it does work? leftPadding: UM.Theme.getSize("narrow_margin").width @@ -155,6 +166,7 @@ Item UM.Label { id: authorBy + visible: !packageData.isMissingPackageInformation Layout.alignment: Qt.AlignCenter text: catalog.i18nc("@label Is followed by the name of an author", "By") @@ -165,6 +177,7 @@ Item // clickable author name Item { + visible: !packageData.isMissingPackageInformation Layout.fillWidth: true implicitHeight: authorBy.height Layout.alignment: Qt.AlignTop @@ -182,10 +195,29 @@ Item } } + Item + { + visible: packageData.isMissingPackageInformation + Layout.fillWidth: true + implicitHeight: readMoreButton.height + Layout.alignment: Qt.AlignTop + Cura.TertiaryButton + { + id: readMoreButton + text: catalog.i18nc("@button:label", "Learn More") + leftPadding: 0 + rightPadding: 0 + iconSource: UM.Theme.getIcon("LinkExternal") + isIconOnRightSide: true + + onClicked: Qt.openUrlExternally(missingPackageReadMoreUrl) + } + } + ManageButton { id: enableManageButton - visible: showDisableButton && packageData.isInstalled && !packageData.isToBeInstalled && packageData.packageType != "material" + visible: showDisableButton && packageData.isInstalled && !packageData.isToBeInstalled && packageData.packageType != "material" && !packageData.isMissingPackageInformation enabled: !packageData.busy button_style: !packageData.isActive @@ -199,7 +231,7 @@ Item ManageButton { id: installManageButton - visible: showInstallButton && (packageData.canDowngrade || !packageData.isBundled) + visible: showInstallButton && (packageData.canDowngrade || !packageData.isBundled) && !packageData.isMissingPackageInformation enabled: !packageData.busy busy: packageData.busy button_style: !(packageData.isInstalled || packageData.isToBeInstalled) @@ -229,7 +261,7 @@ Item ManageButton { id: updateManageButton - visible: showUpdateButton && packageData.canUpdate + visible: showUpdateButton && packageData.canUpdate && !packageData.isMissingPackageInformation enabled: !packageData.busy busy: packageData.busy Layout.alignment: Qt.AlignTop diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml index 70ff7de195..6e83f1e33b 100644 --- a/plugins/Marketplace/resources/qml/Packages.qml +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -62,8 +62,11 @@ ListView hoverEnabled: true onClicked: { - packages.selectedPackage = model.package; - contextStack.push(packageDetailsComponent); + if (!model.package.isMissingPackageInformation) + { + packages.selectedPackage = model.package; + contextStack.push(packageDetailsComponent); + } } PackageCard diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index e0b156dc08..fd8118306b 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -169,7 +169,10 @@ class ClusterApiClient: """ def parse() -> None: - self._anti_gc_callbacks.remove(parse) + try: + self._anti_gc_callbacks.remove(parse) + except ValueError: # Already removed asynchronously. + return # Then the rest of the function is also already executed. # Don't try to parse the reply if we didn't get one if reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) is None: diff --git a/resources/qml/ColorDialog.qml b/resources/qml/ColorDialog.qml index 49adcf0cca..e5216a3404 100644 --- a/resources/qml/ColorDialog.qml +++ b/resources/qml/ColorDialog.qml @@ -29,7 +29,7 @@ UM.Dialog // the size of the dialog ourselves. // Ugly workaround for windows having overlapping elements due to incorrect dialog width minimumWidth: content.width + (Qt.platform.os == "windows" ? 4 * margin : 2 * margin) - minimumHeight: content.height + buttonArea.height + (Qt.platform.os == "windows" ? 5 * margin : 3 * margin) + minimumHeight: content.height + footer.height + (Qt.platform.os == "windows" ? 5 * margin : 3 * margin) property alias color: colorInput.text property var swatchColors: [