diff --git a/plugins/Toolbox/__init__.py b/plugins/Toolbox/__init__.py index 5bdb1c5dce..212e70fd36 100644 --- a/plugins/Toolbox/__init__.py +++ b/plugins/Toolbox/__init__.py @@ -2,7 +2,8 @@ # Toolbox is released under the terms of the LGPLv3 or higher. from .src import Toolbox -from .src.SubscriptionChecker import SubscriptionChecker +from plugins.Toolbox.src.CloudSync.CloudPackageChecker import CloudPackageChecker +from .src.CloudSync.SyncOrchestrator import SyncOrchestrator def getMetaData(): @@ -11,6 +12,5 @@ def getMetaData(): def register(app): return { - "extension": Toolbox.Toolbox(app), - "subscription_checker": SubscriptionChecker(app) + "extension": [Toolbox.Toolbox(app), SyncOrchestrator(app)] } diff --git a/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml b/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml index 85664898a8..b7eed1b2e3 100644 --- a/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml +++ b/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml @@ -139,6 +139,7 @@ UM.Dialog{ anchors.right: parent.right anchors.margins: UM.Theme.getSize("default_margin").height text: catalog.i18nc("@button", "Next") + onClicked: accept() } } } diff --git a/plugins/Toolbox/src/SubscriptionChecker.py b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py similarity index 81% rename from plugins/Toolbox/src/SubscriptionChecker.py rename to plugins/Toolbox/src/CloudSync/CloudPackageChecker.py index b096f5a9c0..0f31b90959 100644 --- a/plugins/Toolbox/src/SubscriptionChecker.py +++ b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py @@ -1,26 +1,25 @@ import json -import os -from typing import Dict, Optional +from typing import Optional from PyQt5.QtCore import QObject from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest -from UM.Extension import Extension from UM.Logger import Logger from UM.Message import Message -from UM.PluginRegistry import PluginRegistry +from UM.Signal import Signal from UM.TaskManagement.HttpRequestScope import UltimakerCloudScope from cura.CuraApplication import CuraApplication from plugins.Toolbox.src.CloudApiModel import CloudApiModel -from plugins.Toolbox.src.SubscribedPackagesModel import SubscribedPackagesModel +from plugins.Toolbox.src.CloudSync.SubscribedPackagesModel import SubscribedPackagesModel from plugins.Toolbox.src.Toolbox import i18n_catalog -class SubscriptionChecker(QObject, Extension): +class CloudPackageChecker(QObject): def __init__(self, application: CuraApplication) -> None: super().__init__() + self.discrepancies = Signal() # Emits SubscribedPackagesModel self._application = application # type: CuraApplication self._scope = UltimakerCloudScope(application) self._model = SubscribedPackagesModel() @@ -39,7 +38,7 @@ class SubscriptionChecker(QObject, Extension): def _fetchUserSubscribedPackages(self): if self._application.getCuraAPI().account.isLoggedIn: - self._getUserPackages("subscribed_packages") + self._getUserPackages() def _handleCompatibilityData(self, json_data) -> None: user_subscribed_packages = [plugin["package_id"] for plugin in json_data] @@ -53,9 +52,9 @@ class SubscriptionChecker(QObject, Extension): self._model.update() if package_discrepancy: - self._handlePackageDiscrepancies(package_discrepancy) + self._handlePackageDiscrepancies() - def _handlePackageDiscrepancies(self, package_discrepancy): + def _handlePackageDiscrepancies(self): Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages") sync_message = Message(i18n_catalog.i18nc( "@info:generic", @@ -72,14 +71,10 @@ class SubscriptionChecker(QObject, Extension): def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None: sync_message.hide() - compatibility_dialog_path = "resources/qml/dialogs/CompatibilityDialog.qml" - plugin_path_prefix = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) - if plugin_path_prefix: - path = os.path.join(plugin_path_prefix, compatibility_dialog_path) - self.compatibility_dialog_view = self._application.createQmlComponent(path, {"subscribedPackagesModel": self._model}) + self.discrepancies.emit(self._model) - def _getUserPackages(self, request_type: str) -> None: - Logger.log("d", "Requesting [%s] metadata from server.", request_type) + def _getUserPackages(self) -> None: + Logger.log("d", "Requesting subscribed packages metadata from server.") url = CloudApiModel.api_url_user_packages self._application.getHttpRequestManager().get(url, diff --git a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py b/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py new file mode 100644 index 0000000000..e0a6cce558 --- /dev/null +++ b/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py @@ -0,0 +1,33 @@ +import os +from typing import Optional + +from PyQt5.QtCore import QObject + +from UM.Qt.QtApplication import QtApplication +from UM.Signal import Signal +from plugins.Toolbox.src.CloudSync.SubscribedPackagesModel import SubscribedPackagesModel + + +## 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. +class DiscrepanciesPresenter(QObject): + + def __init__(self, app: QtApplication): + super().__init__(app) + + self.packageMutations = Signal() # {"SettingsGuide" : "install", "PrinterSettings" : "uninstall"} + + self._app = app + self._dialog = None # type: Optional[QObject] + self._compatibility_dialog_path = "resources/qml/dialogs/CompatibilityDialog.qml" + + def present(self, plugin_path: str, model: SubscribedPackagesModel): + path = os.path.join(plugin_path, self._compatibility_dialog_path) + self._dialog = self._app.createQmlComponent(path, {"subscribedPackagesModel": model}) + self._dialog.accepted.connect(lambda: self._onConfirmClicked(model)) + + def _onConfirmClicked(self, model: SubscribedPackagesModel): + # For now, all packages presented to the user should be installed. + # Later, we will support uninstall ?or ignoring? of a certain package + choices = {item["package_id"]: "install" for item in model.items} + self.packageMutations.emit(choices) diff --git a/plugins/Toolbox/src/SubscribedPackagesModel.py b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py similarity index 92% rename from plugins/Toolbox/src/SubscribedPackagesModel.py rename to plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py index cf0d07c153..7ab6d30fe0 100644 --- a/plugins/Toolbox/src/SubscribedPackagesModel.py +++ b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py @@ -33,7 +33,7 @@ class SubscribedPackagesModel(ListModel): for item in self._metadata: if item["package_id"] not in self._discrepancies: continue - package = {"name": item["display_name"], "sdk_versions": item["sdk_versions"]} + package = {"package_id": item["package_id"], "name": item["display_name"], "sdk_versions": item["sdk_versions"]} if self._sdk_version not in item["sdk_versions"]: package.update({"is_compatible": False}) else: diff --git a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py new file mode 100644 index 0000000000..aca20f98f7 --- /dev/null +++ b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py @@ -0,0 +1,34 @@ +from UM.Extension import Extension +from UM.PluginRegistry import PluginRegistry +from cura.CuraApplication import CuraApplication +from plugins.Toolbox import CloudPackageChecker +from plugins.Toolbox.src.CloudSync.DiscrepanciesPresenter import DiscrepanciesPresenter +from plugins.Toolbox.src.CloudSync.SubscribedPackagesModel import SubscribedPackagesModel + + +## 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 +# - The LicencePresenter extracts licences from the downloaded packages and presents a licence for each package to +# - be installed. It emits the `licenceAnswers` {'packageId' : bool} for accept or declines +# - The CloudPackageManager removes the declined packages from the account +# - The SyncOrchestrator uses PackageManager to install the downloaded packages. +# - Bliss / profit / done +class SyncOrchestrator(Extension): + + def __init__(self, app: CuraApplication): + super().__init__() + + self._checker = CloudPackageChecker(app) + self._checker.discrepancies.connect(self._onDiscrepancies) + + self._discrepanciesPresenter = DiscrepanciesPresenter(app) + + def _onDiscrepancies(self, model: SubscribedPackagesModel): + plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) + self._discrepanciesPresenter.present(plugin_path, model) diff --git a/plugins/Toolbox/src/CloudSync/__init__.py b/plugins/Toolbox/src/CloudSync/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 5cc6353c63..7ef4a27ceb 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -23,7 +23,7 @@ from plugins.Toolbox.src.CloudApiModel import CloudApiModel from .AuthorsModel import AuthorsModel from .PackagesModel import PackagesModel -from .SubscribedPackagesModel import SubscribedPackagesModel +from .CloudSync.SubscribedPackagesModel import SubscribedPackagesModel if TYPE_CHECKING: from UM.TaskManagement.HttpRequestData import HttpRequestData