diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 221ccf9fb0..0d0418b671 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -191,8 +191,6 @@ class CuraApplication(QtApplication): self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions] - self._cura_package_manager = None - self._machine_action_manager = None # type: Optional[MachineActionManager.MachineActionManager] self.empty_container = None # type: EmptyInstanceContainer @@ -476,7 +474,7 @@ class CuraApplication(QtApplication): "CuraEngineBackend", #Cura is useless without this one since you can't slice. "FileLogger", #You want to be able to read the log if something goes wrong. "XmlMaterialProfile", #Cura crashes without this one. - "Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back. + "Toolbox", #This contains the interface to enable/disable plug-ins and the Cloud functionality. "PrepareStage", #Cura is useless without this one since you can't load models. "PreviewStage", #This shows the list of the plugin views that are installed in Cura. "MonitorStage", #Major part of Cura's functionality. @@ -632,6 +630,12 @@ class CuraApplication(QtApplication): def showPreferences(self) -> None: self.showPreferencesWindow.emit() + # This is called by drag-and-dropping curapackage files. + @pyqtSlot(QUrl) + def installPackageViaDragAndDrop(self, file_url: str) -> Optional[str]: + filename = QUrl(file_url).toLocalFile() + return self._package_manager.installPackage(filename) + @override(Application) def getGlobalContainerStack(self) -> Optional["GlobalStack"]: return self._global_container_stack diff --git a/plugins/Toolbox/src/CloudSync/CloudPackageManager.py b/plugins/Toolbox/src/CloudSync/CloudPackageManager.py index 0cbc9eaa7a..4e5e8520cb 100644 --- a/plugins/Toolbox/src/CloudSync/CloudPackageManager.py +++ b/plugins/Toolbox/src/CloudSync/CloudPackageManager.py @@ -1,23 +1,53 @@ +from UM.Logger import Logger +from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from cura.API import Account from cura.CuraApplication import CuraApplication from ..CloudApiModel import CloudApiModel from ..UltimakerCloudScope import UltimakerCloudScope -## 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 class CloudPackageManager: + """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 CloudPackageManager.getInstance() instead of CloudPackageManager() + """ + + __instance = None + + @classmethod + def getInstance(cls, app: CuraApplication): + if not cls.__instance: + cls.__instance = CloudPackageManager(app) + return cls.__instance + def __init__(self, app: CuraApplication) -> None: - self._request_manager = app.getHttpRequestManager() - self._scope = UltimakerCloudScope(app) + if self.__instance is not None: + raise Exception("This is a Singleton. use getInstance()") + + self._request_manager = app.getHttpRequestManager() # type: HttpRequestManager + self.account = app.getCuraAPI().account # type: Account + self._scope = UltimakerCloudScope(app) # type: UltimakerCloudScope + + app.getPackageManager().packageInstalled.connect(self._onPackageInstalled) + + def unsubscribe(self, package_id: str) -> None: + url = CloudApiModel.userPackageUrl(package_id) + self._request_manager.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) self._request_manager.put(url=CloudApiModel.api_url_user_packages, data=data.encode(), scope=self._scope ) - def unsubscribe(self, package_id: str) -> None: - url = CloudApiModel.userPackageUrl(package_id) - self._request_manager.delete(url=url, scope=self._scope) - + def _onPackageInstalled(self, package_id: str): + if self.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/Toolbox/src/CloudSync/SyncOrchestrator.py b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py index 4b8b3cf7f9..0e3d596db9 100644 --- a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py +++ b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py @@ -38,7 +38,8 @@ class SyncOrchestrator(Extension): self._name = "SyncOrchestrator" self._package_manager = app.getPackageManager() - self._cloud_package_manager = CloudPackageManager(app) + # Keep a reference to the CloudPackageManager. it watches for installed packages and subscribes to them + self._cloud_package_manager = CloudPackageManager.getInstance(app) # type: CloudPackageManager self._checker = CloudPackageChecker(app) # type: CloudPackageChecker self._checker.discrepancies.connect(self._onDiscrepancies) @@ -86,7 +87,6 @@ class SyncOrchestrator(Extension): message = "Could not install {}".format(item["package_id"]) self._showErrorMessage(message) continue - self._cloud_package_manager.subscribe(item["package_id"]) has_changes = True else: self._cloud_package_manager.unsubscribe(item["package_id"]) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 56bfa29ea2..782d6668ba 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -21,7 +21,6 @@ from cura.Machines.ContainerTree import ContainerTree from .CloudApiModel import CloudApiModel from .AuthorsModel import AuthorsModel -from .CloudSync.CloudPackageManager import CloudPackageManager from .CloudSync.LicenseModel import LicenseModel from .PackagesModel import PackagesModel from .UltimakerCloudScope import UltimakerCloudScope @@ -44,7 +43,6 @@ class Toolbox(QObject, Extension): self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int] # Network: - self._cloud_package_manager = CloudPackageManager(application) # type: CloudPackageManager self._download_request_data = None # type: Optional[HttpRequestData] self._download_progress = 0 # type: float self._is_downloading = False # type: bool @@ -147,11 +145,6 @@ class Toolbox(QObject, Extension): self._application.getHttpRequestManager().put(url, data = data.encode(), scope = self._scope) - @pyqtSlot(str) - def subscribe(self, package_id: str) -> None: - if self._application.getCuraAPI().account.isLoggedIn: - self._cloud_package_manager.subscribe(package_id) - def getLicenseDialogPluginFileLocation(self) -> str: return self._license_dialog_plugin_file_location @@ -378,7 +371,6 @@ class Toolbox(QObject, Extension): def onLicenseAccepted(self): self.closeLicenseDialog.emit() package_id = self.install(self.getLicenseDialogPluginFileLocation()) - self.subscribe(package_id) @pyqtSlot() @@ -682,7 +674,6 @@ class Toolbox(QObject, Extension): installed_id = self.install(file_path) if installed_id != package_id: Logger.error("Installed package {} does not match {}".format(installed_id, package_id)) - self.subscribe(installed_id) # Getter & Setters for Properties: # -------------------------------------------------------------------------- diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index d6f50f939b..ba305a1104 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -238,7 +238,7 @@ UM.MainWindow if (filename.toLowerCase().endsWith(".curapackage")) { // Try to install plugin & close. - CuraApplication.getPackageManager().installPackageViaDragAndDrop(filename); + CuraApplication.installPackageViaDragAndDrop(filename); packageInstallDialog.text = catalog.i18nc("@label", "This package will be installed after restarting."); packageInstallDialog.icon = StandardIcon.Information; packageInstallDialog.open();