123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387 |
- # Copyright (c) 2021 Ultimaker B.V.
- # Cura is released under the terms of the LGPLv3 or higher.
- import re
- from enum import Enum
- from typing import Any, cast, Dict, List, Optional
- from PyQt6.QtCore import pyqtProperty, QObject, pyqtSignal, pyqtSlot
- from PyQt6.QtQml import QQmlEngine
- from cura.CuraApplication import CuraApplication
- from cura.CuraPackageManager import CuraPackageManager
- from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with.
- from UM.i18n import i18nCatalog # To translate placeholder names if data is not present.
- from UM.Logger import Logger
- from UM.PluginRegistry import PluginRegistry
- catalog = i18nCatalog("cura")
- class PackageModel(QObject):
- """
- Represents a package, containing all the relevant information to be displayed about a package.
- """
- def __init__(self, package_data: Dict[str, Any], section_title: Optional[str] = None, parent: Optional[QObject] = None) -> None:
- """
- Constructs a new model for a single package.
- :param package_data: The data received from the Marketplace API about the package to create.
- :param section_title: If the packages are to be categorized per section provide the section_title
- :param parent: The parent QML object that controls the lifetime of this model (normally a PackageList).
- """
- super().__init__(parent)
- QQmlEngine.setObjectOwnership(self, QQmlEngine.ObjectOwnership.CppOwnership)
- self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
- self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
- self._package_id = package_data.get("package_id", "UnknownPackageId")
- self._package_type = package_data.get("package_type", "")
- self._is_bundled = package_data.get("is_bundled", False)
- self._icon_url = package_data.get("icon_url", "")
- self._marketplace_url = package_data.get("marketplace_url", "")
- self._display_name = package_data.get("display_name", catalog.i18nc("@label:property", "Unknown Package"))
- tags = package_data.get("tags", [])
- self._is_checked_by_ultimaker = (self._package_type == "plugin" and "verified" in tags) or (
- self._package_type == "material" and "certified" in tags)
- self._package_version = package_data.get("package_version", "") # Display purpose, no need for 'UM.Version'.
- self._package_info_url = package_data.get("website", "") # Not to be confused with 'download_url'.
- self._download_count = package_data.get("download_count", 0)
- self._description = package_data.get("description", "")
- self._formatted_description = self._format(self._description)
- self._download_url = package_data.get("download_url", "")
- self._release_notes = package_data.get("release_notes", "") # Not used yet, propose to add to description?
- subdata = package_data.get("data", {})
- self._technical_data_sheet = self._findLink(subdata, "technical_data_sheet")
- self._safety_data_sheet = self._findLink(subdata, "safety_data_sheet")
- self._where_to_buy = self._findLink(subdata, "where_to_buy")
- self._compatible_printers = self._getCompatiblePrinters(subdata)
- self._compatible_support_materials = self._getCompatibleSupportMaterials(subdata)
- self._is_compatible_material_station = self._isCompatibleMaterialStation(subdata)
- self._is_compatible_air_manager = self._isCompatibleAirManager(subdata)
- author_data = package_data.get("author", {})
- self._author_name = author_data.get("display_name", catalog.i18nc("@label:property", "Unknown Author"))
- self._author_info_url = author_data.get("website", "")
- if not self._icon_url or self._icon_url == "":
- self._icon_url = author_data.get("icon_url", "")
- self._can_update = False
- self._section_title = section_title
- self.sdk_version = package_data.get("sdk_version_semver", "")
- # Note that there's a lot more info in the package_data than just these specified here.
- self.enablePackageTriggered.connect(self._plugin_registry.enablePlugin)
- self.disablePackageTriggered.connect(self._plugin_registry.disablePlugin)
- self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.stateManageButtonChanged)
- self._package_manager.packageInstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id))
- self._package_manager.packageUninstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id))
- self._package_manager.packageInstallingFailed.connect(lambda pkg_id: self._packageInstalled(pkg_id))
- self._package_manager.packagesWithUpdateChanged.connect(self._processUpdatedPackages)
- self._is_busy = False
- @pyqtSlot()
- def _processUpdatedPackages(self):
- self.setCanUpdate(self._package_manager.checkIfPackageCanUpdate(self._package_id))
- def __del__(self):
- self._package_manager.packagesWithUpdateChanged.disconnect(self._processUpdatedPackages)
- def __eq__(self, other: object) -> bool:
- if isinstance(other, PackageModel):
- return other == self
- elif isinstance(other, str):
- return other == self._package_id
- else:
- return False
- def __repr__(self) -> str:
- return f"<{self._package_id} : {self._package_version} : {self._section_title}>"
- def _findLink(self, subdata: Dict[str, Any], link_type: str) -> str:
- """
- Searches the package data for a link of a certain type.
- The links are not in a fixed path in the package data. We need to iterate over the available links to find them.
- :param subdata: The "data" element in the package data, which should contain links.
- :param link_type: The type of link to find.
- :return: A URL of where the link leads, or an empty string if there is no link of that type in the package data.
- """
- links = subdata.get("links", [])
- for link in links:
- if link.get("type", "") == link_type:
- return link.get("url", "")
- else:
- return "" # No link with the correct type was found.
- def _format(self, text: str) -> str:
- """
- Formats a user-readable block of text for display.
- :return: A block of rich text with formatting embedded.
- """
- # Turn all in-line hyperlinks into actual links.
- url_regex = re.compile(r"(((http|https)://)[a-zA-Z0-9@:%.\-_+~#?&/=]{2,256}\.[a-z]{2,12}(/[a-zA-Z0-9@:%.\-_+~#?&/=]*)?)")
- text = re.sub(url_regex, r'<a href="\1">\1</a>', text)
- # Turn newlines into <br> so that they get displayed as newlines when rendering as rich text.
- text = text.replace("\n", "<br>")
- return text
- def _getCompatiblePrinters(self, subdata: Dict[str, Any]) -> List[str]:
- """
- Gets the list of printers that this package provides material compatibility with.
- Any printer is listed, even if it's only for a single nozzle on a single material in the package.
- :param subdata: The "data" element in the package data, which should contain this compatibility information.
- :return: A list of printer names that this package provides material compatibility with.
- """
- result = set()
- for material in subdata.get("materials", []):
- for compatibility in material.get("compatibility", []):
- printer_name = compatibility.get("machine_name")
- if printer_name is None:
- continue # Missing printer name information. Skip this one.
- for subcompatibility in compatibility.get("compatibilities", []):
- if subcompatibility.get("hardware_compatible", False):
- result.add(printer_name)
- break
- return list(sorted(result))
- def _getCompatibleSupportMaterials(self, subdata: Dict[str, Any]) -> List[str]:
- """
- Gets the list of support materials that the materials in this package are compatible with.
- Since the materials are individually encoded as keys in the API response, only PVA and Breakaway are currently
- supported.
- :param subdata: The "data" element in the package data, which should contain this compatibility information.
- :return: A list of support materials that the materials in this package are compatible with.
- """
- result = set()
- container_registry = CuraContainerRegistry.getInstance()
- try:
- pva_name = container_registry.findContainersMetadata(id = "ultimaker_pva")[0].get("name", "Ultimaker PVA")
- except IndexError:
- pva_name = "Ultimaker PVA"
- try:
- breakaway_name = container_registry.findContainersMetadata(id = "ultimaker_bam")[0].get("name", "Ultimaker Breakaway")
- except IndexError:
- breakaway_name = "Ultimaker Breakaway"
- for material in subdata.get("materials", []):
- if material.get("pva_compatible", False):
- result.add(pva_name)
- if material.get("breakaway_compatible", False):
- result.add(breakaway_name)
- return list(sorted(result))
- def _isCompatibleMaterialStation(self, subdata: Dict[str, Any]) -> bool:
- """
- Finds out if this package provides any material that is compatible with the material station.
- :param subdata: The "data" element in the package data, which should contain this compatibility information.
- :return: Whether this package provides any material that is compatible with the material station.
- """
- for material in subdata.get("materials", []):
- for compatibility in material.get("compatibility", []):
- if compatibility.get("material_station_optimized", False):
- return True
- return False
- def _isCompatibleAirManager(self, subdata: Dict[str, Any]) -> bool:
- """
- Finds out if this package provides any material that is compatible with the air manager.
- :param subdata: The "data" element in the package data, which should contain this compatibility information.
- :return: Whether this package provides any material that is compatible with the air manager.
- """
- for material in subdata.get("materials", []):
- for compatibility in material.get("compatibility", []):
- if compatibility.get("air_manager_optimized", False):
- return True
- return False
- @pyqtProperty(str, constant = True)
- def packageId(self) -> str:
- return self._package_id
- @pyqtProperty(str, constant=True)
- def marketplaceURL(self)-> str:
- return self._marketplace_url
- @pyqtProperty(str, constant = True)
- def packageType(self) -> str:
- return self._package_type
- @pyqtProperty(str, constant = True)
- def iconUrl(self) -> str:
- return self._icon_url
- @pyqtProperty(str, constant = True)
- def displayName(self) -> str:
- return self._display_name
- @pyqtProperty(bool, constant = True)
- def isCheckedByUltimaker(self):
- return self._is_checked_by_ultimaker
- @pyqtProperty(str, constant = True)
- def packageVersion(self) -> str:
- return self._package_version
- @pyqtProperty(str, constant = True)
- def packageInfoUrl(self) -> str:
- return self._package_info_url
- @pyqtProperty(int, constant = True)
- def downloadCount(self) -> str:
- return self._download_count
- @pyqtProperty(str, constant = True)
- def description(self) -> str:
- return self._description
- @pyqtProperty(str, constant = True)
- def formattedDescription(self) -> str:
- return self._formatted_description
- @pyqtProperty(str, constant = True)
- def authorName(self) -> str:
- return self._author_name
- @pyqtProperty(str, constant = True)
- def authorInfoUrl(self) -> str:
- return self._author_info_url
- @pyqtProperty(str, constant = True)
- def sectionTitle(self) -> Optional[str]:
- return self._section_title
- @pyqtProperty(str, constant = True)
- def technicalDataSheet(self) -> str:
- return self._technical_data_sheet
- @pyqtProperty(str, constant = True)
- def safetyDataSheet(self) -> str:
- return self._safety_data_sheet
- @pyqtProperty(str, constant = True)
- def whereToBuy(self) -> str:
- return self._where_to_buy
- @pyqtProperty("QStringList", constant = True)
- def compatiblePrinters(self) -> List[str]:
- return self._compatible_printers
- @pyqtProperty("QStringList", constant = True)
- def compatibleSupportMaterials(self) -> List[str]:
- return self._compatible_support_materials
- @pyqtProperty(bool, constant = True)
- def isCompatibleMaterialStation(self) -> bool:
- return self._is_compatible_material_station
- @pyqtProperty(bool, constant = True)
- def isCompatibleAirManager(self) -> bool:
- return self._is_compatible_air_manager
- @pyqtProperty(bool, constant = True)
- def isBundled(self) -> bool:
- return self._is_bundled
- def setDownloadUrl(self, download_url):
- self._download_url = download_url
- # --- manage buttons signals ---
- stateManageButtonChanged = pyqtSignal()
- installPackageTriggered = pyqtSignal(str, str)
- uninstallPackageTriggered = pyqtSignal(str)
- updatePackageTriggered = pyqtSignal(str, str)
- enablePackageTriggered = pyqtSignal(str)
- disablePackageTriggered = pyqtSignal(str)
- busyChanged = pyqtSignal()
- @pyqtSlot()
- def install(self):
- self.setBusy(True)
- self.installPackageTriggered.emit(self.packageId, self._download_url)
- @pyqtSlot()
- def update(self):
- self.setBusy(True)
- self.updatePackageTriggered.emit(self.packageId, self._download_url)
- @pyqtSlot()
- def uninstall(self):
- self.uninstallPackageTriggered.emit(self.packageId)
- @pyqtProperty(bool, notify= busyChanged)
- def busy(self):
- """
- Property indicating that some kind of upgrade is active.
- """
- return self._is_busy
- @pyqtSlot()
- def enable(self):
- self.enablePackageTriggered.emit(self.packageId)
- @pyqtSlot()
- def disable(self):
- self.disablePackageTriggered.emit(self.packageId)
- def setBusy(self, value: bool):
- if self._is_busy != value:
- self._is_busy = value
- try:
- self.busyChanged.emit()
- except RuntimeError:
- pass
- def _packageInstalled(self, package_id: str) -> None:
- if self._package_id != package_id:
- return
- self.setBusy(False)
- try:
- self.stateManageButtonChanged.emit()
- except RuntimeError:
- pass
- @pyqtProperty(bool, notify = stateManageButtonChanged)
- def isInstalled(self) -> bool:
- return self._package_id in self._package_manager.getAllInstalledPackageIDs()
- @pyqtProperty(bool, notify = stateManageButtonChanged)
- def isToBeInstalled(self) -> bool:
- return self._package_id in self._package_manager.getPackagesToInstall()
- @pyqtProperty(bool, notify = stateManageButtonChanged)
- def isActive(self) -> bool:
- return not self._package_id in self._plugin_registry.getDisabledPlugins()
- @pyqtProperty(bool, notify = stateManageButtonChanged)
- def canDowngrade(self) -> bool:
- """Flag if the installed package can be downgraded to a bundled version"""
- return self._package_manager.canDowngrade(self._package_id)
- def setCanUpdate(self, value: bool) -> None:
- self._can_update = value
- self.stateManageButtonChanged.emit()
- @pyqtProperty(bool, fset = setCanUpdate, notify = stateManageButtonChanged)
- def canUpdate(self) -> bool:
- """Flag indicating if the package can be updated"""
- return self._can_update
|