From 09e221d64a3954b515e86dccb786b0a9101804f6 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 6 Dec 2021 14:49:50 +0100 Subject: [PATCH] Add license dialog to the Marketplace cura 8587 --- plugins/Marketplace/LicenseModel.py | 66 +++++++++++ plugins/Marketplace/PackageList.py | 39 ++++++- .../resources/qml/LicenseDialog.qml | 110 ++++++++++++++++++ 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 plugins/Marketplace/LicenseModel.py create mode 100644 plugins/Marketplace/resources/qml/LicenseDialog.qml diff --git a/plugins/Marketplace/LicenseModel.py b/plugins/Marketplace/LicenseModel.py new file mode 100644 index 0000000000..cb85b33430 --- /dev/null +++ b/plugins/Marketplace/LicenseModel.py @@ -0,0 +1,66 @@ +from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal +from UM.i18n import i18nCatalog + +catalog = i18nCatalog("cura") + +# Model for the LicenseDialog +class LicenseModel(QObject): + DEFAULT_DECLINE_BUTTON_TEXT = catalog.i18nc("@button", "Decline") + ACCEPT_BUTTON_TEXT = catalog.i18nc("@button", "Agree") + + dialogTitleChanged = pyqtSignal() + packageNameChanged = pyqtSignal() + licenseTextChanged = pyqtSignal() + iconChanged = pyqtSignal() + + def __init__(self, decline_button_text: str = DEFAULT_DECLINE_BUTTON_TEXT) -> None: + super().__init__() + + self._dialogTitle = "" + self._license_text = "" + self._package_name = "" + self._icon_url = "" + self._decline_button_text = decline_button_text + + @pyqtProperty(str, constant = True) + def acceptButtonText(self): + return self.ACCEPT_BUTTON_TEXT + + @pyqtProperty(str, constant = True) + def declineButtonText(self): + return self._decline_button_text + + @pyqtProperty(str, notify=dialogTitleChanged) + def dialogTitle(self) -> str: + return self._dialogTitle + + @pyqtProperty(str, notify=packageNameChanged) + def packageName(self) -> str: + return self._package_name + + def setPackageName(self, name: str) -> None: + self._package_name = name + self.packageNameChanged.emit() + + @pyqtProperty(str, notify=iconChanged) + def iconUrl(self) -> str: + return self._icon_url + + def setIconUrl(self, url: str): + self._icon_url = url + self.iconChanged.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() + + def _updateDialogTitle(self): + self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement") + if self._page_count > 1: + self._dialogTitle = self._dialogTitle + " ({}/{})".format(self._current_page_idx + 1, self._page_count) + self.dialogTitleChanged.emit() diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 301f765dab..0c5895866c 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 Dict, Optional, Set, TYPE_CHECKING @@ -11,6 +12,7 @@ from UM.Qt.ListModel import ListModel from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from UM.TaskManagement.HttpRequestManager import HttpRequestData, HttpRequestManager from UM.Logger import Logger +from UM.PluginRegistry import PluginRegistry from cura.CuraApplication import CuraApplication from cura import CuraPackageManager @@ -18,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 @@ -42,12 +45,24 @@ 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 = PluginRegistry.getInstance().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 + }) + @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""" @@ -119,6 +134,28 @@ class PackageList(ListModel): canInstallChanged = pyqtSignal(str, bool) + def _openLicenseDialog(self, plugin_name: str, license_content: str, icon_url: str) -> None: + self._license_model.setIconUrl(icon_url) + self._license_model.setPackageName(plugin_name) + self._license_model.setLicenseText(license_content) + self._license_dialog.show() + + def _requestInstall(self, package_id: str, update: bool = False) -> None: + Logger.debug(f"Request installing {package_id}") + + package_path = self._to_install.pop(package_id) + license_content = self._manager.getPackageLicense(package_path) + + if not update and license_content is not None: + # open dialog, prompting the using to accept the plugin license + package = self.getPackageModel(package_id) + plugin_name = package.displayName + icon_url = package.iconUrl + self._openLicenseDialog(plugin_name, license_content, icon_url) + 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}") diff --git a/plugins/Marketplace/resources/qml/LicenseDialog.qml b/plugins/Marketplace/resources/qml/LicenseDialog.qml new file mode 100644 index 0000000000..9219f4ed32 --- /dev/null +++ b/plugins/Marketplace/resources/qml/LicenseDialog.qml @@ -0,0 +1,110 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Toolbox 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: licenseModel.dialogTitle + 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") + margin: screenScaleFactor * 10 + + ColumnLayout + { + anchors.fill: parent + spacing: UM.Theme.getSize("thick_margin").height + + UM.I18nCatalog{id: catalog; name: "cura"} + + Label + { + id: licenseHeader + Layout.fillWidth: true + text: catalog.i18nc("@label", "You need to accept the license to install the package") + color: UM.Theme.getColor("text") + wrapMode: Text.Wrap + renderType: Text.NativeRendering + } + + Row { + id: packageRow + + Layout.fillWidth: true + height: childrenRect.height + spacing: UM.Theme.getSize("default_margin").width + leftPadding: UM.Theme.getSize("narrow_margin").width + + Image + { + id: icon + width: 30 * screenScaleFactor + height: width + sourceSize.width: width + sourceSize.height: height + fillMode: Image.PreserveAspectFit + source: licenseModel.iconUrl || "../../images/placeholder.svg" + mipmap: true + } + + Label + { + id: packageName + text: licenseModel.packageName + color: UM.Theme.getColor("text") + font.bold: true + anchors.verticalCenter: icon.verticalCenter + height: contentHeight + 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 + { + leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width + rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width + + text: licenseModel.acceptButtonText + onClicked: { handler.onLicenseAccepted() } + } + ] + + leftButtons: + [ + Cura.SecondaryButton + { + id: declineButton + text: licenseModel.declineButtonText + onClicked: { handler.onLicenseDeclined() } + } + ] +}