diff options
Diffstat (limited to 'plugins/Toolbox/src/Toolbox.py')
-rw-r--r-- | plugins/Toolbox/src/Toolbox.py | 878 |
1 files changed, 0 insertions, 878 deletions
diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py deleted file mode 100644 index 44b62b64af..0000000000 --- a/plugins/Toolbox/src/Toolbox.py +++ /dev/null @@ -1,878 +0,0 @@ -# Copyright (c) 2021 Ultimaker B.V. -# Toolbox is released under the terms of the LGPLv3 or higher. - -import json -import os -import tempfile -from typing import cast, Any, Dict, List, Set, TYPE_CHECKING, Tuple, Optional, Union - -from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot -from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply - -from UM.Extension import Extension -from UM.Logger import Logger -from UM.PluginRegistry import PluginRegistry -from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope -from UM.Version import Version -from UM.i18n import i18nCatalog -from cura import ApplicationMetadata -from cura.CuraApplication import CuraApplication -from cura.Machines.ContainerTree import ContainerTree -from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope -from .AuthorsModel import AuthorsModel -from .CloudApiModel import CloudApiModel -from .CloudSync.LicenseModel import LicenseModel -from .PackagesModel import PackagesModel - -if TYPE_CHECKING: - from UM.TaskManagement.HttpRequestData import HttpRequestData - from cura.Settings.GlobalStack import GlobalStack - -i18n_catalog = i18nCatalog("cura") - -DEFAULT_MARKETPLACE_ROOT = "https://marketplace.ultimaker.com" # type: str - -try: - from cura.CuraVersion import CuraMarketplaceRoot -except ImportError: - CuraMarketplaceRoot = DEFAULT_MARKETPLACE_ROOT - - -class Toolbox(QObject, Extension): - """Provides a marketplace for users to download plugins an materials""" - - def __init__(self, application: CuraApplication) -> None: - super().__init__() - - self._application = application # type: CuraApplication - - # Network: - self._download_request_data = None # type: Optional[HttpRequestData] - self._download_progress = 0 # type: float - self._is_downloading = False # type: bool - self._cloud_scope = UltimakerCloudScope(application) # type: UltimakerCloudScope - self._json_scope = JsonDecoratorScope(self._cloud_scope) # type: JsonDecoratorScope - - self._request_urls = {} # type: Dict[str, str] - self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated - self._old_plugin_ids = set() # type: Set[str] - self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]] - - # The responses as given by the server parsed to a list. - self._server_response_data = { - "authors": [], - "packages": [], - "updates": [] - } # type: Dict[str, List[Any]] - - # Models: - self._models = { - "authors": AuthorsModel(self), - "packages": PackagesModel(self), - "updates": PackagesModel(self) - } # type: Dict[str, Union[AuthorsModel, PackagesModel]] - - self._plugins_showcase_model = PackagesModel(self) - self._plugins_available_model = PackagesModel(self) - self._plugins_installed_model = PackagesModel(self) - self._plugins_installed_model.setFilter({"is_bundled": "False"}) - self._plugins_bundled_model = PackagesModel(self) - self._plugins_bundled_model.setFilter({"is_bundled": "True"}) - self._materials_showcase_model = AuthorsModel(self) - self._materials_available_model = AuthorsModel(self) - self._materials_installed_model = PackagesModel(self) - self._materials_installed_model.setFilter({"is_bundled": "False"}) - self._materials_bundled_model = PackagesModel(self) - self._materials_bundled_model.setFilter({"is_bundled": "True"}) - self._materials_generic_model = PackagesModel(self) - - self._license_model = LicenseModel() - - # These properties are for keeping track of the UI state: - # ---------------------------------------------------------------------- - # View category defines which filter to use, and therefore effectively - # which category is currently being displayed. For example, possible - # values include "plugin" or "material", but also "installed". - self._view_category = "plugin" # type: str - - # View page defines which type of page layout to use. For example, - # possible values include "overview", "detail" or "author". - self._view_page = "welcome" # type: str - - # Active package refers to which package is currently being downloaded, - # installed, or otherwise modified. - self._active_package = None # type: Optional[Dict[str, Any]] - - self._dialog = None # type: Optional[QObject] - self._confirm_reset_dialog = None # type: Optional[QObject] - self._resetUninstallVariables() - - self._restart_required = False # type: bool - - # variables for the license agreement dialog - self._license_dialog_plugin_file_location = "" # type: str - - self._application.initializationFinished.connect(self._onAppInitialized) - - # Signals: - # -------------------------------------------------------------------------- - # Downloading changes - activePackageChanged = pyqtSignal() - onDownloadProgressChanged = pyqtSignal() - onIsDownloadingChanged = pyqtSignal() - restartRequiredChanged = pyqtSignal() - installChanged = pyqtSignal() - toolboxEnabledChanged = pyqtSignal() - - # UI changes - viewChanged = pyqtSignal() - detailViewChanged = pyqtSignal() - filterChanged = pyqtSignal() - metadataChanged = pyqtSignal() - showLicenseDialog = pyqtSignal() - closeLicenseDialog = pyqtSignal() - uninstallVariablesChanged = pyqtSignal() - - def _restart(self): - """Go back to the start state (welcome screen or loading if no login required)""" - - # For an Essentials build, login is mandatory - if not self._application.getCuraAPI().account.isLoggedIn and ApplicationMetadata.IsEnterpriseVersion: - self.setViewPage("welcome") - else: - self.setViewPage("loading") - self._fetchPackageData() - - def _resetUninstallVariables(self) -> None: - self._package_id_to_uninstall = None # type: Optional[str] - self._package_name_to_uninstall = "" - self._package_used_materials = [] # type: List[Tuple[GlobalStack, str, str]] - self._package_used_qualities = [] # type: List[Tuple[GlobalStack, str, str]] - - def getLicenseDialogPluginFileLocation(self) -> str: - return self._license_dialog_plugin_file_location - - def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str, icon_url: str) -> None: - # Set page 1/1 when opening the dialog for a single package - self._license_model.setCurrentPageIdx(0) - self._license_model.setPageCount(1) - self._license_model.setIconUrl(icon_url) - - self._license_model.setPackageName(plugin_name) - self._license_model.setLicenseText(license_content) - self._license_dialog_plugin_file_location = plugin_file_location - self.showLicenseDialog.emit() - - # 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._plugin_registry = self._application.getPluginRegistry() - self._package_manager = self._application.getPackageManager() - - # We need to construct a query like installed_packages=ID:VERSION&installed_packages=ID:VERSION, etc. - installed_package_ids_with_versions = [":".join(items) for items in - self._package_manager.getAllInstalledPackageIdsAndVersions()] - installed_packages_query = "&installed_packages=".join(installed_package_ids_with_versions) - - self._request_urls = { - "authors": "{base_url}/authors".format(base_url = CloudApiModel.api_url), - "packages": "{base_url}/packages".format(base_url = CloudApiModel.api_url), - "updates": "{base_url}/packages/package-updates?installed_packages={query}".format( - base_url = CloudApiModel.api_url, query = installed_packages_query) - } - - self._application.getCuraAPI().account.loginStateChanged.connect(self._restart) - - preferences = CuraApplication.getInstance().getPreferences() - - preferences.addPreference("info/automatic_plugin_update_check", True) - - # On boot we check which packages have updates. - if preferences.getValue("info/automatic_plugin_update_check") and len(installed_package_ids_with_versions) > 0: - # Request the latest and greatest! - self._makeRequestByType("updates") - - def _fetchPackageData(self) -> None: - self._makeRequestByType("packages") - self._makeRequestByType("authors") - self._updateInstalledModels() - - # Displays the toolbox - @pyqtSlot() - def launch(self) -> None: - if not self._dialog: - self._dialog = self._createDialog("Toolbox.qml") - - if not self._dialog: - Logger.log("e", "Unexpected error trying to create the 'Marketplace' dialog.") - return - - self._restart() - - self._dialog.show() - # Apply enabled/disabled state to installed plugins - self.toolboxEnabledChanged.emit() - - def _createDialog(self, qml_name: str) -> Optional[QObject]: - Logger.log("d", "Marketplace: Creating dialog [%s].", qml_name) - plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) - if not plugin_path: - return None - path = os.path.join(plugin_path, "resources", "qml", qml_name) - - dialog = self._application.createQmlComponent(path, { - "toolbox": self, - "handler": self, - "licenseModel": self._license_model - }) - if not dialog: - return None - return dialog - - def _convertPluginMetadata(self, plugin_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: - try: - highest_sdk_version_supported = Version(0) - for supported_version in plugin_data["plugin"]["supported_sdk_versions"]: - if supported_version > highest_sdk_version_supported: - highest_sdk_version_supported = supported_version - - formatted = { - "package_id": plugin_data["id"], - "package_type": "plugin", - "display_name": plugin_data["plugin"]["name"], - "package_version": plugin_data["plugin"]["version"], - "sdk_version": highest_sdk_version_supported, - "author": { - "author_id": plugin_data["plugin"]["author"], - "display_name": plugin_data["plugin"]["author"] - }, - "is_installed": True, - "description": plugin_data["plugin"]["description"] - } - return formatted - except KeyError: - Logger.log("w", "Unable to convert plugin meta data %s", str(plugin_data)) - return None - - @pyqtSlot() - def _updateInstalledModels(self) -> None: - # This is moved here to avoid code duplication and so that after installing plugins they get removed from the - # list of old plugins - old_plugin_ids = self._plugin_registry.getInstalledPlugins() - installed_package_ids = self._package_manager.getAllInstalledPackageIDs() - scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs() - - self._old_plugin_ids = set() - self._old_plugin_metadata = dict() - - for plugin_id in old_plugin_ids: - # Neither the installed packages nor the packages that are scheduled to remove are old plugins - if plugin_id not in installed_package_ids and plugin_id not in scheduled_to_remove_package_ids: - Logger.log("d", "Found a plugin that was installed with the old plugin browser: %s", plugin_id) - - old_metadata = self._plugin_registry.getMetaData(plugin_id) - new_metadata = self._convertPluginMetadata(old_metadata) - if new_metadata is None: - # Something went wrong converting it. - continue - self._old_plugin_ids.add(plugin_id) - self._old_plugin_metadata[new_metadata["package_id"]] = new_metadata - - all_packages = self._package_manager.getAllInstalledPackagesInfo() - if "plugin" in all_packages: - # For old plugins, we only want to include the old custom plugin that were installed via the old toolbox. - # The bundled plugins will be included in JSON files in the "bundled_packages" folder, so the bundled - # plugins should be excluded from the old plugins list/dict. - all_plugin_package_ids = set(package["package_id"] for package in all_packages["plugin"]) - self._old_plugin_ids = set(plugin_id for plugin_id in self._old_plugin_ids - if plugin_id not in all_plugin_package_ids) - self._old_plugin_metadata = {k: v for k, v in self._old_plugin_metadata.items() if k in self._old_plugin_ids} - - self._plugins_installed_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values())) - self._plugins_bundled_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values())) - self.metadataChanged.emit() - if "material" in all_packages: - self._materials_installed_model.setMetadata(all_packages["material"]) - self._materials_bundled_model.setMetadata(all_packages["material"]) - self.metadataChanged.emit() - - @pyqtSlot(str) - def install(self, file_path: str) -> Optional[str]: - package_id = self._package_manager.installPackage(file_path) - self.installChanged.emit() - self._updateInstalledModels() - self.metadataChanged.emit() - self._restart_required = True - self.restartRequiredChanged.emit() - return package_id - - @pyqtSlot(str) - def checkPackageUsageAndUninstall(self, package_id: str) -> None: - """Check package usage and uninstall - - If the package is in use, you'll get a confirmation dialog to set everything to default - """ - - package_used_materials, package_used_qualities = self._package_manager.getMachinesUsingPackage(package_id) - if package_used_materials or package_used_qualities: - # Set up "uninstall variables" for resetMaterialsQualitiesAndUninstall - self._package_id_to_uninstall = package_id - package_info = self._package_manager.getInstalledPackageInfo(package_id) - self._package_name_to_uninstall = package_info.get("display_name", package_info.get("package_id")) - self._package_used_materials = package_used_materials - self._package_used_qualities = package_used_qualities - # Ask change to default material / profile - if self._confirm_reset_dialog is None: - self._confirm_reset_dialog = self._createDialog("dialogs/ToolboxConfirmUninstallResetDialog.qml") - self.uninstallVariablesChanged.emit() - if self._confirm_reset_dialog is None: - Logger.log("e", "ToolboxConfirmUninstallResetDialog should have been initialized, but it is not. Not showing dialog and not uninstalling package.") - else: - self._confirm_reset_dialog.show() - else: - # Plain uninstall - self.uninstall(package_id) - - @pyqtProperty(str, notify = uninstallVariablesChanged) - def pluginToUninstall(self) -> str: - return self._package_name_to_uninstall - - @pyqtProperty(str, notify = uninstallVariablesChanged) - def uninstallUsedMaterials(self) -> str: - return "\n".join(["%s (%s)" % (str(global_stack.getName()), material) for global_stack, extruder_nr, material in self._package_used_materials]) - - @pyqtProperty(str, notify = uninstallVariablesChanged) - def uninstallUsedQualities(self) -> str: - return "\n".join(["%s (%s)" % (str(global_stack.getName()), quality) for global_stack, extruder_nr, quality in self._package_used_qualities]) - - @pyqtSlot() - def closeConfirmResetDialog(self) -> None: - if self._confirm_reset_dialog is not None: - self._confirm_reset_dialog.close() - - @pyqtSlot() - def resetMaterialsQualitiesAndUninstall(self) -> None: - """Uses "uninstall variables" to reset qualities and materials, then uninstall - - It's used as an action on Confirm reset on Uninstall - """ - - application = CuraApplication.getInstance() - machine_manager = application.getMachineManager() - container_tree = ContainerTree.getInstance() - - for global_stack, extruder_nr, container_id in self._package_used_materials: - extruder = global_stack.extruderList[int(extruder_nr)] - approximate_diameter = extruder.getApproximateMaterialDiameter() - variant_node = container_tree.machines[global_stack.definition.getId()].variants[extruder.variant.getName()] - default_material_node = variant_node.preferredMaterial(approximate_diameter) - machine_manager.setMaterial(extruder_nr, default_material_node, global_stack = global_stack) - for global_stack, extruder_nr, container_id in self._package_used_qualities: - variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList] - material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList] - extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList] - definition_id = global_stack.definition.getId() - machine_node = container_tree.machines[definition_id] - default_quality_group = machine_node.getQualityGroups(variant_names, material_bases, extruder_enabled)[machine_node.preferred_quality_type] - machine_manager.setQualityGroup(default_quality_group, global_stack = global_stack) - - if self._package_id_to_uninstall is not None: - self._markPackageMaterialsAsToBeUninstalled(self._package_id_to_uninstall) - self.uninstall(self._package_id_to_uninstall) - self._resetUninstallVariables() - self.closeConfirmResetDialog() - - @pyqtSlot() - def onLicenseAccepted(self): - self.closeLicenseDialog.emit() - package_id = self.install(self.getLicenseDialogPluginFileLocation()) - - - @pyqtSlot() - def onLicenseDeclined(self): - self.closeLicenseDialog.emit() - - def _markPackageMaterialsAsToBeUninstalled(self, package_id: str) -> None: - container_registry = self._application.getContainerRegistry() - - all_containers = self._package_manager.getPackageContainerIds(package_id) - for container_id in all_containers: - containers = container_registry.findInstanceContainers(id = container_id) - if not containers: - continue - container = containers[0] - if container.getMetaDataEntry("type") != "material": - continue - root_material_id = container.getMetaDataEntry("base_file") - root_material_containers = container_registry.findInstanceContainers(id = root_material_id) - if not root_material_containers: - continue - root_material_container = root_material_containers[0] - root_material_container.setMetaDataEntry("removed", True) - - @pyqtSlot(str) - def uninstall(self, package_id: str) -> None: - self._package_manager.removePackage(package_id, force_add = True) - self.installChanged.emit() - self._updateInstalledModels() - self.metadataChanged.emit() - self._restart_required = True - self.restartRequiredChanged.emit() - - def _update(self) -> None: - """Actual update packages that are in self._to_update""" - - if self._to_update: - plugin_id = self._to_update.pop(0) - remote_package = self.getRemotePackage(plugin_id) - if remote_package: - download_url = remote_package["download_url"] - Logger.log("d", "Updating package [%s]..." % plugin_id) - self.startDownload(download_url) - else: - Logger.log("e", "Could not update package [%s] because there is no remote package info available.", plugin_id) - - if self._to_update: - self._application.callLater(self._update) - - @pyqtSlot(str) - def update(self, plugin_id: str) -> None: - """Update a plugin by plugin_id""" - - self._to_update.append(plugin_id) - self._application.callLater(self._update) - - @pyqtSlot(str) - def enable(self, plugin_id: str) -> None: - self._plugin_registry.enablePlugin(plugin_id) - self.toolboxEnabledChanged.emit() - Logger.log("i", "%s was set as 'active'.", plugin_id) - self._restart_required = True - self.restartRequiredChanged.emit() - - @pyqtSlot(str) - def disable(self, plugin_id: str) -> None: - self._plugin_registry.disablePlugin(plugin_id) - self.toolboxEnabledChanged.emit() - Logger.log("i", "%s was set as 'deactive'.", plugin_id) - self._restart_required = True - self.restartRequiredChanged.emit() - - @pyqtProperty(bool, notify = metadataChanged) - def dataReady(self) -> bool: - return self._packages_model is not None - - @pyqtProperty(bool, notify = restartRequiredChanged) - def restartRequired(self) -> bool: - return self._restart_required - - @pyqtSlot() - def restart(self) -> None: - self._application.windowClosed() - - def getRemotePackage(self, package_id: str) -> Optional[Dict]: - # TODO: make the lookup in a dict, not a loop. canUpdate is called for every item. - remote_package = None - for package in self._server_response_data["packages"]: - if package["package_id"] == package_id: - remote_package = package - break - return remote_package - - @pyqtSlot(str, result = bool) - def canDowngrade(self, package_id: str) -> bool: - # If the currently installed version is higher than the bundled version (if present), the we can downgrade - # this package. - local_package = self._package_manager.getInstalledPackageInfo(package_id) - if local_package is None: - return False - - bundled_package = self._package_manager.getBundledPackageInfo(package_id) - if bundled_package is None: - return False - - local_version = Version(local_package["package_version"]) - bundled_version = Version(bundled_package["package_version"]) - return bundled_version < local_version - - @pyqtSlot(str, result = bool) - def isInstalled(self, package_id: str) -> bool: - result = self._package_manager.isPackageInstalled(package_id) - # Also check the old plugins list if it's not found in the package manager. - if not result: - result = self.isOldPlugin(package_id) - return result - - @pyqtSlot(str, result = int) - def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int: - count = 0 - for package in self._materials_installed_model.items: - if package["author_id"] == author_id: - count += 1 - return count - - # This slot is only used to get the number of material packages by author, not any other type of packages. - @pyqtSlot(str, result = int) - def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int: - count = 0 - for package in self._server_response_data["packages"]: - if package["package_type"] == "material": - if package["author"]["author_id"] == author_id: - count += 1 - return count - - @pyqtSlot(str, result = bool) - def isEnabled(self, package_id: str) -> bool: - return package_id in self._plugin_registry.getActivePlugins() - - # Check for plugins that were installed with the old plugin browser - def isOldPlugin(self, plugin_id: str) -> bool: - return plugin_id in self._old_plugin_ids - - def getOldPluginPackageMetadata(self, plugin_id: str) -> Optional[Dict[str, Any]]: - return self._old_plugin_metadata.get(plugin_id) - - def isLoadingComplete(self) -> bool: - populated = 0 - for metadata_list in self._server_response_data.items(): - if metadata_list: - populated += 1 - return populated == len(self._server_response_data.items()) - - # Make API Calls - # -------------------------------------------------------------------------- - def _makeRequestByType(self, request_type: str) -> None: - Logger.debug(f"Requesting {request_type} metadata from server.") - url = self._request_urls[request_type] - - callback = lambda r, rt = request_type: self._onDataRequestFinished(rt, r) - error_callback = lambda r, e, rt = request_type: self._onDataRequestError(rt, r, e) - self._application.getHttpRequestManager().get(url, - callback = callback, - error_callback = error_callback, - scope=self._json_scope) - - @pyqtSlot(str) - def startDownload(self, url: str) -> None: - Logger.info(f"Attempting to download & install package from {url}.") - - callback = lambda r: self._onDownloadFinished(r) - error_callback = lambda r, e: self._onDownloadFailed(r, e) - download_progress_callback = self._onDownloadProgress - request_data = self._application.getHttpRequestManager().get(url, - callback = callback, - error_callback = error_callback, - download_progress_callback = download_progress_callback, - scope=self._cloud_scope - ) - - self._download_request_data = request_data - self.setDownloadProgress(0) - self.setIsDownloading(True) - - @pyqtSlot() - def cancelDownload(self) -> None: - Logger.info(f"User cancelled the download of a package. request {self._download_request_data}") - if self._download_request_data is not None: - self._application.getHttpRequestManager().abortRequest(self._download_request_data) - self._download_request_data = None - self.resetDownload() - - def resetDownload(self) -> None: - self.setDownloadProgress(0) - self.setIsDownloading(False) - - # Handlers for Network Events - # -------------------------------------------------------------------------- - def _onDataRequestError(self, request_type: str, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None: - Logger.error(f"Request {request_type} failed due to error {error}: {reply.errorString()}") - self.setViewPage("errored") - - def _onDataRequestFinished(self, request_type: str, reply: "QNetworkReply") -> None: - if reply.operation() != QNetworkAccessManager.Operation.GetOperation: - Logger.log("e", "_onDataRequestFinished() only handles GET requests but got [%s] instead", reply.operation()) - return - - http_status_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) - if http_status_code != 200: - Logger.log("e", "Request type [%s] got non-200 HTTP response: [%s]", http_status_code) - self.setViewPage("errored") - return - - data = bytes(reply.readAll()) - try: - json_data = json.loads(data.decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("e", "Failed to decode response data as JSON for request type [%s], response data [%s]", - request_type, data) - self.setViewPage("errored") - return - - # Check for errors: - if "errors" in json_data: - for error in json_data["errors"]: - Logger.log("e", "Request type [%s] got response showing error: %s", error.get("title", "No error title found")) - self.setViewPage("errored") - return - - # Create model and apply metadata: - if not self._models[request_type]: - Logger.log("e", "Could not find the model for request type [%s].", request_type) - self.setViewPage("errored") - return - - self._server_response_data[request_type] = json_data["data"] - self._models[request_type].setMetadata(self._server_response_data[request_type]) - - if request_type == "packages": - self._models[request_type].setFilter({"type": "plugin"}) - self.reBuildMaterialsModels() - self.reBuildPluginsModels() - self._notifyPackageManager() - elif request_type == "authors": - self._models[request_type].setFilter({"package_types": "material"}) - self._models[request_type].setFilter({"tags": "generic"}) - elif request_type == "updates": - # Tell the package manager that there's a new set of updates available. - packages = set([pkg["package_id"] for pkg in self._server_response_data[request_type]]) - self._package_manager.setPackagesWithUpdate(packages) - - self.metadataChanged.emit() - - if self.isLoadingComplete(): - self.setViewPage("overview") - - # This function goes through all known remote versions of a package and notifies the package manager of this change - def _notifyPackageManager(self): - for package in self._server_response_data["packages"]: - self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"])) - - def _onDownloadFinished(self, reply: "QNetworkReply") -> None: - self.resetDownload() - - if reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) != 200: - try: - reply_error = json.loads(reply.readAll().data().decode("utf-8")) - except Exception as e: - reply_error = str(e) - Logger.log("w", "Failed to download package. The following error was returned: %s", reply_error) - return - # Must not delete the temporary file on Windows - self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False) - file_path = self._temp_plugin_file.name - # Write first and close, otherwise on Windows, it cannot read the file - self._temp_plugin_file.write(reply.readAll()) - self._temp_plugin_file.close() - self._onDownloadComplete(file_path) - - def _onDownloadFailed(self, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None: - Logger.log("w", "Failed to download package. The following error was returned: %s", error) - - self.resetDownload() - - def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None: - if bytes_total > 0: - new_progress = bytes_sent / bytes_total * 100 - self.setDownloadProgress(new_progress) - Logger.log("d", "new download progress %s / %s : %s%%", bytes_sent, bytes_total, new_progress) - - def _onDownloadComplete(self, file_path: str) -> None: - Logger.log("i", "Download complete.") - package_info = self._package_manager.getPackageInfo(file_path) - if not package_info: - Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path) - return - package_id = package_info["package_id"] - - try: - license_content = self._package_manager.getPackageLicense(file_path) - except EnvironmentError as e: - Logger.error(f"Could not open downloaded package {package_id} to read license file! {type(e)} - {e}") - return - if license_content is not None: - # get the icon url for package_id, make sure the result is a string, never None - icon_url = next((x["icon_url"] for x in self.packagesModel.items if x["id"] == package_id), None) or "" - self.openLicenseDialog(package_info["display_name"], license_content, file_path, icon_url) - return - - installed_id = self.install(file_path) - if installed_id != package_id: - Logger.error("Installed package {} does not match {}".format(installed_id, package_id)) - - # Getter & Setters for Properties: - # -------------------------------------------------------------------------- - def setDownloadProgress(self, progress: float) -> None: - if progress != self._download_progress: - self._download_progress = progress - self.onDownloadProgressChanged.emit() - - @pyqtProperty(int, fset = setDownloadProgress, notify = onDownloadProgressChanged) - def downloadProgress(self) -> float: - return self._download_progress - - def setIsDownloading(self, is_downloading: bool) -> None: - if self._is_downloading != is_downloading: - self._is_downloading = is_downloading - self.onIsDownloadingChanged.emit() - - @pyqtProperty(bool, fset = setIsDownloading, notify = onIsDownloadingChanged) - def isDownloading(self) -> bool: - return self._is_downloading - - def setActivePackage(self, package: QObject) -> None: - if self._active_package != package: - self._active_package = package - self.activePackageChanged.emit() - - @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged) - def activePackage(self) -> Optional[QObject]: - """The active package is the package that is currently being downloaded""" - - return self._active_package - - def setViewCategory(self, category: str = "plugin") -> None: - if self._view_category != category: - self._view_category = category - self.viewChanged.emit() - - # Function explicitly defined so that it can be called through the callExtensionsMethod - # which cannot receive arguments. - def setViewCategoryToMaterials(self) -> None: - self.setViewCategory("material") - - @pyqtProperty(str, fset = setViewCategory, notify = viewChanged) - def viewCategory(self) -> str: - return self._view_category - - def setViewPage(self, page: str = "overview") -> None: - if self._view_page != page: - self._view_page = page - self.viewChanged.emit() - - @pyqtProperty(str, fset = setViewPage, notify = viewChanged) - def viewPage(self) -> str: - return self._view_page - - # Exposed Models: - # -------------------------------------------------------------------------- - @pyqtProperty(QObject, constant = True) - def authorsModel(self) -> AuthorsModel: - return cast(AuthorsModel, self._models["authors"]) - - @pyqtProperty(QObject, constant = True) - def packagesModel(self) -> PackagesModel: - return cast(PackagesModel, self._models["packages"]) - - @pyqtProperty(QObject, constant = True) - def pluginsShowcaseModel(self) -> PackagesModel: - return self._plugins_showcase_model - - @pyqtProperty(QObject, constant = True) - def pluginsAvailableModel(self) -> PackagesModel: - return self._plugins_available_model - - @pyqtProperty(QObject, constant = True) - def pluginsInstalledModel(self) -> PackagesModel: - return self._plugins_installed_model - - @pyqtProperty(QObject, constant = True) - def pluginsBundledModel(self) -> PackagesModel: - return self._plugins_bundled_model - - @pyqtProperty(QObject, constant = True) - def materialsShowcaseModel(self) -> AuthorsModel: - return self._materials_showcase_model - - @pyqtProperty(QObject, constant = True) - def materialsAvailableModel(self) -> AuthorsModel: - return self._materials_available_model - - @pyqtProperty(QObject, constant = True) - def materialsInstalledModel(self) -> PackagesModel: - return self._materials_installed_model - - @pyqtProperty(QObject, constant = True) - def materialsBundledModel(self) -> PackagesModel: - return self._materials_bundled_model - - @pyqtProperty(QObject, constant = True) - def materialsGenericModel(self) -> PackagesModel: - return self._materials_generic_model - - @pyqtSlot(str, result = str) - def getWebMarketplaceUrl(self, page: str) -> str: - root = CuraMarketplaceRoot - if root == "": - root = DEFAULT_MARKETPLACE_ROOT - return root + "/app/cura/" + page - - # Filter Models: - # -------------------------------------------------------------------------- - @pyqtSlot(str, str, str) - def filterModelByProp(self, model_type: str, filter_type: str, parameter: str) -> None: - if not self._models[model_type]: - Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type) - return - self._models[model_type].setFilter({filter_type: parameter}) - self.filterChanged.emit() - - @pyqtSlot(str, "QVariantMap") - def setFilters(self, model_type: str, filter_dict: dict) -> None: - if not self._models[model_type]: - Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type) - return - self._models[model_type].setFilter(filter_dict) - self.filterChanged.emit() - - @pyqtSlot(str) - def removeFilters(self, model_type: str) -> None: - if not self._models[model_type]: - Logger.log("w", "Couldn't remove filters on %s model because it doesn't exist.", model_type) - return - self._models[model_type].setFilter({}) - self.filterChanged.emit() - - # HACK(S): - # -------------------------------------------------------------------------- - def reBuildMaterialsModels(self) -> None: - materials_showcase_metadata = [] - materials_available_metadata = [] - materials_generic_metadata = [] - - processed_authors = [] # type: List[str] - - for item in self._server_response_data["packages"]: - if item["package_type"] == "material": - - author = item["author"] - if author["author_id"] in processed_authors: - continue - - # Generic materials to be in the same section - if "generic" in item["tags"]: - materials_generic_metadata.append(item) - else: - if "showcase" in item["tags"]: - materials_showcase_metadata.append(author) - else: - materials_available_metadata.append(author) - - processed_authors.append(author["author_id"]) - - self._materials_showcase_model.setMetadata(materials_showcase_metadata) - self._materials_available_model.setMetadata(materials_available_metadata) - self._materials_generic_model.setMetadata(materials_generic_metadata) - - def reBuildPluginsModels(self) -> None: - plugins_showcase_metadata = [] - plugins_available_metadata = [] - - for item in self._server_response_data["packages"]: - if item["package_type"] == "plugin": - if "showcase" in item["tags"]: - plugins_showcase_metadata.append(item) - else: - plugins_available_metadata.append(item) - - self._plugins_showcase_model.setMetadata(plugins_showcase_metadata) - self._plugins_available_model.setMetadata(plugins_available_metadata) |