From 5a6227809799da8f6e8231bbb13062d1f95b8a31 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Apr 2022 10:16:00 +0200 Subject: Fix display of username CURA-9146 --- resources/qml/Account/UserOperations.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/Account/UserOperations.qml b/resources/qml/Account/UserOperations.qml index d7677264fb..c8642681ae 100644 --- a/resources/qml/Account/UserOperations.qml +++ b/resources/qml/Account/UserOperations.qml @@ -46,7 +46,7 @@ Column { id: initialLabel anchors.centerIn: parent - text: profile["username"].charAt(0).toUpperCase() + text: profile.username.charAt(0).toUpperCase() font: UM.Theme.getFont("large_bold") horizontalAlignment: Text.AlignHCenter } -- cgit v1.2.3 From 5ce5ce769e19f3b2ab47b23998b850d4aac92b89 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Apr 2022 13:12:06 +0200 Subject: Re-add the CloudSync functionality It got removed in the marketplace upgrade. CURA-9146 --- plugins/Marketplace/CloudApiModel.py | 29 ++++ plugins/Marketplace/CloudSync/CloudApiClient.py | 52 +++++++ .../Marketplace/CloudSync/CloudPackageChecker.py | 166 +++++++++++++++++++++ .../CloudSync/DiscrepanciesPresenter.py | 41 +++++ plugins/Marketplace/CloudSync/DownloadPresenter.py | 153 +++++++++++++++++++ plugins/Marketplace/CloudSync/LicenseModel.py | 77 ++++++++++ plugins/Marketplace/CloudSync/LicensePresenter.py | 139 +++++++++++++++++ .../CloudSync/RestartApplicationPresenter.py | 32 ++++ .../CloudSync/SubscribedPackagesModel.py | 74 +++++++++ plugins/Marketplace/CloudSync/SyncOrchestrator.py | 114 ++++++++++++++ plugins/Marketplace/CloudSync/__init__.py | 0 plugins/Marketplace/__init__.py | 4 +- .../resources/qml/CompatibilityDialog.qml | 154 +++++++++++++++++++ .../resources/qml/MarketplaceLicenseDialog.qml | 114 ++++++++++++++ 14 files changed, 1147 insertions(+), 2 deletions(-) create mode 100644 plugins/Marketplace/CloudApiModel.py create mode 100644 plugins/Marketplace/CloudSync/CloudApiClient.py create mode 100644 plugins/Marketplace/CloudSync/CloudPackageChecker.py create mode 100644 plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py create mode 100644 plugins/Marketplace/CloudSync/DownloadPresenter.py create mode 100644 plugins/Marketplace/CloudSync/LicenseModel.py create mode 100644 plugins/Marketplace/CloudSync/LicensePresenter.py create mode 100644 plugins/Marketplace/CloudSync/RestartApplicationPresenter.py create mode 100644 plugins/Marketplace/CloudSync/SubscribedPackagesModel.py create mode 100644 plugins/Marketplace/CloudSync/SyncOrchestrator.py create mode 100644 plugins/Marketplace/CloudSync/__init__.py create mode 100644 plugins/Marketplace/resources/qml/CompatibilityDialog.qml create mode 100644 plugins/Marketplace/resources/qml/MarketplaceLicenseDialog.qml diff --git a/plugins/Marketplace/CloudApiModel.py b/plugins/Marketplace/CloudApiModel.py new file mode 100644 index 0000000000..bef37d8173 --- /dev/null +++ b/plugins/Marketplace/CloudApiModel.py @@ -0,0 +1,29 @@ +from typing import Union + +from cura import ApplicationMetadata +from cura.UltimakerCloud import UltimakerCloudConstants + + +class CloudApiModel: + sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int] + cloud_api_version = UltimakerCloudConstants.CuraCloudAPIVersion # type: str + cloud_api_root = UltimakerCloudConstants.CuraCloudAPIRoot # type: str + api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format( + cloud_api_root = cloud_api_root, + cloud_api_version = cloud_api_version, + sdk_version = sdk_version + ) # type: str + + # https://api.ultimaker.com/cura-packages/v1/user/packages + api_url_user_packages = "{cloud_api_root}/cura-packages/v{cloud_api_version}/user/packages".format( + cloud_api_root=cloud_api_root, + cloud_api_version=cloud_api_version, + ) + + @classmethod + def userPackageUrl(cls, package_id: str) -> str: + """https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id}""" + + return (CloudApiModel.api_url_user_packages + "/{package_id}").format( + package_id=package_id + ) diff --git a/plugins/Marketplace/CloudSync/CloudApiClient.py b/plugins/Marketplace/CloudSync/CloudApiClient.py new file mode 100644 index 0000000000..21eb1bdbd2 --- /dev/null +++ b/plugins/Marketplace/CloudSync/CloudApiClient.py @@ -0,0 +1,52 @@ +from UM.Logger import Logger +from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope +from cura.CuraApplication import CuraApplication +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope +from ..CloudApiModel import CloudApiModel + + +class CloudApiClient: + """Manages Cloud subscriptions + + When a package is added to a user's account, the user is 'subscribed' to that package. + Whenever the user logs in on another instance of Cura, these subscriptions can be used to sync the user's plugins + + Singleton: use CloudApiClient.getInstance() instead of CloudApiClient() + """ + + __instance = None + + @classmethod + def getInstance(cls, app: CuraApplication): + if not cls.__instance: + cls.__instance = CloudApiClient(app) + return cls.__instance + + def __init__(self, app: CuraApplication) -> None: + if self.__instance is not None: + raise RuntimeError("This is a Singleton. use getInstance()") + + self._scope = JsonDecoratorScope(UltimakerCloudScope(app)) # type: JsonDecoratorScope + + app.getPackageManager().packageInstalled.connect(self._onPackageInstalled) + + def unsubscribe(self, package_id: str) -> None: + url = CloudApiModel.userPackageUrl(package_id) + HttpRequestManager.getInstance().delete(url = url, scope = self._scope) + + def _subscribe(self, package_id: str) -> None: + """You probably don't want to use this directly. All installed packages will be automatically subscribed.""" + + Logger.debug("Subscribing to {}", package_id) + data = "{\"data\": {\"package_id\": \"%s\", \"sdk_version\": \"%s\"}}" % (package_id, CloudApiModel.sdk_version) + HttpRequestManager.getInstance().put( + url = CloudApiModel.api_url_user_packages, + data = data.encode(), + scope = self._scope + ) + + def _onPackageInstalled(self, package_id: str): + if CuraApplication.getInstance().getCuraAPI().account.isLoggedIn: + # We might already be subscribed, but checking would take one extra request. Instead, simply subscribe + self._subscribe(package_id) diff --git a/plugins/Marketplace/CloudSync/CloudPackageChecker.py b/plugins/Marketplace/CloudSync/CloudPackageChecker.py new file mode 100644 index 0000000000..b27b2e0ab3 --- /dev/null +++ b/plugins/Marketplace/CloudSync/CloudPackageChecker.py @@ -0,0 +1,166 @@ +# Copyright (c) 2020 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import json +from typing import List, Dict, Any, Set +from typing import Optional + +from PyQt6.QtCore import QObject +from PyQt6.QtNetwork import QNetworkReply + +from UM import i18nCatalog +from UM.Logger import Logger +from UM.Message import Message +from UM.Signal import Signal +from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope +from cura.API.Account import SyncState +from cura.CuraApplication import CuraApplication, ApplicationMetadata +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope +from .SubscribedPackagesModel import SubscribedPackagesModel +from ..CloudApiModel import CloudApiModel + + +class CloudPackageChecker(QObject): + + SYNC_SERVICE_NAME = "CloudPackageChecker" + + def __init__(self, application: CuraApplication) -> None: + super().__init__() + + self.discrepancies = Signal() # Emits SubscribedPackagesModel + self._application = application # type: CuraApplication + self._scope = JsonDecoratorScope(UltimakerCloudScope(application)) + self._model = SubscribedPackagesModel() + self._message = None # type: Optional[Message] + + self._application.initializationFinished.connect(self._onAppInitialized) + self._i18n_catalog = i18nCatalog("cura") + self._sdk_version = ApplicationMetadata.CuraSDKVersion + + self._last_notified_packages = set() # type: Set[str] + """Packages for which a notification has been shown. No need to bother the user twice for equal content""" + + # This is a plugin, so most of the components required are not ready when + # this is initialized. Therefore, we wait until the application is ready. + def _onAppInitialized(self) -> None: + self._package_manager = self._application.getPackageManager() + # initial check + self._getPackagesIfLoggedIn() + + self._application.getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged) + self._application.getCuraAPI().account.syncRequested.connect(self._getPackagesIfLoggedIn) + + def _onLoginStateChanged(self) -> None: + # reset session + self._last_notified_packages = set() + self._getPackagesIfLoggedIn() + + def _getPackagesIfLoggedIn(self) -> None: + if self._application.getCuraAPI().account.isLoggedIn: + self._getUserSubscribedPackages() + else: + self._hideSyncMessage() + + def _getUserSubscribedPackages(self) -> None: + self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SYNCING) + url = CloudApiModel.api_url_user_packages + self._application.getHttpRequestManager().get(url, + callback = self._onUserPackagesRequestFinished, + error_callback = self._onUserPackagesRequestFinished, + timeout = 10, + scope = self._scope) + + def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None: + if error is not None or HttpRequestManager.safeHttpStatus(reply) != 200: + Logger.log("w", + "Requesting user packages failed, response code %s while trying to connect to %s", + HttpRequestManager.safeHttpStatus(reply), reply.url()) + self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) + return + + try: + json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) + # Check for errors: + if "errors" in json_data: + for error in json_data["errors"]: + Logger.log("e", "%s", error["title"]) + self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) + return + self._handleCompatibilityData(json_data["data"]) + except json.decoder.JSONDecodeError: + Logger.log("w", "Received invalid JSON for user subscribed packages from the Web Marketplace") + + self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS) + + def _handleCompatibilityData(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None: + user_subscribed_packages = {plugin["package_id"] for plugin in subscribed_packages_payload} + user_installed_packages = self._package_manager.getAllInstalledPackageIDs() + + # We need to re-evaluate the dismissed packages + # (i.e. some package might got updated to the correct SDK version in the meantime, + # hence remove them from the Dismissed Incompatible list) + self._package_manager.reEvaluateDismissedPackages(subscribed_packages_payload, self._sdk_version) + user_dismissed_packages = self._package_manager.getDismissedPackages() + if user_dismissed_packages: + user_installed_packages.update(user_dismissed_packages) + + # We check if there are packages installed in Web Marketplace but not in Cura marketplace + package_discrepancy = list(user_subscribed_packages.difference(user_installed_packages)) + + if user_subscribed_packages != self._last_notified_packages: + # scenario: + # 1. user subscribes to a package + # 2. dismisses the license/unsubscribes + # 3. subscribes to the same package again + # in this scenario we want to notify the user again. To capture that there was a change during + # step 2, we clear the last_notified after step 2. This way, the user will be notified after + # step 3 even though the list of packages for step 1 and 3 are equal + self._last_notified_packages = set() + + if package_discrepancy: + account = self._application.getCuraAPI().account + account.setUpdatePackagesAction(lambda: self._onSyncButtonClicked(None, None)) + + if user_subscribed_packages == self._last_notified_packages: + # already notified user about these + return + + Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages") + self._model.addDiscrepancies(package_discrepancy) + self._model.initialize(self._package_manager, subscribed_packages_payload) + self._showSyncMessage() + self._last_notified_packages = user_subscribed_packages + + def _showSyncMessage(self) -> None: + """Show the message if it is not already shown""" + + if self._message is not None: + self._message.show() + return + + sync_message = Message(self._i18n_catalog.i18nc( + "@info:generic", + "Do you want to sync material and software packages with your account?"), + title = self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", )) + sync_message.addAction("sync", + name = self._i18n_catalog.i18nc("@action:button", "Sync"), + icon = "", + description = "Sync your plugins and print profiles to Ultimaker Cura.", + button_align = Message.ActionButtonAlignment.ALIGN_RIGHT) + sync_message.actionTriggered.connect(self._onSyncButtonClicked) + sync_message.show() + self._message = sync_message + + def _hideSyncMessage(self) -> None: + """Hide the message if it is showing""" + + if self._message is not None: + self._message.hide() + self._message = None + + def _onSyncButtonClicked(self, sync_message: Optional[Message], sync_message_action: Optional[str]) -> None: + if sync_message is not None: + sync_message.hide() + self._hideSyncMessage() # Should be the same message, but also sets _message to None + self.discrepancies.emit(self._model) diff --git a/plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py b/plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py new file mode 100644 index 0000000000..45ce70b32c --- /dev/null +++ b/plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py @@ -0,0 +1,41 @@ +import os +from typing import Optional + +from PyQt6.QtCore import QObject + +from UM.Qt.QtApplication import QtApplication +from UM.Signal import Signal +from .SubscribedPackagesModel import SubscribedPackagesModel + + +class DiscrepanciesPresenter(QObject): + """Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's + + choices are emitted on the `packageMutations` Signal. + """ + + def __init__(self, app: QtApplication) -> None: + super().__init__() + + self.packageMutations = Signal() # Emits SubscribedPackagesModel + + self._app = app + self._package_manager = app.getPackageManager() + self._dialog = None # type: Optional[QObject] + self._compatibility_dialog_path = "resources/qml/CompatibilityDialog.qml" + + def present(self, plugin_path: str, model: SubscribedPackagesModel) -> None: + path = os.path.join(plugin_path, self._compatibility_dialog_path) + self._dialog = self._app.createQmlComponent(path, {"subscribedPackagesModel": model, "handler": self}) + assert self._dialog + self._dialog.accepted.connect(lambda: self._onConfirmClicked(model)) + + def _onConfirmClicked(self, model: SubscribedPackagesModel) -> None: + # If there are incompatible packages - automatically dismiss them + if model.getIncompatiblePackages(): + self._package_manager.dismissAllIncompatiblePackages(model.getIncompatiblePackages()) + # For now, all compatible packages presented to the user should be installed. + # Later, we might remove items for which the user unselected the package + if model.getCompatiblePackages(): + model.setItems(model.getCompatiblePackages()) + self.packageMutations.emit(model) diff --git a/plugins/Marketplace/CloudSync/DownloadPresenter.py b/plugins/Marketplace/CloudSync/DownloadPresenter.py new file mode 100644 index 0000000000..e7ac682d69 --- /dev/null +++ b/plugins/Marketplace/CloudSync/DownloadPresenter.py @@ -0,0 +1,153 @@ +# Copyright (c) 2020 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import tempfile +from typing import Dict, List, Any + +from PyQt6.QtNetwork import QNetworkReply + +from UM.i18n import i18nCatalog +from UM.Logger import Logger +from UM.Message import Message +from UM.Signal import Signal +from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from cura.CuraApplication import CuraApplication +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope +from .SubscribedPackagesModel import SubscribedPackagesModel + +i18n_catalog = i18nCatalog("cura") + + +class DownloadPresenter: + """Downloads a set of packages from the Ultimaker Cloud Marketplace + + use download() exactly once: should not be used for multiple sets of downloads since this class contains state + """ + + DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB + + def __init__(self, app: CuraApplication) -> None: + # Emits (Dict[str, str], List[str]) # (success_items, error_items) + # Dict{success_package_id, temp_file_path} + # List[errored_package_id] + self.done = Signal() + + self._app = app + self._scope = UltimakerCloudScope(app) + + self._started = False + self._progress_message = self._createProgressMessage() + self._progress = {} # type: Dict[str, Dict[str, Any]] # package_id, Dict + self._error = [] # type: List[str] # package_id + + def download(self, model: SubscribedPackagesModel) -> None: + if self._started: + Logger.error("Download already started. Create a new %s instead", self.__class__.__name__) + return + + manager = HttpRequestManager.getInstance() + for item in model.items: + package_id = item["package_id"] + + def finishedCallback(reply: QNetworkReply, pid = package_id) -> None: + self._onFinished(pid, reply) + + def progressCallback(rx: int, rt: int, pid = package_id) -> None: + self._onProgress(pid, rx, rt) + + def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, pid = package_id) -> None: + self._onError(pid) + + request_data = manager.get( + item["download_url"], + callback = finishedCallback, + download_progress_callback = progressCallback, + error_callback = errorCallback, + scope = self._scope) + + self._progress[package_id] = { + "received": 0, + "total": 1, # make sure this is not considered done yet. Also divByZero-safe + "file_written": None, + "request_data": request_data, + "package_model": item + } + + self._started = True + self._progress_message.show() + + def abort(self) -> None: + manager = HttpRequestManager.getInstance() + for item in self._progress.values(): + manager.abortRequest(item["request_data"]) + + # Aborts all current operations and returns a copy with the same settings such as app and scope + def resetCopy(self) -> "DownloadPresenter": + self.abort() + self.done.disconnectAll() + return DownloadPresenter(self._app) + + def _createProgressMessage(self) -> Message: + return Message(i18n_catalog.i18nc("@info:generic", "Syncing..."), + lifetime = 0, + use_inactivity_timer = False, + progress = 0.0, + title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account")) + + def _onFinished(self, package_id: str, reply: QNetworkReply) -> None: + self._progress[package_id]["received"] = self._progress[package_id]["total"] + + try: + with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file: + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + while bytes_read: + temp_file.write(bytes_read) + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + self._app.processEvents() + self._progress[package_id]["file_written"] = temp_file.name + except IOError as e: + Logger.logException("e", "Failed to write downloaded package to temp file", e) + self._onError(package_id) + temp_file.close() + + self._checkDone() + + def _onProgress(self, package_id: str, rx: int, rt: int) -> None: + self._progress[package_id]["received"] = rx + self._progress[package_id]["total"] = rt + + received = 0 + total = 0 + for item in self._progress.values(): + received += item["received"] + total += item["total"] + + if total == 0: # Total download size is 0, or unknown, or there are no progress items at all. + self._progress_message.setProgress(100.0) + return + + self._progress_message.setProgress(100.0 * (received / total)) # [0 .. 100] % + + def _onError(self, package_id: str) -> None: + self._progress.pop(package_id) + self._error.append(package_id) + self._checkDone() + + def _checkDone(self) -> bool: + for item in self._progress.values(): + if not item["file_written"]: + return False + + success_items = { + package_id: + { + "package_path": value["file_written"], + "icon_url": value["package_model"]["icon_url"] + } + for package_id, value in self._progress.items() + } + error_items = [package_id for package_id in self._error] + + self._progress_message.hide() + self.done.emit(success_items, error_items) + return True diff --git a/plugins/Marketplace/CloudSync/LicenseModel.py b/plugins/Marketplace/CloudSync/LicenseModel.py new file mode 100644 index 0000000000..8c036c48b7 --- /dev/null +++ b/plugins/Marketplace/CloudSync/LicenseModel.py @@ -0,0 +1,77 @@ +from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal +from UM.i18n import i18nCatalog + +catalog = i18nCatalog("cura") + + +# Model for the ToolboxLicenseDialog +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, parent = None) -> None: + super().__init__(parent) + + self._current_page_idx = 0 + self._page_count = 1 + 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 setCurrentPageIdx(self, idx: int) -> None: + self._current_page_idx = idx + self._updateDialogTitle() + + def setPageCount(self, count: int) -> None: + self._page_count = count + self._updateDialogTitle() + + 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/CloudSync/LicensePresenter.py b/plugins/Marketplace/CloudSync/LicensePresenter.py new file mode 100644 index 0000000000..ce0d198c01 --- /dev/null +++ b/plugins/Marketplace/CloudSync/LicensePresenter.py @@ -0,0 +1,139 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import os +from typing import Dict, Optional, List, Any + +from PyQt6.QtCore import QObject, pyqtSlot + +from UM.Logger import Logger +from UM.PackageManager import PackageManager +from UM.Signal import Signal +from cura.CuraApplication import CuraApplication +from UM.i18n import i18nCatalog + +from .LicenseModel import LicenseModel + + +class LicensePresenter(QObject): + """Presents licenses for a set of packages for the user to accept or reject. + + Call present() exactly once to show a licenseDialog for a set of packages + Before presenting another set of licenses, create a new instance using resetCopy(). + + licenseAnswers emits a list of Dicts containing answers when the user has made a choice for all provided packages. + """ + + def __init__(self, app: CuraApplication) -> None: + super().__init__() + self._presented = False + """Whether present() has been called and state is expected to be initialized""" + self._catalog = i18nCatalog("cura") + self._dialog = None # type: Optional[QObject] + self._package_manager = app.getPackageManager() # type: PackageManager + # Emits List[Dict[str, [Any]] containing for example + # [{ "package_id": "BarbarianPlugin", "package_path" : "/tmp/dg345as", "accepted" : True }] + self.licenseAnswers = Signal() + + self._current_package_idx = 0 + self._package_models = [] # type: List[Dict] + decline_button_text = self._catalog.i18nc("@button", "Decline and remove from account") + self._license_model = LicenseModel(decline_button_text=decline_button_text) # type: LicenseModel + self._page_count = 0 + + self._app = app + + self._compatibility_dialog_path = "resources/qml/MarketplaceLicenseDialog.qml" + + def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None: + """Show a license dialog for multiple packages where users can read a license and accept or decline them + + :param plugin_path: Root directory of the Toolbox plugin + :param packages: Dict[package id, file path] + """ + if self._presented: + Logger.error("{clazz} is single-use. Create a new {clazz} instead", clazz=self.__class__.__name__) + return + + path = os.path.join(plugin_path, self._compatibility_dialog_path) + + self._initState(packages) + + if self._page_count == 0: + self.licenseAnswers.emit(self._package_models) + return + + if self._dialog is None: + context_properties = { + "licenseModel": self._license_model, + "handler": self + } + self._dialog = self._app.createQmlComponent(path, context_properties) + self._presentCurrentPackage() + self._presented = True + + def resetCopy(self) -> "LicensePresenter": + """Clean up and return a new copy with the same settings such as app""" + if self._dialog: + self._dialog.close() + self.licenseAnswers.disconnectAll() + return LicensePresenter(self._app) + + @pyqtSlot() + def onLicenseAccepted(self) -> None: + self._package_models[self._current_package_idx]["accepted"] = True + self._checkNextPage() + + @pyqtSlot() + def onLicenseDeclined(self) -> None: + self._package_models[self._current_package_idx]["accepted"] = False + self._checkNextPage() + + def _initState(self, packages: Dict[str, Dict[str, Any]]) -> None: + + implicitly_accepted_count = 0 + + for package_id, item in packages.items(): + item["package_id"] = package_id + try: + item["licence_content"] = self._package_manager.getPackageLicense(item["package_path"]) + except EnvironmentError as e: + Logger.error(f"Could not open downloaded package {package_id} to read license file! {type(e)} - {e}") + continue # Skip this package. + if item["licence_content"] is None: + # Implicitly accept when there is no license + item["accepted"] = True + implicitly_accepted_count = implicitly_accepted_count + 1 + self._package_models.append(item) + else: + item["accepted"] = None #: None: no answer yet + # When presenting the packages, we want to show packages which have a license first. + # In fact, we don't want to show the others at all because they are implicitly accepted + self._package_models.insert(0, item) + CuraApplication.getInstance().processEvents() + self._page_count = len(self._package_models) - implicitly_accepted_count + self._license_model.setPageCount(self._page_count) + + + def _presentCurrentPackage(self) -> None: + package_model = self._package_models[self._current_package_idx] + package_info = self._package_manager.getPackageInfo(package_model["package_path"]) + + self._license_model.setCurrentPageIdx(self._current_package_idx) + self._license_model.setPackageName(package_info["display_name"]) + self._license_model.setIconUrl(package_model["icon_url"]) + self._license_model.setLicenseText(package_model["licence_content"]) + if self._dialog: + self._dialog.open() # Does nothing if already open + + def _checkNextPage(self) -> None: + if self._current_package_idx + 1 < self._page_count: + self._current_package_idx += 1 + self._presentCurrentPackage() + else: + if self._dialog: + self._dialog.close() + self.licenseAnswers.emit(self._package_models) + + + diff --git a/plugins/Marketplace/CloudSync/RestartApplicationPresenter.py b/plugins/Marketplace/CloudSync/RestartApplicationPresenter.py new file mode 100644 index 0000000000..8776d1782a --- /dev/null +++ b/plugins/Marketplace/CloudSync/RestartApplicationPresenter.py @@ -0,0 +1,32 @@ +from UM import i18nCatalog +from UM.Message import Message +from cura.CuraApplication import CuraApplication + + +class RestartApplicationPresenter: + """Presents a dialog telling the user that a restart is required to apply changes + + Since we cannot restart Cura, the app is closed instead when the button is clicked + """ + def __init__(self, app: CuraApplication) -> None: + self._app = app + self._i18n_catalog = i18nCatalog("cura") + + def present(self) -> None: + app_name = self._app.getApplicationDisplayName() + + message = Message(self._i18n_catalog.i18nc("@info:generic", + "You need to quit and restart {} before changes have effect.", + app_name)) + + message.addAction("quit", + name="Quit " + app_name, + icon = "", + description="Close the application", + button_align=Message.ActionButtonAlignment.ALIGN_RIGHT) + + message.actionTriggered.connect(self._quitClicked) + message.show() + + def _quitClicked(self, *_): + self._app.windowClosed() diff --git a/plugins/Marketplace/CloudSync/SubscribedPackagesModel.py b/plugins/Marketplace/CloudSync/SubscribedPackagesModel.py new file mode 100644 index 0000000000..fcce62fab5 --- /dev/null +++ b/plugins/Marketplace/CloudSync/SubscribedPackagesModel.py @@ -0,0 +1,74 @@ +# Copyright (c) 2020 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt6.QtCore import Qt, pyqtProperty + +from UM.PackageManager import PackageManager +from UM.Qt.ListModel import ListModel +from UM.Version import Version + +from cura import ApplicationMetadata +from typing import List, Dict, Any + + +class SubscribedPackagesModel(ListModel): + def __init__(self, parent = None): + super().__init__(parent) + + self._items = [] + self._metadata = None + self._discrepancies = None + self._sdk_version = ApplicationMetadata.CuraSDKVersion + + self.addRoleName(Qt.ItemDataRole.UserRole + 1, "package_id") + self.addRoleName(Qt.ItemDataRole.UserRole + 2, "display_name") + self.addRoleName(Qt.ItemDataRole.UserRole + 3, "icon_url") + self.addRoleName(Qt.ItemDataRole.UserRole + 4, "is_compatible") + self.addRoleName(Qt.ItemDataRole.UserRole + 5, "is_dismissed") + + @pyqtProperty(bool, constant=True) + def hasCompatiblePackages(self) -> bool: + for item in self._items: + if item['is_compatible']: + return True + return False + + @pyqtProperty(bool, constant=True) + def hasIncompatiblePackages(self) -> bool: + for item in self._items: + if not item['is_compatible']: + return True + return False + + def addDiscrepancies(self, discrepancy: List[str]) -> None: + self._discrepancies = discrepancy + + def getCompatiblePackages(self) -> List[Dict[str, Any]]: + return [package for package in self._items if package["is_compatible"]] + + def getIncompatiblePackages(self) -> List[str]: + return [package["package_id"] for package in self._items if not package["is_compatible"]] + + def initialize(self, package_manager: PackageManager, subscribed_packages_payload: List[Dict[str, Any]]) -> None: + self._items.clear() + for item in subscribed_packages_payload: + if item["package_id"] not in self._discrepancies: + continue + package = { + "package_id": item["package_id"], + "display_name": item["display_name"], + "sdk_versions": item["sdk_versions"], + "download_url": item["download_url"], + "md5_hash": item["md5_hash"], + "is_dismissed": False, + } + + compatible = any(package_manager.isPackageCompatible(Version(version)) for version in item["sdk_versions"]) + package.update({"is_compatible": compatible}) + + try: + package.update({"icon_url": item["icon_url"]}) + except KeyError: # There is no 'icon_url" in the response payload for this package + package.update({"icon_url": ""}) + self._items.append(package) + self.setItems(self._items) diff --git a/plugins/Marketplace/CloudSync/SyncOrchestrator.py b/plugins/Marketplace/CloudSync/SyncOrchestrator.py new file mode 100644 index 0000000000..bfa449b40a --- /dev/null +++ b/plugins/Marketplace/CloudSync/SyncOrchestrator.py @@ -0,0 +1,114 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import os +from typing import List, Dict, Any, cast + +from UM import i18n_catalog +from UM.Extension import Extension +from UM.Logger import Logger +from UM.Message import Message +from UM.PluginRegistry import PluginRegistry +from cura.CuraApplication import CuraApplication +from .CloudPackageChecker import CloudPackageChecker +from .CloudApiClient import CloudApiClient +from .DiscrepanciesPresenter import DiscrepanciesPresenter +from .DownloadPresenter import DownloadPresenter +from .LicensePresenter import LicensePresenter +from .RestartApplicationPresenter import RestartApplicationPresenter +from .SubscribedPackagesModel import SubscribedPackagesModel + + +class SyncOrchestrator(Extension): + """Orchestrates the synchronizing of packages from the user account to the installed packages + + Example flow: + + - CloudPackageChecker compares a list of packages the user `subscribed` to in their account + If there are `discrepancies` between the account and locally installed packages, they are emitted + - DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations` + the user selected to be performed + - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed + - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads + - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to + be installed. It emits the `licenseAnswers` signal for accept or declines + - The CloudApiClient removes the declined packages from the account + - The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files. + - The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect + """ + + def __init__(self, app: CuraApplication) -> None: + super().__init__() + # Differentiate This PluginObject from the Marketplace. self.getId() includes _name. + # getPluginId() will return the same value for The Marketplace extension and this one + self._name = "SyncOrchestrator" + + self._package_manager = app.getPackageManager() + # Keep a reference to the CloudApiClient. it watches for installed packages and subscribes to them + self._cloud_api = CloudApiClient.getInstance(app) # type: CloudApiClient + + self._checker = CloudPackageChecker(app) # type: CloudPackageChecker + self._checker.discrepancies.connect(self._onDiscrepancies) + + self._discrepancies_presenter = DiscrepanciesPresenter(app) # type: DiscrepanciesPresenter + self._discrepancies_presenter.packageMutations.connect(self._onPackageMutations) + + self._download_presenter = DownloadPresenter(app) # type: DownloadPresenter + + self._license_presenter = LicensePresenter(app) # type: LicensePresenter + self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers) + + self._restart_presenter = RestartApplicationPresenter(app) + + def _onDiscrepancies(self, model: SubscribedPackagesModel) -> None: + plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId())) + self._discrepancies_presenter.present(plugin_path, model) + + def _onPackageMutations(self, mutations: SubscribedPackagesModel) -> None: + self._download_presenter = self._download_presenter.resetCopy() + self._download_presenter.done.connect(self._onDownloadFinished) + self._download_presenter.download(mutations) + + def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None: + """Called when a set of packages have finished downloading + + :param success_items:: Dict[package_id, Dict[str, str]] + :param error_items:: List[package_id] + """ + if error_items: + message = i18n_catalog.i18nc("@info:generic", "{} plugins failed to download".format(len(error_items))) + self._showErrorMessage(message) + + plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId())) + self._license_presenter = self._license_presenter.resetCopy() + self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers) + self._license_presenter.present(plugin_path, success_items) + + # Called when user has accepted / declined all licenses for the downloaded packages + def _onLicenseAnswers(self, answers: List[Dict[str, Any]]) -> None: + has_changes = False # True when at least one package is installed + + for item in answers: + if item["accepted"]: + # install and subscribe packages + if not self._package_manager.installPackage(item["package_path"]): + message = "Could not install {}".format(item["package_id"]) + self._showErrorMessage(message) + continue + has_changes = True + else: + self._cloud_api.unsubscribe(item["package_id"]) + # delete temp file + try: + os.remove(item["package_path"]) + except EnvironmentError as e: # File was already removed, no access rights, etc. + Logger.error("Can't delete temporary package file: {err}".format(err = str(e))) + + if has_changes: + self._restart_presenter.present() + + def _showErrorMessage(self, text: str): + """Logs an error and shows it to the user""" + + Logger.error(text) + Message(text, lifetime = 0, message_type = Message.MessageType.ERROR).show() diff --git a/plugins/Marketplace/CloudSync/__init__.py b/plugins/Marketplace/CloudSync/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/Marketplace/__init__.py b/plugins/Marketplace/__init__.py index bd65062ba6..ef1beab33e 100644 --- a/plugins/Marketplace/__init__.py +++ b/plugins/Marketplace/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - +from .CloudSync.SyncOrchestrator import SyncOrchestrator from .Marketplace import Marketplace def getMetaData(): @@ -14,4 +14,4 @@ def register(app): """ Register the plug-in object with Uranium. """ - return { "extension": Marketplace() } + return { "extension": [Marketplace(), SyncOrchestrator(app)] } diff --git a/plugins/Marketplace/resources/qml/CompatibilityDialog.qml b/plugins/Marketplace/resources/qml/CompatibilityDialog.qml new file mode 100644 index 0000000000..2e8595c811 --- /dev/null +++ b/plugins/Marketplace/resources/qml/CompatibilityDialog.qml @@ -0,0 +1,154 @@ +// Copyright (c) 2022 Ultimaker B.V. +// Marketplace is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.10 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.3 + +import UM 1.1 as UM +import Cura 1.6 as Cura + + +UM.Dialog +{ + visible: true + title: catalog.i18nc("@title", "Changes from your account") + width: UM.Theme.getSize("popup_dialog").width + height: UM.Theme.getSize("popup_dialog").height + minimumWidth: width + maximumWidth: minimumWidth + minimumHeight: height + maximumHeight: minimumHeight + margin: 0 + + property string actionButtonText: subscribedPackagesModel.hasIncompatiblePackages && !subscribedPackagesModel.hasCompatiblePackages ? catalog.i18nc("@button", "Dismiss") : catalog.i18nc("@button", "Next") + + Rectangle + { + id: root + anchors.fill: parent + color: UM.Theme.getColor("main_background") + + UM.I18nCatalog + { + id: catalog + name: "cura" + } + + ScrollView + { + width: parent.width + height: parent.height - nextButton.height - nextButton.anchors.margins * 2 // We want some leftover space for the button at the bottom + clip: true + + Column + { + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_margin").width + + // Compatible packages + Label + { + font: UM.Theme.getFont("default") + text: catalog.i18nc("@label", "The following packages will be added:") + visible: subscribedPackagesModel.hasCompatiblePackages + color: UM.Theme.getColor("text") + height: contentHeight + UM.Theme.getSize("default_margin").height + } + Repeater + { + model: subscribedPackagesModel + Component + { + Item + { + width: parent.width + property int lineHeight: 60 + visible: model.is_compatible + height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the compatible packages here + Image + { + id: packageIcon + source: model.icon_url || "../../images/placeholder.svg" + height: lineHeight + width: height + sourceSize.height: height + sourceSize.width: width + mipmap: true + fillMode: Image.PreserveAspectFit + } + Label + { + text: model.display_name + font: UM.Theme.getFont("medium_bold") + anchors.left: packageIcon.right + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.verticalCenter: packageIcon.verticalCenter + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + } + } + + // Incompatible packages + Label + { + font: UM.Theme.getFont("default") + text: catalog.i18nc("@label", "The following packages can not be installed because of an incompatible Cura version:") + visible: subscribedPackagesModel.hasIncompatiblePackages + color: UM.Theme.getColor("text") + height: contentHeight + UM.Theme.getSize("default_margin").height + } + Repeater + { + model: subscribedPackagesModel + Component + { + Item + { + width: parent.width + property int lineHeight: 60 + visible: !model.is_compatible && !model.is_dismissed + height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the incompatible packages here + Image + { + id: packageIcon + source: model.icon_url || "../../images/placeholder.svg" + height: lineHeight + width: height + sourceSize.height: height + sourceSize.width: width + mipmap: true + fillMode: Image.PreserveAspectFit + } + Label + { + text: model.display_name + font: UM.Theme.getFont("medium_bold") + anchors.left: packageIcon.right + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.verticalCenter: packageIcon.verticalCenter + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + } + } + } + + } // End of ScrollView + + Cura.PrimaryButton + { + id: nextButton + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: UM.Theme.getSize("default_margin").height + text: actionButtonText + onClicked: accept() + leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width + rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width + } + } +} diff --git a/plugins/Marketplace/resources/qml/MarketplaceLicenseDialog.qml b/plugins/Marketplace/resources/qml/MarketplaceLicenseDialog.qml new file mode 100644 index 0000000000..b4f1ef3dea --- /dev/null +++ b/plugins/Marketplace/resources/qml/MarketplaceLicenseDialog.qml @@ -0,0 +1,114 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Marketplace is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.10 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 + +import UM 1.1 as UM +import Cura 1.6 as Cura + +UM.Dialog +{ + id: licenseDialog + title: licenseModel.dialogTitle + Component.onCompleted: + { + for(var p in licenseModel) + console.log(p + ": " + item[p]); + } + + 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() } + } + ] +} -- cgit v1.2.3 From 3ed182cd7449fe3b696012598fd53446096c04ce Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2022 11:08:54 +0200 Subject: Rename MarketplaceLicenseDialog to MultipleLicenceDialog CURA-9146 --- plugins/Marketplace/CloudSync/LicensePresenter.py | 8 +- .../resources/qml/MarketplaceLicenseDialog.qml | 114 --------------------- .../resources/qml/MultipleLicenseDialog.qml | 101 ++++++++++++++++++ 3 files changed, 105 insertions(+), 118 deletions(-) delete mode 100644 plugins/Marketplace/resources/qml/MarketplaceLicenseDialog.qml create mode 100644 plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml diff --git a/plugins/Marketplace/CloudSync/LicensePresenter.py b/plugins/Marketplace/CloudSync/LicensePresenter.py index ce0d198c01..6f581e2c4c 100644 --- a/plugins/Marketplace/CloudSync/LicensePresenter.py +++ b/plugins/Marketplace/CloudSync/LicensePresenter.py @@ -28,7 +28,7 @@ class LicensePresenter(QObject): super().__init__() self._presented = False """Whether present() has been called and state is expected to be initialized""" - self._catalog = i18nCatalog("cura") + self._dialog = None # type: Optional[QObject] self._package_manager = app.getPackageManager() # type: PackageManager # Emits List[Dict[str, [Any]] containing for example @@ -37,13 +37,15 @@ class LicensePresenter(QObject): self._current_package_idx = 0 self._package_models = [] # type: List[Dict] + + self._catalog = i18nCatalog("cura") decline_button_text = self._catalog.i18nc("@button", "Decline and remove from account") self._license_model = LicenseModel(decline_button_text=decline_button_text) # type: LicenseModel self._page_count = 0 self._app = app - self._compatibility_dialog_path = "resources/qml/MarketplaceLicenseDialog.qml" + self._compatibility_dialog_path = "resources/qml/MultipleLicenseDialog.qml" def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None: """Show a license dialog for multiple packages where users can read a license and accept or decline them @@ -90,7 +92,6 @@ class LicensePresenter(QObject): self._checkNextPage() def _initState(self, packages: Dict[str, Dict[str, Any]]) -> None: - implicitly_accepted_count = 0 for package_id, item in packages.items(): @@ -114,7 +115,6 @@ class LicensePresenter(QObject): self._page_count = len(self._package_models) - implicitly_accepted_count self._license_model.setPageCount(self._page_count) - def _presentCurrentPackage(self) -> None: package_model = self._package_models[self._current_package_idx] package_info = self._package_manager.getPackageInfo(package_model["package_path"]) diff --git a/plugins/Marketplace/resources/qml/MarketplaceLicenseDialog.qml b/plugins/Marketplace/resources/qml/MarketplaceLicenseDialog.qml deleted file mode 100644 index b4f1ef3dea..0000000000 --- a/plugins/Marketplace/resources/qml/MarketplaceLicenseDialog.qml +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2021 Ultimaker B.V. -// Marketplace is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Window 2.2 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.3 - -import UM 1.1 as UM -import Cura 1.6 as Cura - -UM.Dialog -{ - id: licenseDialog - title: licenseModel.dialogTitle - Component.onCompleted: - { - for(var p in licenseModel) - console.log(p + ": " + item[p]); - } - - 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() } - } - ] -} diff --git a/plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml b/plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml new file mode 100644 index 0000000000..9bee5a030e --- /dev/null +++ b/plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml @@ -0,0 +1,101 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Marketplace is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.10 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 + +import UM 1.5 as UM +import Cura 1.6 as Cura + +UM.Dialog +{ + id: licenseDialog + title: licenseModel.dialogTitle + + minimumWidth: UM.Theme.getSize("modal_window_minimum").width + minimumHeight: UM.Theme.getSize("modal_window_minimum").height + width: minimumWidth + height: minimumHeight + backgroundColor: UM.Theme.getColor("main_background") + margin: UM.Theme.getSize("default_margin").width + + ColumnLayout + { + anchors.fill: parent + spacing: UM.Theme.getSize("thick_margin").height + + UM.I18nCatalog { id: catalog; name: "cura" } + + UM.Label + { + id: licenseHeader + Layout.fillWidth: true + text: catalog.i18nc("@label", "You need to accept the license to install the package") + } + + 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 + } + + UM.Label + { + id: packageName + text: licenseModel.packageName + + font.bold: true + anchors.verticalCenter: icon.verticalCenter + height: contentHeight + } + } + + 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() } + } + ] +} -- cgit v1.2.3 From 05263b9a5869a28f3d7d90329e6c0cfe31ed2f03 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2022 11:17:38 +0200 Subject: Fix layout of dialog CURA-9146 --- .../Marketplace/resources/qml/CompatibilityDialog.qml | 18 +++++------------- .../resources/qml/MultipleLicenseDialog.qml | 9 +++------ 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/plugins/Marketplace/resources/qml/CompatibilityDialog.qml b/plugins/Marketplace/resources/qml/CompatibilityDialog.qml index 2e8595c811..e96737660b 100644 --- a/plugins/Marketplace/resources/qml/CompatibilityDialog.qml +++ b/plugins/Marketplace/resources/qml/CompatibilityDialog.qml @@ -5,7 +5,7 @@ import QtQuick 2.10 import QtQuick.Window 2.2 import QtQuick.Controls 2.3 -import UM 1.1 as UM +import UM 1.5 as UM import Cura 1.6 as Cura @@ -47,12 +47,10 @@ UM.Dialog anchors.margins: UM.Theme.getSize("default_margin").width // Compatible packages - Label + UM.Label { - font: UM.Theme.getFont("default") text: catalog.i18nc("@label", "The following packages will be added:") visible: subscribedPackagesModel.hasCompatiblePackages - color: UM.Theme.getColor("text") height: contentHeight + UM.Theme.getSize("default_margin").height } Repeater @@ -77,14 +75,13 @@ UM.Dialog mipmap: true fillMode: Image.PreserveAspectFit } - Label + UM.Label { text: model.display_name font: UM.Theme.getFont("medium_bold") anchors.left: packageIcon.right anchors.leftMargin: UM.Theme.getSize("default_margin").width anchors.verticalCenter: packageIcon.verticalCenter - color: UM.Theme.getColor("text") elide: Text.ElideRight } } @@ -92,12 +89,10 @@ UM.Dialog } // Incompatible packages - Label + UM.Label { - font: UM.Theme.getFont("default") text: catalog.i18nc("@label", "The following packages can not be installed because of an incompatible Cura version:") visible: subscribedPackagesModel.hasIncompatiblePackages - color: UM.Theme.getColor("text") height: contentHeight + UM.Theme.getSize("default_margin").height } Repeater @@ -122,14 +117,13 @@ UM.Dialog mipmap: true fillMode: Image.PreserveAspectFit } - Label + UM.Label { text: model.display_name font: UM.Theme.getFont("medium_bold") anchors.left: packageIcon.right anchors.leftMargin: UM.Theme.getSize("default_margin").width anchors.verticalCenter: packageIcon.verticalCenter - color: UM.Theme.getColor("text") elide: Text.ElideRight } } @@ -147,8 +141,6 @@ UM.Dialog anchors.margins: UM.Theme.getSize("default_margin").height text: actionButtonText onClicked: accept() - leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width - rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width } } } diff --git a/plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml b/plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml index 9bee5a030e..2da92a6a5e 100644 --- a/plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml +++ b/plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml @@ -46,7 +46,7 @@ UM.Dialog Image { id: icon - width: 30 * screenScaleFactor + width: UM.Theme.getSize("card_icon").width height: width sourceSize.width: width sourceSize.height: height @@ -81,11 +81,8 @@ UM.Dialog [ 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() } + onClicked: handler.onLicenseAccepted() } ] @@ -95,7 +92,7 @@ UM.Dialog { id: declineButton text: licenseModel.declineButtonText - onClicked: { handler.onLicenseDeclined() } + onClicked: handler.onLicenseDeclined() } ] } -- cgit v1.2.3 From 3aff3d5825d1de932d0443a3eee447b45c078189 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2022 11:19:58 +0200 Subject: Fix "read more" not being translated Yeah, i know, this breaks the stringfreeze. But this wasn't translated in the first place. So the actual effect here is zero (but at least it will be translated in the next round!) CURA-9146 --- plugins/Marketplace/resources/qml/OnboardBanner.qml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index 278c50ab57..cd98f7df68 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -19,7 +19,6 @@ Rectangle implicitHeight: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height color: UM.Theme.getColor("action_panel_secondary") - // Icon UM.ColorImage { id: onboardingIcon @@ -33,7 +32,6 @@ Rectangle height: UM.Theme.getSize("banner_icon_size").height } - // Close button UM.SimpleButton { id: onboardingClose @@ -52,8 +50,8 @@ Rectangle onClicked: onRemove() } - // Body - Label { + UM.Label + { id: infoText anchors { @@ -63,11 +61,8 @@ Rectangle margins: UM.Theme.getSize("default_margin").width } - font: UM.Theme.getFont("default") - renderType: Text.NativeRendering color: UM.Theme.getColor("primary_text") - wrapMode: Text.Wrap elide: Text.ElideRight onLineLaidOut: (line) => @@ -102,7 +97,7 @@ Rectangle id: readMoreButton anchors.left: infoText.left anchors.bottom: infoText.bottom - text: "Learn More" + text: catalog.i18nc("@button:label", "Learn More") textFont: UM.Theme.getFont("default") textColor: infoText.color leftPadding: 0 -- cgit v1.2.3 From 773ff5fa7e544292013db4be48bf80f67af65fb0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Apr 2022 11:21:12 +0200 Subject: Remove unneeded renderType CURA-9146 --- plugins/Marketplace/resources/qml/OnboardBanner.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml index cd98f7df68..8217a7d03f 100644 --- a/plugins/Marketplace/resources/qml/OnboardBanner.qml +++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml @@ -61,7 +61,6 @@ Rectangle margins: UM.Theme.getSize("default_margin").width } - renderType: Text.NativeRendering color: UM.Theme.getColor("primary_text") elide: Text.ElideRight -- cgit v1.2.3 From d0a35bad3d390a7a338e897ef7f936879d28e1bc Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Thu, 21 Apr 2022 11:29:42 +0200 Subject: Use updated type definitions CURA-9146 --- plugins/Marketplace/CloudApiModel.py | 10 +++++----- plugins/Marketplace/CloudSync/CloudApiClient.py | 2 +- plugins/Marketplace/CloudSync/CloudPackageChecker.py | 4 ++-- plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py | 2 +- plugins/Marketplace/CloudSync/DownloadPresenter.py | 4 ++-- plugins/Marketplace/CloudSync/LicensePresenter.py | 8 ++++---- plugins/Marketplace/CloudSync/SyncOrchestrator.py | 10 +++++----- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/plugins/Marketplace/CloudApiModel.py b/plugins/Marketplace/CloudApiModel.py index bef37d8173..a85de7c3c5 100644 --- a/plugins/Marketplace/CloudApiModel.py +++ b/plugins/Marketplace/CloudApiModel.py @@ -5,14 +5,14 @@ from cura.UltimakerCloud import UltimakerCloudConstants class CloudApiModel: - sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int] - cloud_api_version = UltimakerCloudConstants.CuraCloudAPIVersion # type: str - cloud_api_root = UltimakerCloudConstants.CuraCloudAPIRoot # type: str - api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format( + sdk_version: Union[str, int] = ApplicationMetadata.CuraSDKVersion + cloud_api_version: str = UltimakerCloudConstants.CuraCloudAPIVersion + cloud_api_root: str = UltimakerCloudConstants.CuraCloudAPIRoot + api_url: str = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format( cloud_api_root = cloud_api_root, cloud_api_version = cloud_api_version, sdk_version = sdk_version - ) # type: str + ) # https://api.ultimaker.com/cura-packages/v1/user/packages api_url_user_packages = "{cloud_api_root}/cura-packages/v{cloud_api_version}/user/packages".format( diff --git a/plugins/Marketplace/CloudSync/CloudApiClient.py b/plugins/Marketplace/CloudSync/CloudApiClient.py index 21eb1bdbd2..fd4f9969ac 100644 --- a/plugins/Marketplace/CloudSync/CloudApiClient.py +++ b/plugins/Marketplace/CloudSync/CloudApiClient.py @@ -27,7 +27,7 @@ class CloudApiClient: if self.__instance is not None: raise RuntimeError("This is a Singleton. use getInstance()") - self._scope = JsonDecoratorScope(UltimakerCloudScope(app)) # type: JsonDecoratorScope + self._scope: JsonDecoratorScope = JsonDecoratorScope(UltimakerCloudScope(app)) app.getPackageManager().packageInstalled.connect(self._onPackageInstalled) diff --git a/plugins/Marketplace/CloudSync/CloudPackageChecker.py b/plugins/Marketplace/CloudSync/CloudPackageChecker.py index b27b2e0ab3..a250358606 100644 --- a/plugins/Marketplace/CloudSync/CloudPackageChecker.py +++ b/plugins/Marketplace/CloudSync/CloudPackageChecker.py @@ -29,10 +29,10 @@ class CloudPackageChecker(QObject): super().__init__() self.discrepancies = Signal() # Emits SubscribedPackagesModel - self._application = application # type: CuraApplication + self._application: CuraApplication = application self._scope = JsonDecoratorScope(UltimakerCloudScope(application)) self._model = SubscribedPackagesModel() - self._message = None # type: Optional[Message] + self._message: Optional[Message] = None self._application.initializationFinished.connect(self._onAppInitialized) self._i18n_catalog = i18nCatalog("cura") diff --git a/plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py b/plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py index 45ce70b32c..fc2a8db505 100644 --- a/plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py +++ b/plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py @@ -21,7 +21,7 @@ class DiscrepanciesPresenter(QObject): self._app = app self._package_manager = app.getPackageManager() - self._dialog = None # type: Optional[QObject] + self._dialog: Optional[QObject] = None self._compatibility_dialog_path = "resources/qml/CompatibilityDialog.qml" def present(self, plugin_path: str, model: SubscribedPackagesModel) -> None: diff --git a/plugins/Marketplace/CloudSync/DownloadPresenter.py b/plugins/Marketplace/CloudSync/DownloadPresenter.py index e7ac682d69..c9e1356e75 100644 --- a/plugins/Marketplace/CloudSync/DownloadPresenter.py +++ b/plugins/Marketplace/CloudSync/DownloadPresenter.py @@ -37,8 +37,8 @@ class DownloadPresenter: self._started = False self._progress_message = self._createProgressMessage() - self._progress = {} # type: Dict[str, Dict[str, Any]] # package_id, Dict - self._error = [] # type: List[str] # package_id + self._progress: Dict[str, Dict[str, Any]] = {} + self._error: List[str] = [] def download(self, model: SubscribedPackagesModel) -> None: if self._started: diff --git a/plugins/Marketplace/CloudSync/LicensePresenter.py b/plugins/Marketplace/CloudSync/LicensePresenter.py index 6f581e2c4c..d678f2b882 100644 --- a/plugins/Marketplace/CloudSync/LicensePresenter.py +++ b/plugins/Marketplace/CloudSync/LicensePresenter.py @@ -29,18 +29,18 @@ class LicensePresenter(QObject): self._presented = False """Whether present() has been called and state is expected to be initialized""" - self._dialog = None # type: Optional[QObject] - self._package_manager = app.getPackageManager() # type: PackageManager + self._dialog: Optional[QObject] = None + self._package_manager: PackageManager = app.getPackageManager() # Emits List[Dict[str, [Any]] containing for example # [{ "package_id": "BarbarianPlugin", "package_path" : "/tmp/dg345as", "accepted" : True }] self.licenseAnswers = Signal() self._current_package_idx = 0 - self._package_models = [] # type: List[Dict] + self._package_models: List[Dict] = [] self._catalog = i18nCatalog("cura") decline_button_text = self._catalog.i18nc("@button", "Decline and remove from account") - self._license_model = LicenseModel(decline_button_text=decline_button_text) # type: LicenseModel + self._license_model: LicenseModel = LicenseModel(decline_button_text=decline_button_text) self._page_count = 0 self._app = app diff --git a/plugins/Marketplace/CloudSync/SyncOrchestrator.py b/plugins/Marketplace/CloudSync/SyncOrchestrator.py index bfa449b40a..c67c79eb74 100644 --- a/plugins/Marketplace/CloudSync/SyncOrchestrator.py +++ b/plugins/Marketplace/CloudSync/SyncOrchestrator.py @@ -45,17 +45,17 @@ class SyncOrchestrator(Extension): self._package_manager = app.getPackageManager() # Keep a reference to the CloudApiClient. it watches for installed packages and subscribes to them - self._cloud_api = CloudApiClient.getInstance(app) # type: CloudApiClient + self._cloud_api: CloudApiClient = CloudApiClient.getInstance(app) - self._checker = CloudPackageChecker(app) # type: CloudPackageChecker + self._checker: CloudPackageChecker = CloudPackageChecker(app) self._checker.discrepancies.connect(self._onDiscrepancies) - self._discrepancies_presenter = DiscrepanciesPresenter(app) # type: DiscrepanciesPresenter + self._discrepancies_presenter: DiscrepanciesPresenter = DiscrepanciesPresenter(app) self._discrepancies_presenter.packageMutations.connect(self._onPackageMutations) - self._download_presenter = DownloadPresenter(app) # type: DownloadPresenter + self._download_presenter: DownloadPresenter = DownloadPresenter(app) - self._license_presenter = LicensePresenter(app) # type: LicensePresenter + self._license_presenter: LicensePresenter = LicensePresenter(app) self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers) self._restart_presenter = RestartApplicationPresenter(app) -- cgit v1.2.3 From 66fde9100ca16730d4228b68f524263ef38827ea Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Thu, 21 Apr 2022 11:32:03 +0200 Subject: Use QML defined enum in sync state These are the enums from cura/API/account.py somehow exposing these enums from python to QML doesn't work properly anymore A better solution should be found in the very near future CURA-9146 --- resources/qml/Account/SyncState.qml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/resources/qml/Account/SyncState.qml b/resources/qml/Account/SyncState.qml index 9c978b4607..b558714376 100644 --- a/resources/qml/Account/SyncState.qml +++ b/resources/qml/Account/SyncState.qml @@ -13,31 +13,38 @@ Row // Sync state icon + message height: childrenRect.height spacing: UM.Theme.getSize("narrow_margin").height + // These are the enums from cura/API/account.py + // somehow exposing these enums from python to QML doesn't work properly anymore + property var _Cura_AccountSyncState_SYNCING: 0 + property var _Cura_AccountSyncState_SUCCESS: 1 + property var _Cura_AccountSyncState_ERROR: 2 + property var _Cura_AccountSyncState_IDLE: 3 + states: [ State { name: "idle" - when: syncState == Cura.AccountSyncState.IDLE + when: syncState == _Cura_AccountSyncState_IDLE PropertyChanges { target: icon; source: UM.Theme.getIcon("ArrowDoubleCircleRight")} }, State { name: "syncing" - when: syncState == Cura.AccountSyncState.SYNCING + when: syncState == _Cura_AccountSyncState_SYNCING PropertyChanges { target: icon; source: UM.Theme.getIcon("ArrowDoubleCircleRight") } PropertyChanges { target: stateLabel; text: catalog.i18nc("@label", "Checking...")} }, State { name: "up_to_date" - when: syncState == Cura.AccountSyncState.SUCCESS + when: syncState == _Cura_AccountSyncState_SUCCESS PropertyChanges { target: icon; source: UM.Theme.getIcon("CheckCircle") } PropertyChanges { target: stateLabel; text: catalog.i18nc("@label", "Account synced")} }, State { name: "error" - when: syncState == Cura.AccountSyncState.ERROR + when: syncState == _Cura_AccountSyncState_ERROR PropertyChanges { target: icon; source: UM.Theme.getIcon("Warning") } PropertyChanges { target: stateLabel; text: catalog.i18nc("@label", "Something went wrong...")} } -- cgit v1.2.3 From 7a3af720a84268a5244e8322dc77a67de97f60d7 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Thu, 21 Apr 2022 11:32:46 +0200 Subject: Resolve urls through qml CURA-9146 --- plugins/Marketplace/resources/qml/CompatibilityDialog.qml | 4 ++-- plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/Marketplace/resources/qml/CompatibilityDialog.qml b/plugins/Marketplace/resources/qml/CompatibilityDialog.qml index e96737660b..837c4defa5 100644 --- a/plugins/Marketplace/resources/qml/CompatibilityDialog.qml +++ b/plugins/Marketplace/resources/qml/CompatibilityDialog.qml @@ -67,7 +67,7 @@ UM.Dialog Image { id: packageIcon - source: model.icon_url || "../../images/placeholder.svg" + source: model.icon_url || Qt.resolvedUrl("../../images/placeholder.svg") height: lineHeight width: height sourceSize.height: height @@ -109,7 +109,7 @@ UM.Dialog Image { id: packageIcon - source: model.icon_url || "../../images/placeholder.svg" + source: model.icon_url || Qt.resolvedUrl("../../images/placeholder.svg") height: lineHeight width: height sourceSize.height: height diff --git a/plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml b/plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml index 2da92a6a5e..aa11d3a7e9 100644 --- a/plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml +++ b/plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml @@ -51,7 +51,7 @@ UM.Dialog sourceSize.width: width sourceSize.height: height fillMode: Image.PreserveAspectFit - source: licenseModel.iconUrl || "../../images/placeholder.svg" + source: licenseModel.iconUrl || Qt.resolvedUrl("../../images/placeholder.svg") mipmap: true } -- cgit v1.2.3 From 9a3d089bd90ec11bf8ce649797d2fd4583a5f631 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Thu, 21 Apr 2022 11:34:16 +0200 Subject: Properly add and remove menu items Not really part of the ticket but logging was complaining CURA-9146 --- resources/qml/Menus/PrinterTypeMenu.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/qml/Menus/PrinterTypeMenu.qml b/resources/qml/Menus/PrinterTypeMenu.qml index 17986ac1b8..1447113208 100644 --- a/resources/qml/Menus/PrinterTypeMenu.qml +++ b/resources/qml/Menus/PrinterTypeMenu.qml @@ -28,7 +28,7 @@ Cura.Menu Cura.MachineManager.switchPrinterType(modelData.machine_type) } } - onObjectAdded: menu.insertItem(index, object) - onObjectRemoved: menu.removeItem(object) + onObjectAdded: function(index, object) { return menu.insertItem(index, object); } + onObjectRemoved: function(object) { return menu.removeItem(object); } } } -- cgit v1.2.3 From 345f0ea41c5b92635d10cefe0cb2fedec6d96c0b Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 21 Apr 2022 13:18:03 +0200 Subject: Fix copyright headers and code style a bit Contributes to issue CURA-9146. --- plugins/Marketplace/CloudApiModel.py | 6 +++--- plugins/Marketplace/CloudSync/CloudApiClient.py | 3 +++ plugins/Marketplace/CloudSync/CloudPackageChecker.py | 2 +- plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py | 3 +++ plugins/Marketplace/CloudSync/DownloadPresenter.py | 2 +- plugins/Marketplace/CloudSync/LicenseModel.py | 3 +++ plugins/Marketplace/CloudSync/LicensePresenter.py | 2 +- plugins/Marketplace/CloudSync/RestartApplicationPresenter.py | 3 +++ plugins/Marketplace/CloudSync/SubscribedPackagesModel.py | 2 +- plugins/Marketplace/CloudSync/SyncOrchestrator.py | 2 +- 10 files changed, 20 insertions(+), 8 deletions(-) diff --git a/plugins/Marketplace/CloudApiModel.py b/plugins/Marketplace/CloudApiModel.py index a85de7c3c5..90173c5390 100644 --- a/plugins/Marketplace/CloudApiModel.py +++ b/plugins/Marketplace/CloudApiModel.py @@ -16,8 +16,8 @@ class CloudApiModel: # https://api.ultimaker.com/cura-packages/v1/user/packages api_url_user_packages = "{cloud_api_root}/cura-packages/v{cloud_api_version}/user/packages".format( - cloud_api_root=cloud_api_root, - cloud_api_version=cloud_api_version, + cloud_api_root = cloud_api_root, + cloud_api_version = cloud_api_version, ) @classmethod @@ -25,5 +25,5 @@ class CloudApiModel: """https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id}""" return (CloudApiModel.api_url_user_packages + "/{package_id}").format( - package_id=package_id + package_id = package_id ) diff --git a/plugins/Marketplace/CloudSync/CloudApiClient.py b/plugins/Marketplace/CloudSync/CloudApiClient.py index fd4f9969ac..cf83567f17 100644 --- a/plugins/Marketplace/CloudSync/CloudApiClient.py +++ b/plugins/Marketplace/CloudSync/CloudApiClient.py @@ -1,3 +1,6 @@ +# Copyright (c) 2022 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + from UM.Logger import Logger from UM.TaskManagement.HttpRequestManager import HttpRequestManager from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope diff --git a/plugins/Marketplace/CloudSync/CloudPackageChecker.py b/plugins/Marketplace/CloudSync/CloudPackageChecker.py index a250358606..53087e0502 100644 --- a/plugins/Marketplace/CloudSync/CloudPackageChecker.py +++ b/plugins/Marketplace/CloudSync/CloudPackageChecker.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Ultimaker B.V. +# Copyright (c) 2022 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import json diff --git a/plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py b/plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py index fc2a8db505..182cc75929 100644 --- a/plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py +++ b/plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py @@ -1,3 +1,6 @@ +# Copyright (c) 2022 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + import os from typing import Optional diff --git a/plugins/Marketplace/CloudSync/DownloadPresenter.py b/plugins/Marketplace/CloudSync/DownloadPresenter.py index c9e1356e75..8325c27eb7 100644 --- a/plugins/Marketplace/CloudSync/DownloadPresenter.py +++ b/plugins/Marketplace/CloudSync/DownloadPresenter.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Ultimaker B.V. +# Copyright (c) 2022 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import tempfile diff --git a/plugins/Marketplace/CloudSync/LicenseModel.py b/plugins/Marketplace/CloudSync/LicenseModel.py index 8c036c48b7..777ecca1e9 100644 --- a/plugins/Marketplace/CloudSync/LicenseModel.py +++ b/plugins/Marketplace/CloudSync/LicenseModel.py @@ -1,3 +1,6 @@ +# Copyright (c) 2022 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal from UM.i18n import i18nCatalog diff --git a/plugins/Marketplace/CloudSync/LicensePresenter.py b/plugins/Marketplace/CloudSync/LicensePresenter.py index d678f2b882..aecf70e061 100644 --- a/plugins/Marketplace/CloudSync/LicensePresenter.py +++ b/plugins/Marketplace/CloudSync/LicensePresenter.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021 Ultimaker B.V. +# Copyright (c) 2022 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os diff --git a/plugins/Marketplace/CloudSync/RestartApplicationPresenter.py b/plugins/Marketplace/CloudSync/RestartApplicationPresenter.py index 8776d1782a..1e1ebc33dc 100644 --- a/plugins/Marketplace/CloudSync/RestartApplicationPresenter.py +++ b/plugins/Marketplace/CloudSync/RestartApplicationPresenter.py @@ -1,3 +1,6 @@ +# Copyright (c) 2022 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + from UM import i18nCatalog from UM.Message import Message from cura.CuraApplication import CuraApplication diff --git a/plugins/Marketplace/CloudSync/SubscribedPackagesModel.py b/plugins/Marketplace/CloudSync/SubscribedPackagesModel.py index fcce62fab5..96b1d58bbf 100644 --- a/plugins/Marketplace/CloudSync/SubscribedPackagesModel.py +++ b/plugins/Marketplace/CloudSync/SubscribedPackagesModel.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Ultimaker B.V. +# Copyright (c) 2022 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from PyQt6.QtCore import Qt, pyqtProperty diff --git a/plugins/Marketplace/CloudSync/SyncOrchestrator.py b/plugins/Marketplace/CloudSync/SyncOrchestrator.py index c67c79eb74..a68d3c8fa2 100644 --- a/plugins/Marketplace/CloudSync/SyncOrchestrator.py +++ b/plugins/Marketplace/CloudSync/SyncOrchestrator.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021 Ultimaker B.V. +# Copyright (c) 2022 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os -- cgit v1.2.3