diff --git a/plugins/Marketplace/LicenseModel.py b/plugins/Marketplace/LicenseModel.py new file mode 100644 index 0000000000..8eb439d4d6 --- /dev/null +++ b/plugins/Marketplace/LicenseModel.py @@ -0,0 +1,31 @@ +from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal +from UM.i18n import i18nCatalog + +catalog = i18nCatalog("cura") + +# Model for the LicenseDialog +class LicenseModel(QObject): + packageIdChanged = pyqtSignal() + licenseTextChanged = pyqtSignal() + + def __init__(self) -> None: + super().__init__() + self._license_text = "" + self._package_id = "" + + @pyqtProperty(str, notify=packageIdChanged) + def packageId(self) -> str: + return self._package_id + + def setPackageId(self, name: str) -> None: + self._package_id = name + self.packageIdChanged.emit() + + @pyqtProperty(str, notify=licenseTextChanged) + def licenseText(self) -> str: + return self._license_text + + def setLicenseText(self, license_text: str) -> None: + if self._license_text != license_text: + self._license_text = license_text + self.licenseTextChanged.emit() diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 665451ba18..39d1d9fab6 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import tempfile import json +import os.path from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt from typing import cast, Dict, Optional, Set, TYPE_CHECKING @@ -19,6 +20,7 @@ from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To ma from .PackageModel import PackageModel from .Constants import USER_PACKAGES_URL +from .LicenseModel import LicenseModel if TYPE_CHECKING: from PyQt5.QtCore import QObject @@ -45,12 +47,25 @@ class PackageList(ListModel): self._has_more = False self._has_footer = True self._to_install: Dict[str, str] = {} - self.canInstallChanged.connect(self._install) + self.canInstallChanged.connect(self._requestInstall) self._local_packages: Set[str] = {p["package_id"] for p in self._manager.local_packages} self._ongoing_request: Optional[HttpRequestData] = None self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) + self._license_model = LicenseModel() + + 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", "LicenseDialog.qml") + self._license_dialog = CuraApplication.getInstance().createQmlComponent(license_dialog_component_path, { + "licenseModel": self._license_model, + "handler": self + }) + @pyqtSlot() def updatePackages(self) -> None: """ A Qt slot which will update the List from a source. Actual implementation should be done in the child class""" @@ -122,9 +137,44 @@ class PackageList(ListModel): canInstallChanged = pyqtSignal(str, bool) + def _openLicenseDialog(self, plugin_name: str, license_content: str) -> None: + Logger.debug(f"Prompting license for {plugin_name}") + self._license_model.setPackageId(plugin_name) + self._license_model.setLicenseText(license_content) + self._license_dialog.show() + + @pyqtSlot() + def onLicenseAccepted(self) -> None: + package_id = self._license_model.packageId + Logger.debug(f"Accepted license for {package_id}") + self._license_dialog.close() + self._install(package_id) + + @pyqtSlot() + def onLicenseDeclined(self) -> None: + package_id = self._license_model.packageId + Logger.debug(f"Declined license for {package_id}") + self._license_dialog.close() + package = self.getPackageModel(package_id) + package.is_installing = False + + def _requestInstall(self, package_id: str, update: bool = False) -> None: + Logger.debug(f"Request installing {package_id}") + + package_path = self._to_install[package_id] + license_content = self._manager.getPackageLicense(package_path) + + if not update and license_content is not None: + # If installation is not and update, and the packages contains a license then + # open dialog, prompting the using to accept the plugin license + self._openLicenseDialog(package_id, license_content) + else: + # Otherwise continue the installation + self._install(package_id, update) + def _install(self, package_id: str, update: bool = False) -> None: - package_path = self._to_install.pop(package_id) Logger.debug(f"Installing {package_id}") + package_path = self._to_install.pop(package_id) to_be_installed = self._manager.installPackage(package_path) is not None package = self.getPackageModel(package_id) if package.can_update and to_be_installed: diff --git a/plugins/Marketplace/resources/qml/LicenseDialog.qml b/plugins/Marketplace/resources/qml/LicenseDialog.qml new file mode 100644 index 0000000000..7bee6f9108 --- /dev/null +++ b/plugins/Marketplace/resources/qml/LicenseDialog.qml @@ -0,0 +1,88 @@ +//Copyright (c) 2021 Ultimaker B.V. +//Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.10 +import QtQuick.Dialogs 1.1 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Styles 1.4 + +import UM 1.1 as UM +import Cura 1.6 as Cura + +UM.Dialog +{ + id: licenseDialog + title: catalog.i18nc("@button", "Plugin license agreement") + minimumWidth: UM.Theme.getSize("license_window_minimum").width + minimumHeight: UM.Theme.getSize("license_window_minimum").height + width: minimumWidth + height: minimumHeight + backgroundColor: UM.Theme.getColor("main_background") + + ColumnLayout + { + anchors.fill: parent + spacing: UM.Theme.getSize("thick_margin").height + + UM.I18nCatalog{id: catalog; name: "cura"} + + Row { + Layout.fillWidth: true + height: childrenRect.height + spacing: UM.Theme.getSize("default_margin").width + leftPadding: UM.Theme.getSize("narrow_margin").width + + UM.RecolorImage + { + width: UM.Theme.getSize("marketplace_large_icon").width + height: UM.Theme.getSize("marketplace_large_icon").height + color: UM.Theme.getColor("text") + source: UM.Theme.getIcon("Certificate", "high") + } + + Label + { + text: catalog.i18nc("@text", "Please read and agree with the plugin licence.") + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("large") + anchors.verticalCenter: icon.verticalCenter + height: UM.Theme.getSize("marketplace_large_icon").height + verticalAlignment: Qt.AlignVCenter + wrapMode: Text.Wrap + renderType: Text.NativeRendering + } + + } + + Cura.ScrollableTextArea + { + + Layout.fillWidth: true + Layout.fillHeight: true + anchors.topMargin: UM.Theme.getSize("default_margin").height + + textArea.text: licenseModel.licenseText + textArea.readOnly: true + } + + } + rightButtons: + [ + Cura.PrimaryButton + { + text: catalog.i18nc("@button", "Accept") + onClicked: { handler.onLicenseAccepted() } + } + ] + + leftButtons: + [ + Cura.SecondaryButton + { + text: catalog.i18nc("@button", "Decline") + onClicked: { handler.onLicenseDeclined() } + } + ] +} diff --git a/resources/themes/cura-light/icons/high/Certificate.svg b/resources/themes/cura-light/icons/high/Certificate.svg new file mode 100644 index 0000000000..b588bddd8b --- /dev/null +++ b/resources/themes/cura-light/icons/high/Certificate.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 149bfab308..8e9db0e9fe 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -686,6 +686,8 @@ "welcome_wizard_content_image_big": [18, 15], "welcome_wizard_cloud_content_image": [4, 4], - "banner_icon_size": [2.0, 2.0] + "banner_icon_size": [2.0, 2.0], + + "marketplace_large_icon": [4.0, 4.0] } }