From 700ae4bebb2218610fbf0095f57e7aabb81e52ba Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 5 Dec 2018 10:48:06 +0100 Subject: [PATCH 01/40] Removed super spammy logging CURA-6006 --- plugins/Toolbox/src/Toolbox.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 562a964f01..2b78581deb 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -491,11 +491,8 @@ class Toolbox(QObject, Extension): def canUpdate(self, package_id: str) -> bool: local_package = self._package_manager.getInstalledPackageInfo(package_id) if local_package is None: - Logger.log("i", "Could not find package [%s] as installed in the package manager, fall back to check the old plugins", - package_id) local_package = self.getOldPluginPackageMetadata(package_id) if local_package is None: - Logger.log("i", "Could not find package [%s] in the old plugins", package_id) return False remote_package = self.getRemotePackage(package_id) From 978a01e4c89873b42909991ae5ecc49988beab33 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 5 Dec 2018 10:51:05 +0100 Subject: [PATCH 02/40] Fix typing & codestyle CURA-6006 --- plugins/Toolbox/src/Toolbox.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 2b78581deb..1fe7c961ba 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -31,8 +31,8 @@ i18n_catalog = i18nCatalog("cura") ## The Toolbox class is responsible of communicating with the server through the API class Toolbox(QObject, Extension): - DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" #type: str - DEFAULT_CLOUD_API_VERSION = 1 #type: int + DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str + DEFAULT_CLOUD_API_VERSION = 1 # type: int def __init__(self, application: CuraApplication) -> None: super().__init__() @@ -192,9 +192,9 @@ class Toolbox(QObject, Extension): return self.DEFAULT_CLOUD_API_ROOT if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): # type: ignore return self.DEFAULT_CLOUD_API_ROOT - if not cura.CuraVersion.CuraCloudAPIRoot: # type: ignore + if not cura.CuraVersion.CuraCloudAPIRoot: # type: ignore return self.DEFAULT_CLOUD_API_ROOT - return cura.CuraVersion.CuraCloudAPIRoot # type: ignore + return cura.CuraVersion.CuraCloudAPIRoot # type: ignore # Get the cloud API version from CuraVersion def _getCloudAPIVersion(self) -> int: @@ -202,9 +202,9 @@ class Toolbox(QObject, Extension): return self.DEFAULT_CLOUD_API_VERSION if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): # type: ignore return self.DEFAULT_CLOUD_API_VERSION - if not cura.CuraVersion.CuraCloudAPIVersion: # type: ignore + if not cura.CuraVersion.CuraCloudAPIVersion: # type: ignore return self.DEFAULT_CLOUD_API_VERSION - return cura.CuraVersion.CuraCloudAPIVersion # type: ignore + return cura.CuraVersion.CuraCloudAPIVersion # type: ignore # Get the packages version depending on Cura version settings. def _getSDKVersion(self) -> Union[int, str]: @@ -231,12 +231,6 @@ class Toolbox(QObject, Extension): # Make remote requests: self._makeRequestByType("packages") self._makeRequestByType("authors") - # TODO: Uncomment in the future when the tag-filtered api calls work in the cloud server - # self._makeRequestByType("plugins_showcase") - # self._makeRequestByType("plugins_available") - # self._makeRequestByType("materials_showcase") - # self._makeRequestByType("materials_available") - # self._makeRequestByType("materials_generic") # Gather installed packages: self._updateInstalledModels() @@ -281,7 +275,7 @@ class Toolbox(QObject, Extension): "description": plugin_data["plugin"]["description"] } return formatted - except: + except KeyError: Logger.log("w", "Unable to convert plugin meta data %s", str(plugin_data)) return None From d9135ac72fa9aa4aa155179fb44e57cda2d1ad96 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 5 Dec 2018 11:06:14 +0100 Subject: [PATCH 03/40] Fix more codestyle issues CURA-6006 --- plugins/Toolbox/src/Toolbox.py | 62 +++++++++++++++------------------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 1fe7c961ba..b6cfafe40c 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -559,34 +559,30 @@ class Toolbox(QObject, Extension): # Check for plugins that were installed with the old plugin browser def isOldPlugin(self, plugin_id: str) -> bool: - if plugin_id in self._old_plugin_ids: - return True - return False + 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 loadingComplete(self) -> bool: + def isLoadingComplete(self) -> bool: populated = 0 - for list in self._metadata.items(): - if len(list) > 0: + for metadata_list in self._metadata.items(): + if metadata_list: populated += 1 - if populated == len(self._metadata.items()): - return True - return False + return populated == len(self._metadata.items()) # Make API Calls # -------------------------------------------------------------------------- - def _makeRequestByType(self, type: str) -> None: - Logger.log("i", "Marketplace: Requesting %s metadata from server.", type) - request = QNetworkRequest(self._request_urls[type]) + def _makeRequestByType(self, request_type: str) -> None: + Logger.log("i", "Requesting %s metadata from server.", request_type) + request = QNetworkRequest(self._request_urls[request_type]) request.setRawHeader(*self._request_header) if self._network_manager: self._network_manager.get(request) @pyqtSlot(str) def startDownload(self, url: str) -> None: - Logger.log("i", "Marketplace: Attempting to download & install package from %s.", url) + Logger.log("i", "Attempting to download & install package from %s.", url) url = QUrl(url) self._download_request = QNetworkRequest(url) if hasattr(QNetworkRequest, "FollowRedirectsAttribute"): @@ -603,7 +599,7 @@ class Toolbox(QObject, Extension): @pyqtSlot() def cancelDownload(self) -> None: - Logger.log("i", "Marketplace: User cancelled the download of a package.") + Logger.log("i", "User cancelled the download of a package.") self.resetDownload() def resetDownload(self) -> None: @@ -647,10 +643,10 @@ class Toolbox(QObject, Extension): ] if reply.operation() == QNetworkAccessManager.GetOperation: - for type, url in self._request_urls.items(): + for response_type, url in self._request_urls.items(): # HACK: Do nothing because we'll handle these from the "packages" call - if type in do_not_handle: + if response_type in do_not_handle: continue if reply.url() == url: @@ -665,38 +661,35 @@ class Toolbox(QObject, Extension): return # Create model and apply metadata: - if not self._models[type]: - Logger.log("e", "Could not find the %s model.", type) + if not self._models[response_type]: + Logger.log("e", "Could not find the %s model.", response_type) break - self._metadata[type] = json_data["data"] - self._models[type].setMetadata(self._metadata[type]) + self._metadata[response_type] = json_data["data"] + self._models[response_type].setMetadata(self._metadata[response_type]) # Do some auto filtering # TODO: Make multiple API calls in the future to handle this - if type is "packages": - self._models[type].setFilter({"type": "plugin"}) + if response_type is "packages": + self._models[response_type].setFilter({"type": "plugin"}) self.buildMaterialsModels() self.buildPluginsModels() - if type is "authors": - self._models[type].setFilter({"package_types": "material"}) - if type is "materials_generic": - self._models[type].setFilter({"tags": "generic"}) + if response_type is "authors": + self._models[response_type].setFilter({"package_types": "material"}) + if response_type is "materials_generic": + self._models[response_type].setFilter({"tags": "generic"}) self.metadataChanged.emit() - if self.loadingComplete() is True: + if self.isLoadingComplete(): self.setViewPage("overview") - return except json.decoder.JSONDecodeError: - Logger.log("w", "Marketplace: Received invalid JSON for %s.", type) + Logger.log("w", "Received invalid JSON for %s.", response_type) break else: self.setViewPage("errored") self.resetDownload() - return - else: # Ignore any operation that is not a get operation pass @@ -717,10 +710,10 @@ class Toolbox(QObject, Extension): self._onDownloadComplete(file_path) def _onDownloadComplete(self, file_path: str) -> None: - Logger.log("i", "Marketplace: Download complete.") + Logger.log("i", "Download complete.") package_info = self._package_manager.getPackageInfo(file_path) if not package_info: - Logger.log("w", "Marketplace: Package file [%s] was not a valid CuraPackage.", file_path) + Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path) return license_content = self._package_manager.getPackageLicense(file_path) @@ -729,7 +722,6 @@ class Toolbox(QObject, Extension): return self.install(file_path) - return # Getter & Setters for Properties: # -------------------------------------------------------------------------- @@ -847,7 +839,7 @@ class Toolbox(QObject, Extension): self._metadata["materials_available"] = [] self._metadata["materials_generic"] = [] - processed_authors = [] # type: List[str] + processed_authors = [] # type: List[str] for item in self._metadata["packages"]: if item["package_type"] == "material": From abcc621cc62120a0f9f1e5f32599f8356e607dc2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 5 Dec 2018 11:29:44 +0100 Subject: [PATCH 04/40] Added missing typing --- plugins/Toolbox/src/AuthorsModel.py | 43 ++++++++++++++-------------- plugins/Toolbox/src/PackagesModel.py | 2 +- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/plugins/Toolbox/src/AuthorsModel.py b/plugins/Toolbox/src/AuthorsModel.py index bea3893504..6f4b5bd280 100644 --- a/plugins/Toolbox/src/AuthorsModel.py +++ b/plugins/Toolbox/src/AuthorsModel.py @@ -2,18 +2,19 @@ # Cura is released under the terms of the LGPLv3 or higher. import re -from typing import Dict +from typing import Dict, List, Optional, Union from PyQt5.QtCore import Qt, pyqtProperty, pyqtSignal from UM.Qt.ListModel import ListModel + ## Model that holds cura packages. By setting the filter property the instances held by this model can be changed. class AuthorsModel(ListModel): - def __init__(self, parent = None): + def __init__(self, parent = None) -> None: super().__init__(parent) - self._metadata = None + self._metadata = None # type: Optional[List[Dict[str, Union[str, List[str], int]]]] self.addRoleName(Qt.UserRole + 1, "id") self.addRoleName(Qt.UserRole + 2, "name") @@ -25,39 +26,39 @@ class AuthorsModel(ListModel): self.addRoleName(Qt.UserRole + 8, "description") # List of filters for queries. The result is the union of the each list of results. - self._filter = {} # type: Dict[str,str] + self._filter = {} # type: Dict[str, str] - def setMetadata(self, data): + def setMetadata(self, data: List[Dict[str, Union[str, List[str], int]]]): self._metadata = data self._update() - def _update(self): - items = [] + def _update(self) -> None: + items = [] # type: List[Dict[str, Union[str, List[str], int, None]]] if not self._metadata: - self.setItems([]) + self.setItems(items) return for author in self._metadata: items.append({ - "id": author["author_id"], - "name": author["display_name"], - "email": author["email"] if "email" in author else None, - "website": author["website"], - "package_count": author["package_count"] if "package_count" in author else 0, - "package_types": author["package_types"] if "package_types" in author else [], - "icon_url": author["icon_url"] if "icon_url" in author else None, - "description": "Material and quality profiles from {author_name}".format(author_name = author["display_name"]) + "id": author.get("author_id"), + "name": author.get("display_name"), + "email": author.get("email"), + "website": author.get("website"), + "package_count": author.get("package_count", 0), + "package_types": author.get("package_types", []), + "icon_url": author.get("icon_url"), + "description": "Material and quality profiles from {author_name}".format(author_name = author.get("display_name", "")) }) # Filter on all the key-word arguments. for key, value in self._filter.items(): if key is "package_types": - key_filter = lambda item, value = value: value in item["package_types"] + key_filter = lambda item, value = value: value in item["package_types"] # type: ignore elif "*" in value: - key_filter = lambda item, key = key, value = value: self._matchRegExp(item, key, value) + key_filter = lambda item, key = key, value = value: self._matchRegExp(item, key, value) # type: ignore else: - key_filter = lambda item, key = key, value = value: self._matchString(item, key, value) - items = filter(key_filter, items) + key_filter = lambda item, key = key, value = value: self._matchString(item, key, value) # type: ignore + items = filter(key_filter, items) # type: ignore # Execute all filters. filtered_items = list(items) @@ -72,7 +73,7 @@ class AuthorsModel(ListModel): self._filter = filter_dict self._update() - @pyqtProperty("QVariantMap", fset = setFilter, constant = True) + @pyqtProperty("QStringMap", fset = setFilter, constant = True) def filter(self) -> Dict[str, str]: return self._filter diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py index a31facf75a..2849a29c99 100644 --- a/plugins/Toolbox/src/PackagesModel.py +++ b/plugins/Toolbox/src/PackagesModel.py @@ -33,7 +33,7 @@ class PackagesModel(ListModel): self.addRoleName(Qt.UserRole + 12, "last_updated") self.addRoleName(Qt.UserRole + 13, "is_bundled") self.addRoleName(Qt.UserRole + 14, "is_active") - self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed + self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed self.addRoleName(Qt.UserRole + 16, "has_configs") self.addRoleName(Qt.UserRole + 17, "supported_configs") self.addRoleName(Qt.UserRole + 18, "download_count") From b1440737e641a708683c06ea12c7874dd3fb8ab7 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 5 Dec 2018 13:21:17 +0100 Subject: [PATCH 05/40] Remove a whole bunch of unused code CURA-6006 --- plugins/Toolbox/src/AuthorsModel.py | 2 +- plugins/Toolbox/src/Toolbox.py | 70 +++++++++-------------------- 2 files changed, 22 insertions(+), 50 deletions(-) diff --git a/plugins/Toolbox/src/AuthorsModel.py b/plugins/Toolbox/src/AuthorsModel.py index 6f4b5bd280..8fafda54ec 100644 --- a/plugins/Toolbox/src/AuthorsModel.py +++ b/plugins/Toolbox/src/AuthorsModel.py @@ -73,7 +73,7 @@ class AuthorsModel(ListModel): self._filter = filter_dict self._update() - @pyqtProperty("QStringMap", fset = setFilter, constant = True) + @pyqtProperty("QVariantMap", fset = setFilter, constant = True) def filter(self) -> Dict[str, str]: return self._filter diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index b6cfafe40c..5db3dd934f 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -70,13 +70,6 @@ class Toolbox(QObject, Extension): self._metadata = { "authors": [], "packages": [], - "plugins_showcase": [], - "plugins_available": [], - "plugins_installed": [], - "materials_showcase": [], - "materials_available": [], - "materials_installed": [], - "materials_generic": [] } # type: Dict[str, List[Any]] # Models: @@ -178,12 +171,7 @@ class Toolbox(QObject, Extension): ) self._request_urls = { "authors": QUrl("{base_url}/authors".format(base_url = self._api_url)), - "packages": QUrl("{base_url}/packages".format(base_url = self._api_url)), - "plugins_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)), - "plugins_available": QUrl("{base_url}/packages?package_type=plugin".format(base_url = self._api_url)), - "materials_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)), - "materials_available": QUrl("{base_url}/packages?package_type=material".format(base_url = self._api_url)), - "materials_generic": QUrl("{base_url}/packages?package_type=material&tags=generic".format(base_url = self._api_url)) + "packages": QUrl("{base_url}/packages".format(base_url = self._api_url)) } # Get the API root for the packages API depending on Cura version settings. @@ -606,8 +594,8 @@ class Toolbox(QObject, Extension): if self._download_reply: try: self._download_reply.downloadProgress.disconnect(self._onDownloadProgress) - except TypeError: #Raised when the method is not connected to the signal yet. - pass #Don't need to disconnect. + except TypeError: # Raised when the method is not connected to the signal yet. + pass # Don't need to disconnect. self._download_reply.abort() self._download_reply = None self._download_request = None @@ -633,22 +621,8 @@ class Toolbox(QObject, Extension): self.resetDownload() return - # HACK: These request are not handled independently at this moment, but together from the "packages" call - do_not_handle = [ - "materials_available", - "materials_showcase", - "materials_generic", - "plugins_available", - "plugins_showcase", - ] - if reply.operation() == QNetworkAccessManager.GetOperation: for response_type, url in self._request_urls.items(): - - # HACK: Do nothing because we'll handle these from the "packages" call - if response_type in do_not_handle: - continue - if reply.url() == url: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: try: @@ -672,11 +646,10 @@ class Toolbox(QObject, Extension): # TODO: Make multiple API calls in the future to handle this if response_type is "packages": self._models[response_type].setFilter({"type": "plugin"}) - self.buildMaterialsModels() + self.reBuildMaterialsModels() self.buildPluginsModels() - if response_type is "authors": + elif response_type is "authors": self._models[response_type].setFilter({"package_types": "material"}) - if response_type is "materials_generic": self._models[response_type].setFilter({"tags": "generic"}) self.metadataChanged.emit() @@ -834,10 +807,10 @@ class Toolbox(QObject, Extension): # HACK(S): # -------------------------------------------------------------------------- - def buildMaterialsModels(self) -> None: - self._metadata["materials_showcase"] = [] - self._metadata["materials_available"] = [] - self._metadata["materials_generic"] = [] + def reBuildMaterialsModels(self) -> None: + materials_showcase_metadata = [] + materials_available_metadata = [] + materials_generic_metadata = [] processed_authors = [] # type: List[str] @@ -850,30 +823,29 @@ class Toolbox(QObject, Extension): # Generic materials to be in the same section if "generic" in item["tags"]: - self._metadata["materials_generic"].append(item) + materials_generic_metadata.append(item) else: if "showcase" in item["tags"]: - self._metadata["materials_showcase"].append(author) + materials_showcase_metadata.append(author) else: - self._metadata["materials_available"].append(author) + materials_available_metadata.append(author) processed_authors.append(author["author_id"]) - self._models["materials_showcase"].setMetadata(self._metadata["materials_showcase"]) - self._models["materials_available"].setMetadata(self._metadata["materials_available"]) - self._models["materials_generic"].setMetadata(self._metadata["materials_generic"]) + self._models["materials_showcase"].setMetadata(materials_showcase_metadata) + self._models["materials_available"].setMetadata(materials_available_metadata) + self._models["materials_generic"].setMetadata(materials_generic_metadata) def buildPluginsModels(self) -> None: - self._metadata["plugins_showcase"] = [] - self._metadata["plugins_available"] = [] + plugins_showcase_metadata = [] + plugins_available_metadata = [] for item in self._metadata["packages"]: if item["package_type"] == "plugin": - if "showcase" in item["tags"]: - self._metadata["plugins_showcase"].append(item) + plugins_showcase_metadata.append(item) else: - self._metadata["plugins_available"].append(item) + plugins_available_metadata.append(item) - self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"]) - self._models["plugins_available"].setMetadata(self._metadata["plugins_available"]) + self._models["plugins_showcase"].setMetadata(plugins_showcase_metadata) + self._models["plugins_available"].setMetadata(plugins_available_metadata) From 07d210483c91d77c3198548a386247fdc993df2f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 5 Dec 2018 13:42:13 +0100 Subject: [PATCH 06/40] Greatly decrease the bloat / complexity of the toolbox There was a lot of stuff going on that didn't need to happen, so I cut those parts out in order to improve the overview. --- plugins/Toolbox/src/AuthorsModel.py | 5 +-- plugins/Toolbox/src/PackagesModel.py | 5 +-- plugins/Toolbox/src/Toolbox.py | 54 ++++++++++++++-------------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/plugins/Toolbox/src/AuthorsModel.py b/plugins/Toolbox/src/AuthorsModel.py index 8fafda54ec..877f8256ee 100644 --- a/plugins/Toolbox/src/AuthorsModel.py +++ b/plugins/Toolbox/src/AuthorsModel.py @@ -29,8 +29,9 @@ class AuthorsModel(ListModel): self._filter = {} # type: Dict[str, str] def setMetadata(self, data: List[Dict[str, Union[str, List[str], int]]]): - self._metadata = data - self._update() + if self._metadata != data: + self._metadata = data + self._update() def _update(self) -> None: items = [] # type: List[Dict[str, Union[str, List[str], int, None]]] diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py index 2849a29c99..f941804653 100644 --- a/plugins/Toolbox/src/PackagesModel.py +++ b/plugins/Toolbox/src/PackagesModel.py @@ -45,8 +45,9 @@ class PackagesModel(ListModel): self._filter = {} # type: Dict[str, str] def setMetadata(self, data): - self._metadata = data - self._update() + if self._metadata != data: + self._metadata = data + self._update() def _update(self): items = [] diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 5db3dd934f..7b1442ff3c 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -66,8 +66,8 @@ class Toolbox(QObject, Extension): self._old_plugin_ids = set() # type: Set[str] self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]] - # Data: - self._metadata = { + # The responses as given by the server parsed to a list. + self._server_response_data = { "authors": [], "packages": [], } # type: Dict[str, List[Any]] @@ -301,13 +301,13 @@ class Toolbox(QObject, Extension): 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._metadata["plugins_installed"] = all_packages["plugin"] + list(self._old_plugin_metadata.values()) - self._models["plugins_installed"].setMetadata(self._metadata["plugins_installed"]) + self._server_response_data["plugins_installed"] = all_packages["plugin"] + list(self._old_plugin_metadata.values()) + self._models["plugins_installed"].setMetadata(self._server_response_data["plugins_installed"]) self.metadataChanged.emit() if "material" in all_packages: - self._metadata["materials_installed"] = all_packages["material"] + self._server_response_data["materials_installed"] = all_packages["material"] # TODO: ADD MATERIALS HERE ONCE MATERIALS PORTION OF TOOLBOX IS LIVE - self._models["materials_installed"].setMetadata(self._metadata["materials_installed"]) + self._models["materials_installed"].setMetadata(self._server_response_data["materials_installed"]) self.metadataChanged.emit() @pyqtSlot(str) @@ -461,7 +461,7 @@ class Toolbox(QObject, Extension): 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._metadata["packages"]: + for package in self._server_response_data["packages"]: if package["package_id"] == package_id: remote_package = package break @@ -524,7 +524,7 @@ class Toolbox(QObject, Extension): @pyqtSlot(str, result = int) def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int: count = 0 - for package in self._metadata["materials_installed"]: + for package in self._server_response_data["materials_installed"]: if package["author"]["author_id"] == author_id: count += 1 return count @@ -533,7 +533,7 @@ class Toolbox(QObject, Extension): @pyqtSlot(str, result = int) def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int: count = 0 - for package in self._metadata["packages"]: + for package in self._server_response_data["packages"]: if package["package_type"] == "material": if package["author"]["author_id"] == author_id: count += 1 @@ -554,10 +554,10 @@ class Toolbox(QObject, Extension): def isLoadingComplete(self) -> bool: populated = 0 - for metadata_list in self._metadata.items(): + for metadata_list in self._server_response_data.items(): if metadata_list: populated += 1 - return populated == len(self._metadata.items()) + return populated == len(self._server_response_data.items()) # Make API Calls # -------------------------------------------------------------------------- @@ -639,15 +639,13 @@ class Toolbox(QObject, Extension): Logger.log("e", "Could not find the %s model.", response_type) break - self._metadata[response_type] = json_data["data"] - self._models[response_type].setMetadata(self._metadata[response_type]) + self._server_response_data[response_type] = json_data["data"] + self._models[response_type].setMetadata(self._server_response_data[response_type]) - # Do some auto filtering - # TODO: Make multiple API calls in the future to handle this if response_type is "packages": self._models[response_type].setFilter({"type": "plugin"}) self.reBuildMaterialsModels() - self.buildPluginsModels() + self.reBuildPluginsModels() elif response_type is "authors": self._models[response_type].setFilter({"package_types": "material"}) self._models[response_type].setFilter({"tags": "generic"}) @@ -743,39 +741,39 @@ class Toolbox(QObject, Extension): # Exposed Models: # -------------------------------------------------------------------------- - @pyqtProperty(QObject, notify = metadataChanged) + @pyqtProperty(QObject, constant=True) def authorsModel(self) -> AuthorsModel: return cast(AuthorsModel, self._models["authors"]) - @pyqtProperty(QObject, notify = metadataChanged) + @pyqtProperty(QObject, constant=True) def packagesModel(self) -> PackagesModel: return cast(PackagesModel, self._models["packages"]) - @pyqtProperty(QObject, notify = metadataChanged) + @pyqtProperty(QObject, constant=True) def pluginsShowcaseModel(self) -> PackagesModel: return cast(PackagesModel, self._models["plugins_showcase"]) - @pyqtProperty(QObject, notify = metadataChanged) + @pyqtProperty(QObject, constant=True) def pluginsAvailableModel(self) -> PackagesModel: return cast(PackagesModel, self._models["plugins_available"]) - @pyqtProperty(QObject, notify = metadataChanged) + @pyqtProperty(QObject, constant=True) def pluginsInstalledModel(self) -> PackagesModel: return cast(PackagesModel, self._models["plugins_installed"]) - @pyqtProperty(QObject, notify = metadataChanged) + @pyqtProperty(QObject, constant=True) def materialsShowcaseModel(self) -> AuthorsModel: return cast(AuthorsModel, self._models["materials_showcase"]) - @pyqtProperty(QObject, notify = metadataChanged) + @pyqtProperty(QObject, constant=True) def materialsAvailableModel(self) -> AuthorsModel: return cast(AuthorsModel, self._models["materials_available"]) - @pyqtProperty(QObject, notify = metadataChanged) + @pyqtProperty(QObject, constant=True) def materialsInstalledModel(self) -> PackagesModel: return cast(PackagesModel, self._models["materials_installed"]) - @pyqtProperty(QObject, notify=metadataChanged) + @pyqtProperty(QObject, constant=True) def materialsGenericModel(self) -> PackagesModel: return cast(PackagesModel, self._models["materials_generic"]) @@ -814,7 +812,7 @@ class Toolbox(QObject, Extension): processed_authors = [] # type: List[str] - for item in self._metadata["packages"]: + for item in self._server_response_data["packages"]: if item["package_type"] == "material": author = item["author"] @@ -836,11 +834,11 @@ class Toolbox(QObject, Extension): self._models["materials_available"].setMetadata(materials_available_metadata) self._models["materials_generic"].setMetadata(materials_generic_metadata) - def buildPluginsModels(self) -> None: + def reBuildPluginsModels(self) -> None: plugins_showcase_metadata = [] plugins_available_metadata = [] - for item in self._metadata["packages"]: + for item in self._server_response_data["packages"]: if item["package_type"] == "plugin": if "showcase" in item["tags"]: plugins_showcase_metadata.append(item) From a52f866f818324994b1a1b34f06805ffd3ed24d0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 5 Dec 2018 14:03:32 +0100 Subject: [PATCH 07/40] Move most models out of dictionary CURA-6006 --- plugins/Toolbox/src/Toolbox.py | 47 ++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 7b1442ff3c..ada81dbc07 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -70,21 +70,25 @@ class Toolbox(QObject, Extension): self._server_response_data = { "authors": [], "packages": [], + "plugins_installed": [], + "materials_installed": [] } # type: Dict[str, List[Any]] # Models: self._models = { "authors": AuthorsModel(self), "packages": PackagesModel(self), - "plugins_showcase": PackagesModel(self), - "plugins_available": PackagesModel(self), - "plugins_installed": PackagesModel(self), - "materials_showcase": AuthorsModel(self), - "materials_available": AuthorsModel(self), - "materials_installed": PackagesModel(self), - "materials_generic": PackagesModel(self) } # type: Dict[str, ListModel] + self._plugins_showcase_model = PackagesModel(self) + self._plugins_available_model = PackagesModel(self) + self._plugins_installed_model = PackagesModel(self) + + self._materials_showcase_model = AuthorsModel(self) + self._materials_available_model = AuthorsModel(self) + self._materials_installed_model = PackagesModel(self) + self._materials_generic_model = PackagesModel(self) + # These properties are for keeping track of the UI state: # ---------------------------------------------------------------------- # View category defines which filter to use, and therefore effectively @@ -302,12 +306,11 @@ class Toolbox(QObject, Extension): self._old_plugin_metadata = {k: v for k, v in self._old_plugin_metadata.items() if k in self._old_plugin_ids} self._server_response_data["plugins_installed"] = all_packages["plugin"] + list(self._old_plugin_metadata.values()) - self._models["plugins_installed"].setMetadata(self._server_response_data["plugins_installed"]) + self._plugins_installed_model.setMetadata(self._server_response_data["plugins_installed"]) self.metadataChanged.emit() if "material" in all_packages: self._server_response_data["materials_installed"] = all_packages["material"] - # TODO: ADD MATERIALS HERE ONCE MATERIALS PORTION OF TOOLBOX IS LIVE - self._models["materials_installed"].setMetadata(self._server_response_data["materials_installed"]) + self._materials_installed_model.setMetadata(all_packages["material"]) self.metadataChanged.emit() @pyqtSlot(str) @@ -751,31 +754,31 @@ class Toolbox(QObject, Extension): @pyqtProperty(QObject, constant=True) def pluginsShowcaseModel(self) -> PackagesModel: - return cast(PackagesModel, self._models["plugins_showcase"]) + return self._plugins_showcase_model @pyqtProperty(QObject, constant=True) def pluginsAvailableModel(self) -> PackagesModel: - return cast(PackagesModel, self._models["plugins_available"]) + return self._plugins_available_model @pyqtProperty(QObject, constant=True) def pluginsInstalledModel(self) -> PackagesModel: - return cast(PackagesModel, self._models["plugins_installed"]) + return self._plugins_installed_model @pyqtProperty(QObject, constant=True) def materialsShowcaseModel(self) -> AuthorsModel: - return cast(AuthorsModel, self._models["materials_showcase"]) + return self._materials_showcase_model @pyqtProperty(QObject, constant=True) def materialsAvailableModel(self) -> AuthorsModel: - return cast(AuthorsModel, self._models["materials_available"]) + return self._materials_available_model @pyqtProperty(QObject, constant=True) def materialsInstalledModel(self) -> PackagesModel: - return cast(PackagesModel, self._models["materials_installed"]) + return self._materials_installed_model @pyqtProperty(QObject, constant=True) def materialsGenericModel(self) -> PackagesModel: - return cast(PackagesModel, self._models["materials_generic"]) + return self._materials_generic_model # Filter Models: # -------------------------------------------------------------------------- @@ -830,9 +833,9 @@ class Toolbox(QObject, Extension): processed_authors.append(author["author_id"]) - self._models["materials_showcase"].setMetadata(materials_showcase_metadata) - self._models["materials_available"].setMetadata(materials_available_metadata) - self._models["materials_generic"].setMetadata(materials_generic_metadata) + 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 = [] @@ -845,5 +848,5 @@ class Toolbox(QObject, Extension): else: plugins_available_metadata.append(item) - self._models["plugins_showcase"].setMetadata(plugins_showcase_metadata) - self._models["plugins_available"].setMetadata(plugins_available_metadata) + self._plugins_showcase_model.setMetadata(plugins_showcase_metadata) + self._plugins_available_model.setMetadata(plugins_available_metadata) From d6630b68815781ba92edae3510aaf62cf77b4d0d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 5 Dec 2018 14:09:55 +0100 Subject: [PATCH 08/40] Removed some more cases where data was duplicated and re-used for different purposes CURA-6006 --- plugins/Toolbox/src/Toolbox.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index ada81dbc07..f70543d5d7 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -69,16 +69,14 @@ class Toolbox(QObject, Extension): # The responses as given by the server parsed to a list. self._server_response_data = { "authors": [], - "packages": [], - "plugins_installed": [], - "materials_installed": [] + "packages": [] } # type: Dict[str, List[Any]] # Models: self._models = { "authors": AuthorsModel(self), "packages": PackagesModel(self), - } # type: Dict[str, ListModel] + } # type: Dict[str, Union[AuthorsModel, PackagesModel]] self._plugins_showcase_model = PackagesModel(self) self._plugins_available_model = PackagesModel(self) @@ -305,11 +303,9 @@ class Toolbox(QObject, Extension): 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._server_response_data["plugins_installed"] = all_packages["plugin"] + list(self._old_plugin_metadata.values()) - self._plugins_installed_model.setMetadata(self._server_response_data["plugins_installed"]) + self._plugins_installed_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values())) self.metadataChanged.emit() if "material" in all_packages: - self._server_response_data["materials_installed"] = all_packages["material"] self._materials_installed_model.setMetadata(all_packages["material"]) self.metadataChanged.emit() @@ -527,8 +523,8 @@ class Toolbox(QObject, Extension): @pyqtSlot(str, result = int) def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int: count = 0 - for package in self._server_response_data["materials_installed"]: - if package["author"]["author_id"] == author_id: + for package in self._materials_installed_model.items: + if package["author_id"] == author_id: count += 1 return count From 00e95e68eb974e19ac3f6960b86965dd9ada8fbf Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 5 Dec 2018 14:26:47 +0100 Subject: [PATCH 09/40] Removed unneeded Marketplace tag in logging The logger does that all by itself already. CURA-6006 --- plugins/Toolbox/src/Toolbox.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index f70543d5d7..cd20d26eca 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -781,7 +781,7 @@ class Toolbox(QObject, Extension): @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", "Marketplace: Couldn't filter %s model because it doesn't exist.", 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() @@ -789,7 +789,7 @@ class Toolbox(QObject, Extension): @pyqtSlot(str, "QVariantMap") def setFilters(self, model_type: str, filter_dict: dict) -> None: if not self._models[model_type]: - Logger.log("w", "Marketplace: Couldn't filter %s model because it doesn't exist.", 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() @@ -797,7 +797,7 @@ class Toolbox(QObject, Extension): @pyqtSlot(str) def removeFilters(self, model_type: str) -> None: if not self._models[model_type]: - Logger.log("w", "Marketplace: Couldn't remove filters on %s model because it doesn't exist.", 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() From 838949dac74831e38e9f07fe7d628af7807320db Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Dec 2018 09:45:27 +0100 Subject: [PATCH 10/40] Moved qml pages of toolbox to a loader This dramatically improves the loading of the toolbox dialog CURA-6006 --- plugins/Toolbox/resources/qml/Toolbox.qml | 54 ++++++++++----------- plugins/Toolbox/src/Toolbox.py | 59 +++++++++++------------ 2 files changed, 52 insertions(+), 61 deletions(-) diff --git a/plugins/Toolbox/resources/qml/Toolbox.qml b/plugins/Toolbox/resources/qml/Toolbox.qml index 853cec399d..d3d980b0b3 100644 --- a/plugins/Toolbox/resources/qml/Toolbox.qml +++ b/plugins/Toolbox/resources/qml/Toolbox.qml @@ -44,36 +44,31 @@ Window top: header.bottom bottom: footer.top } - // TODO: This could be improved using viewFilter instead of viewCategory - ToolboxLoadingPage + + Loader { - id: viewLoading - visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "loading" - } - ToolboxErrorPage - { - id: viewErrored - visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "errored" - } - ToolboxDownloadsPage - { - id: viewDownloads - visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "overview" - } - ToolboxDetailPage - { - id: viewDetail - visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "detail" - } - ToolboxAuthorPage - { - id: viewAuthor - visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "author" - } - ToolboxInstalledPage - { - id: installedPluginList - visible: toolbox.viewCategory == "installed" + anchors.fill:parent + source: + { + if(toolbox.viewCategory == "installed") + { + return "ToolboxInstalledPage.qml" + } + + switch (toolbox.viewPage) + { + case "loading": + return "ToolboxLoadingPage.qml" + case "errored": + return "ToolboxErrorPage.qml" + case "overview": + return "ToolboxDownloadsPage.qml" + case "detail": + return "ToolboxDetailPage.qml" + case "author": + return "ToolboxAuthorPage.qml" + } + } } } @@ -95,6 +90,7 @@ Window licenseDialog.show(); } } + ToolboxLicenseDialog { id: licenseDialog diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index cd20d26eca..c8349827a9 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -622,44 +622,39 @@ class Toolbox(QObject, Extension): if reply.operation() == QNetworkAccessManager.GetOperation: for response_type, url in self._request_urls.items(): - if reply.url() == url: - if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: - try: - json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) + if reply.url() != url: + continue - # Check for errors: - if "errors" in json_data: - for error in json_data["errors"]: - Logger.log("e", "%s", error["title"]) - return + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: + try: + json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) + except json.decoder.JSONDecodeError: + Logger.log("w", "Received invalid JSON for %s.", response_type) + break - # Create model and apply metadata: - if not self._models[response_type]: - Logger.log("e", "Could not find the %s model.", response_type) - break - - self._server_response_data[response_type] = json_data["data"] - self._models[response_type].setMetadata(self._server_response_data[response_type]) + # Check for errors: + if "errors" in json_data: + for error in json_data["errors"]: + Logger.log("e", "%s", error["title"]) + return - if response_type is "packages": - self._models[response_type].setFilter({"type": "plugin"}) - self.reBuildMaterialsModels() - self.reBuildPluginsModels() - elif response_type is "authors": - self._models[response_type].setFilter({"package_types": "material"}) - self._models[response_type].setFilter({"tags": "generic"}) + self._server_response_data[response_type] = json_data["data"] + self._models[response_type].setMetadata(json_data["data"]) - self.metadataChanged.emit() + if response_type is "packages": + self._models["packages"].setFilter({"type": "plugin"}) + self.reBuildMaterialsModels() + self.reBuildPluginsModels() + elif response_type is "authors": + self._models["authors"].setFilter({"tags": "generic"}) - if self.isLoadingComplete(): - self.setViewPage("overview") + self.metadataChanged.emit() - except json.decoder.JSONDecodeError: - Logger.log("w", "Received invalid JSON for %s.", response_type) - break - else: - self.setViewPage("errored") - self.resetDownload() + if self.isLoadingComplete(): + self.setViewPage("overview") + else: + self.setViewPage("errored") + self.resetDownload() else: # Ignore any operation that is not a get operation pass From 6a466c99b241035a6dc6d9cbbf174f39d51aad3d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Dec 2018 09:54:32 +0100 Subject: [PATCH 11/40] Make the progressButton use signals instead of functions Although the naming is still a bit off, it's much cleaner to use signals instead of functions. It's also more in line with how default QML components handle these kind of situations CURA-6006 --- plugins/Toolbox/resources/qml/Toolbox.qml | 2 +- .../qml/ToolboxDetailTileActions.qml | 23 ++++++++----------- .../qml/ToolboxInstalledTileActions.qml | 8 +++---- .../resources/qml/ToolboxProgressButton.qml | 12 +++++----- 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/plugins/Toolbox/resources/qml/Toolbox.qml b/plugins/Toolbox/resources/qml/Toolbox.qml index d3d980b0b3..b2bab4355a 100644 --- a/plugins/Toolbox/resources/qml/Toolbox.qml +++ b/plugins/Toolbox/resources/qml/Toolbox.qml @@ -90,7 +90,7 @@ Window licenseDialog.show(); } } - + ToolboxLicenseDialog { id: licenseDialog diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml index cd1e4cdbda..72a9d14dcd 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml @@ -18,19 +18,15 @@ Column id: installButton active: toolbox.isDownloading && toolbox.activePackage == model complete: installed - readyAction: function() + onReadyAction: { toolbox.activePackage = model toolbox.startDownload(model.download_url) } - activeAction: function() - { - toolbox.cancelDownload() - } - completeAction: function() - { - toolbox.viewCategory = "installed" - } + onActiveAction: toolbox.cancelDownload() + + onCompleteAction: toolbox.viewCategory = "installed" + // Don't allow installing while another download is running enabled: installed || !(toolbox.isDownloading && toolbox.activePackage != model) opacity: enabled ? 1.0 : 0.5 @@ -44,20 +40,19 @@ Column readyLabel: catalog.i18nc("@action:button", "Update") activeLabel: catalog.i18nc("@action:button", "Updating") completeLabel: catalog.i18nc("@action:button", "Updated") - readyAction: function() + + onReadyAction: { toolbox.activePackage = model toolbox.update(model.id) } - activeAction: function() - { - toolbox.cancelDownload() - } + onActiveAction: toolbox.cancelDownload() // Don't allow installing while another download is running enabled: !(toolbox.isDownloading && toolbox.activePackage != model) opacity: enabled ? 1.0 : 0.5 visible: canUpdate } + Connections { target: toolbox diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml index 8fd88b1cfd..621ecd96ea 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml @@ -30,15 +30,13 @@ Column readyLabel: catalog.i18nc("@action:button", "Update") activeLabel: catalog.i18nc("@action:button", "Updating") completeLabel: catalog.i18nc("@action:button", "Updated") - readyAction: function() + onReadyAction: { toolbox.activePackage = model toolbox.update(model.id) } - activeAction: function() - { - toolbox.cancelDownload() - } + onActiveAction: toolbox.cancelDownload() + // Don't allow installing while another download is running enabled: !(toolbox.isDownloading && toolbox.activePackage != model) opacity: enabled ? 1.0 : 0.5 diff --git a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml index 2744e40ec9..00b0b985f5 100644 --- a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml +++ b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml @@ -18,9 +18,9 @@ Item property var activeLabel: catalog.i18nc("@action:button", "Cancel") property var completeLabel: catalog.i18nc("@action:button", "Installed") - property var readyAction: null // Action when button is ready and clicked (likely install) - property var activeAction: null // Action when button is active and clicked (likely cancel) - property var completeAction: null // Action when button is complete and clicked (likely go to installed) + signal readyAction() // Action when button is ready and clicked (likely install) + signal activeAction() // Action when button is active and clicked (likely cancel) + signal completeAction() // Action when button is complete and clicked (likely go to installed) width: UM.Theme.getSize("toolbox_action_button").width height: UM.Theme.getSize("toolbox_action_button").height @@ -47,15 +47,15 @@ Item { if (complete) { - return completeAction() + completeAction() } else if (active) { - return activeAction() + activeAction() } else { - return readyAction() + readyAction() } } style: ButtonStyle From 2602d5bf02287745831d3d264bbc8e3dcda37541 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Dec 2018 10:46:19 +0100 Subject: [PATCH 12/40] Add changed checks to prevent unneeded signals from being fired CURA-6006 --- plugins/Toolbox/src/Toolbox.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index c8349827a9..68919cf987 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -546,7 +546,7 @@ class Toolbox(QObject, Extension): # 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 + 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) @@ -709,8 +709,9 @@ class Toolbox(QObject, Extension): return self._is_downloading def setActivePackage(self, package: Dict[str, Any]) -> None: - self._active_package = package - self.activePackageChanged.emit() + if self._active_package != package: + self._active_package = package + self.activePackageChanged.emit() ## The active package is the package that is currently being downloaded @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged) @@ -718,16 +719,18 @@ class Toolbox(QObject, Extension): return self._active_package def setViewCategory(self, category: str = "plugin") -> None: - self._view_category = category - self.viewChanged.emit() + if self._view_category != category: + self._view_category = category + self.viewChanged.emit() @pyqtProperty(str, fset = setViewCategory, notify = viewChanged) def viewCategory(self) -> str: return self._view_category def setViewPage(self, page: str = "overview") -> None: - self._view_page = page - self.viewChanged.emit() + if self._view_page != page: + self._view_page = page + self.viewChanged.emit() @pyqtProperty(str, fset = setViewPage, notify = viewChanged) def viewPage(self) -> str: From 54def4edee6d743eaf43988a8b2568bebc8fee53 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Dec 2018 14:12:35 +0100 Subject: [PATCH 13/40] Revert "Moved qml pages of toolbox to a loader" This reverts commit 838949dac74831e38e9f07fe7d628af7807320db. --- plugins/Toolbox/resources/qml/Toolbox.qml | 55 +++++++++++---------- plugins/Toolbox/src/Toolbox.py | 59 ++++++++++++----------- 2 files changed, 62 insertions(+), 52 deletions(-) diff --git a/plugins/Toolbox/resources/qml/Toolbox.qml b/plugins/Toolbox/resources/qml/Toolbox.qml index b2bab4355a..7cc5a730f2 100644 --- a/plugins/Toolbox/resources/qml/Toolbox.qml +++ b/plugins/Toolbox/resources/qml/Toolbox.qml @@ -44,31 +44,36 @@ Window top: header.bottom bottom: footer.top } - - Loader + // TODO: This could be improved using viewFilter instead of viewCategory + ToolboxLoadingPage { - anchors.fill:parent - source: - { - if(toolbox.viewCategory == "installed") - { - return "ToolboxInstalledPage.qml" - } - - switch (toolbox.viewPage) - { - case "loading": - return "ToolboxLoadingPage.qml" - case "errored": - return "ToolboxErrorPage.qml" - case "overview": - return "ToolboxDownloadsPage.qml" - case "detail": - return "ToolboxDetailPage.qml" - case "author": - return "ToolboxAuthorPage.qml" - } - } + id: viewLoading + visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "loading" + } + ToolboxErrorPage + { + id: viewErrored + visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "errored" + } + ToolboxDownloadsPage + { + id: viewDownloads + visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "overview" + } + ToolboxDetailPage + { + id: viewDetail + visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "detail" + } + ToolboxAuthorPage + { + id: viewAuthor + visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "author" + } + ToolboxInstalledPage + { + id: installedPluginList + visible: toolbox.viewCategory == "installed" } } @@ -90,7 +95,7 @@ Window licenseDialog.show(); } } - + ToolboxLicenseDialog { id: licenseDialog diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 68919cf987..ef67dc3c86 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -622,39 +622,44 @@ class Toolbox(QObject, Extension): if reply.operation() == QNetworkAccessManager.GetOperation: for response_type, url in self._request_urls.items(): - if reply.url() != url: - continue + if reply.url() == url: + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: + try: + json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) - if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: - try: - json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received invalid JSON for %s.", response_type) - break + # Check for errors: + if "errors" in json_data: + for error in json_data["errors"]: + Logger.log("e", "%s", error["title"]) + return - # Check for errors: - if "errors" in json_data: - for error in json_data["errors"]: - Logger.log("e", "%s", error["title"]) - return + # Create model and apply metadata: + if not self._models[response_type]: + Logger.log("e", "Could not find the %s model.", response_type) + break + + self._server_response_data[response_type] = json_data["data"] + self._models[response_type].setMetadata(self._server_response_data[response_type]) - self._server_response_data[response_type] = json_data["data"] - self._models[response_type].setMetadata(json_data["data"]) + if response_type is "packages": + self._models[response_type].setFilter({"type": "plugin"}) + self.reBuildMaterialsModels() + self.reBuildPluginsModels() + elif response_type is "authors": + self._models[response_type].setFilter({"package_types": "material"}) + self._models[response_type].setFilter({"tags": "generic"}) - if response_type is "packages": - self._models["packages"].setFilter({"type": "plugin"}) - self.reBuildMaterialsModels() - self.reBuildPluginsModels() - elif response_type is "authors": - self._models["authors"].setFilter({"tags": "generic"}) + self.metadataChanged.emit() - self.metadataChanged.emit() + if self.isLoadingComplete(): + self.setViewPage("overview") - if self.isLoadingComplete(): - self.setViewPage("overview") - else: - self.setViewPage("errored") - self.resetDownload() + except json.decoder.JSONDecodeError: + Logger.log("w", "Received invalid JSON for %s.", response_type) + break + else: + self.setViewPage("errored") + self.resetDownload() else: # Ignore any operation that is not a get operation pass From a77ad329993ae46799376689c4908de06292c801 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Dec 2018 14:35:07 +0100 Subject: [PATCH 14/40] Move all the seperate tiles into loaders instead of the entire page Otherwise the details selection didn't work anymore and I didn't want to add more hacks. CURA-6006 --- .../resources/qml/ToolboxDetailList.qml | 7 ++++++- .../resources/qml/ToolboxDownloadsGrid.qml | 10 ++++++---- .../qml/ToolboxDownloadsShowcase.qml | 19 +++++++++++-------- .../resources/qml/ToolboxInstalledPage.qml | 12 ++++++++++-- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailList.qml b/plugins/Toolbox/resources/qml/ToolboxDetailList.qml index 2e5eae098c..1700a58ebe 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailList.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailList.qml @@ -26,10 +26,15 @@ Item } height: childrenRect.height + 2 * UM.Theme.getSize("wide_margin").height spacing: UM.Theme.getSize("default_margin").height + Repeater { model: toolbox.packagesModel - delegate: ToolboxDetailTile {} + delegate: Loader + { + asynchronous: true + source: "ToolboxDetailTile.qml" + } } } } diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml index c586828969..3e2643938b 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml @@ -24,7 +24,7 @@ Column color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("medium") } - GridLayout + Grid { id: grid width: parent.width - 2 * parent.padding @@ -34,10 +34,12 @@ Column Repeater { model: gridArea.model - delegate: ToolboxDownloadsGridTile + delegate: Loader { - Layout.preferredWidth: (grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns - Layout.preferredHeight: UM.Theme.getSize("toolbox_thumbnail_small").height + asynchronous: true + width: (grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns + height: UM.Theme.getSize("toolbox_thumbnail_small").height + source: "ToolboxDownloadsGridTile.qml" } } } diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml index 46f5debfdd..9851128076 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml @@ -30,23 +30,26 @@ Rectangle height: childrenRect.height spacing: UM.Theme.getSize("wide_margin").width columns: 3 - anchors - { - horizontalCenter: parent.horizontalCenter - } + anchors.horizontalCenter: parent.horizontalCenter + Repeater { - model: { - if ( toolbox.viewCategory == "plugin" ) + model: + { + if (toolbox.viewCategory == "plugin") { return toolbox.pluginsShowcaseModel } - if ( toolbox.viewCategory == "material" ) + if (toolbox.viewCategory == "material") { return toolbox.materialsShowcaseModel } } - delegate: ToolboxDownloadsShowcaseTile {} + delegate: Loader + { + asynchronous: true + source: "ToolboxDownloadsShowcaseTile.qml" + } } } } diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml index e683f89823..145e544b19 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml @@ -64,7 +64,11 @@ ScrollView { id: materialList model: toolbox.pluginsInstalledModel - delegate: ToolboxInstalledTile {} + delegate: Loader + { + asynchronous: true + source: "ToolboxInstalledTile.qml" + } } } } @@ -101,7 +105,11 @@ ScrollView { id: pluginList model: toolbox.materialsInstalledModel - delegate: ToolboxInstalledTile {} + delegate: Loader + { + asynchronous: true + source: "ToolboxInstalledTile.qml" + } } } } From 62c53989335d4d955be4bcb1ac07df234bbb2896 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Dec 2018 14:58:28 +0100 Subject: [PATCH 15/40] Change buttons to use either secondary or primary CURA-6006 --- .../resources/qml/ToolboxInstalledPage.qml | 12 +-- .../qml/ToolboxInstalledTileActions.qml | 42 ++------- .../resources/qml/ToolboxProgressButton.qml | 93 +------------------ 3 files changed, 16 insertions(+), 131 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml index 145e544b19..e683f89823 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml @@ -64,11 +64,7 @@ ScrollView { id: materialList model: toolbox.pluginsInstalledModel - delegate: Loader - { - asynchronous: true - source: "ToolboxInstalledTile.qml" - } + delegate: ToolboxInstalledTile {} } } } @@ -105,11 +101,7 @@ ScrollView { id: pluginList model: toolbox.materialsInstalledModel - delegate: Loader - { - asynchronous: true - source: "ToolboxInstalledTile.qml" - } + delegate: ToolboxInstalledTile {} } } } diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml index 621ecd96ea..eb3a93f274 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml @@ -6,6 +6,8 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM +import Cura 1.0 as Cura + Column { property bool canUpdate: false @@ -43,44 +45,18 @@ Column visible: canUpdate } - Button + Cura.SecondaryButton { id: removeButton text: canDowngrade ? catalog.i18nc("@action:button", "Downgrade") : catalog.i18nc("@action:button", "Uninstall") visible: !model.is_bundled && model.is_installed enabled: !toolbox.isDownloading - style: ButtonStyle - { - background: Rectangle - { - implicitWidth: UM.Theme.getSize("toolbox_action_button").width - implicitHeight: UM.Theme.getSize("toolbox_action_button").height - color: "transparent" - border - { - width: UM.Theme.getSize("default_lining").width - color: - { - if (control.hovered) - { - return UM.Theme.getColor("primary_hover") - } - else - { - return UM.Theme.getColor("lining") - } - } - } - } - label: Label - { - text: control.text - color: UM.Theme.getColor("text") - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - font: UM.Theme.getFont("default") - } - } + + width: UM.Theme.getSize("toolbox_action_button").width + height: UM.Theme.getSize("toolbox_action_button").height + + fixedWidthMode: true + onClicked: toolbox.checkPackageUsageAndUninstall(model.id) Connections { diff --git a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml index 00b0b985f5..0b574e8653 100644 --- a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml +++ b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml @@ -5,7 +5,7 @@ import QtQuick 2.2 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM - +import Cura 1.0 as Cura Item { @@ -25,9 +25,12 @@ Item width: UM.Theme.getSize("toolbox_action_button").width height: UM.Theme.getSize("toolbox_action_button").height - Button + Cura.PrimaryButton { id: button + width: UM.Theme.getSize("toolbox_action_button").width + height: UM.Theme.getSize("toolbox_action_button").height + fixedWidthMode: true text: { if (complete) @@ -58,92 +61,6 @@ Item readyAction() } } - style: ButtonStyle - { - background: Rectangle - { - implicitWidth: UM.Theme.getSize("toolbox_action_button").width - implicitHeight: UM.Theme.getSize("toolbox_action_button").height - color: - { - if (base.complete) - { - return "transparent" - } - else - { - if (control.hovered) - { - return UM.Theme.getColor("primary_hover") - } - else - { - return UM.Theme.getColor("primary") - } - } - } - border - { - width: - { - if (base.complete) - { - UM.Theme.getSize("default_lining").width - } - else - { - return 0 - } - } - color: - { - if (control.hovered) - { - return UM.Theme.getColor("primary_hover") - } - else - { - return UM.Theme.getColor("lining") - } - } - } - } - label: Label - { - text: control.text - color: - { - if (base.complete) - { - return UM.Theme.getColor("text") - } - else - { - if (control.hovered) - { - return UM.Theme.getColor("button_text_hover") - } - else - { - return UM.Theme.getColor("button_text") - } - } - } - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - font: - { - if (base.complete) - { - return UM.Theme.getFont("default") - } - else - { - return UM.Theme.getFont("default_bold") - } - } - } - } } AnimatedImage From 4e2ab163ed5dffec65a7328820e494434def97e5 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Dec 2018 16:24:12 +0100 Subject: [PATCH 16/40] Add login fequired link to packages that have the login-required tag CURA-6006 --- .../qml/ToolboxDetailTileActions.qml | 22 ++++++++++++++++++- plugins/Toolbox/src/PackagesModel.py | 2 ++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml index 72a9d14dcd..8a11b402d2 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml @@ -5,11 +5,14 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM +import Cura 1.1 as Cura Column { property bool installed: toolbox.isInstalled(model.id) property bool canUpdate: toolbox.canUpdate(model.id) + property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn + width: UM.Theme.getSize("toolbox_action_button").width spacing: UM.Theme.getSize("narrow_margin").height @@ -28,11 +31,28 @@ Column onCompleteAction: toolbox.viewCategory = "installed" // Don't allow installing while another download is running - enabled: installed || !(toolbox.isDownloading && toolbox.activePackage != model) + enabled: installed || (!(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired) opacity: enabled ? 1.0 : 0.5 visible: !updateButton.visible // Don't show when the update button is visible } + + Label + { + wrapMode: Text.WordWrap + text:"Log in is required to install" + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + linkColor: UM.Theme.getColor("text_link") + visible: loginRequired + width: installButton.width + MouseArea + { + anchors.fill: parent + onClicked:Cura.API.account.login() + } + } + ToolboxProgressButton { id: updateButton diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py index f941804653..bcc02955a2 100644 --- a/plugins/Toolbox/src/PackagesModel.py +++ b/plugins/Toolbox/src/PackagesModel.py @@ -40,6 +40,7 @@ class PackagesModel(ListModel): self.addRoleName(Qt.UserRole + 19, "tags") self.addRoleName(Qt.UserRole + 20, "links") self.addRoleName(Qt.UserRole + 21, "website") + self.addRoleName(Qt.UserRole + 22, "login_required") # List of filters for queries. The result is the union of the each list of results. self._filter = {} # type: Dict[str, str] @@ -100,6 +101,7 @@ class PackagesModel(ListModel): "tags": package["tags"] if "tags" in package else [], "links": links_dict, "website": package["website"] if "website" in package else None, + "login_required": "login-required" in package.get("tags", []) }) # Filter on all the key-word arguments. From 4b8e3c32cbd46ef2f4ff63ca6a53654d2c22b0ec Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Dec 2018 16:37:47 +0100 Subject: [PATCH 17/40] Also show the login required if an update is needed CURA-6006 --- plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml index 8a11b402d2..37d9bce4c5 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml @@ -40,7 +40,7 @@ Column Label { wrapMode: Text.WordWrap - text:"Log in is required to install" + text: catalog.i18nc("@label:The string between and is the highlighted link", "Log in is required to install or update") font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") linkColor: UM.Theme.getColor("text_link") @@ -49,7 +49,7 @@ Column MouseArea { anchors.fill: parent - onClicked:Cura.API.account.login() + onClicked: Cura.API.account.login() } } @@ -68,7 +68,7 @@ Column } onActiveAction: toolbox.cancelDownload() // Don't allow installing while another download is running - enabled: !(toolbox.isDownloading && toolbox.activePackage != model) + enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired opacity: enabled ? 1.0 : 0.5 visible: canUpdate } From 717fb260c15b2364e28d2de6ab4200d2c148a697 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Dec 2018 16:58:01 +0100 Subject: [PATCH 18/40] Change toolbox tabs to controls2 CURA-6006 --- .../resources/qml/ToolboxTabButton.qml | 75 +++++++++---------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxTabButton.qml b/plugins/Toolbox/resources/qml/ToolboxTabButton.qml index b671d779f8..fa4f75d6fe 100644 --- a/plugins/Toolbox/resources/qml/ToolboxTabButton.qml +++ b/plugins/Toolbox/resources/qml/ToolboxTabButton.qml @@ -2,50 +2,49 @@ // Toolbox is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 +import QtQuick.Controls 2.0 import UM 1.1 as UM Button { + id: control property bool active: false - style: ButtonStyle + hoverEnabled: true + + background: Item { - background: Rectangle + implicitWidth: UM.Theme.getSize("toolbox_header_tab").width + implicitHeight: UM.Theme.getSize("toolbox_header_tab").height + Rectangle { - color: "transparent" - implicitWidth: UM.Theme.getSize("toolbox_header_tab").width - implicitHeight: UM.Theme.getSize("toolbox_header_tab").height - Rectangle - { - visible: control.active - color: UM.Theme.getColor("toolbox_header_highlight_hover") - anchors.bottom: parent.bottom - width: parent.width - height: UM.Theme.getSize("toolbox_header_highlight").height - } - } - label: Label - { - text: control.text - color: - { - if(control.hovered) - { - return UM.Theme.getColor("toolbox_header_button_text_hovered"); - } - if(control.active) - { - return UM.Theme.getColor("toolbox_header_button_text_active"); - } - else - { - return UM.Theme.getColor("toolbox_header_button_text_inactive"); - } - } - font: control.enabled ? (control.active ? UM.Theme.getFont("medium_bold") : UM.Theme.getFont("medium")) : UM.Theme.getFont("default_italic") - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter + visible: control.active + color: UM.Theme.getColor("primary") + anchors.bottom: parent.bottom + width: parent.width + height: UM.Theme.getSize("toolbox_header_highlight").height } } -} + contentItem: Label + { + id: label + text: control.text + color: + { + if(control.hovered) + { + return UM.Theme.getColor("toolbox_header_button_text_hovered"); + } + if(control.active) + { + return UM.Theme.getColor("toolbox_header_button_text_active"); + } + else + { + return UM.Theme.getColor("toolbox_header_button_text_inactive"); + } + } + font: control.enabled ? (control.active ? UM.Theme.getFont("medium_bold") : UM.Theme.getFont("medium")) : UM.Theme.getFont("default_italic") + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } +} \ No newline at end of file From 2c9c9d8c962607311488a9a14526fb286de543a9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Dec 2018 17:23:02 +0100 Subject: [PATCH 19/40] Handle non-happy path for downloading CURA-6006 --- plugins/Toolbox/src/Toolbox.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index ef67dc3c86..ab975548ce 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -671,6 +671,11 @@ class Toolbox(QObject, Extension): if bytes_sent == bytes_total: self.setIsDownloading(False) cast(QNetworkReply, self._download_reply).downloadProgress.disconnect(self._onDownloadProgress) + + # Check if the download was sucessfull + if self._download_reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: + Logger.log("w", "Failed to download package. The following error was returned: %s", json.loads(bytes(self._download_reply.readAll()).decode("utf-8"))) + 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 From d4a255c9e5779c8131ebb9743a5adf472530f99f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Dec 2018 17:35:58 +0100 Subject: [PATCH 20/40] Also add the login required label at the installed plugin list CURA-6006 --- .../qml/ToolboxDetailTileActions.qml | 1 - .../qml/ToolboxInstalledTileActions.qml | 21 +++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml index 37d9bce4c5..cc32ff032d 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml @@ -36,7 +36,6 @@ Column visible: !updateButton.visible // Don't show when the update button is visible } - Label { wrapMode: Text.WordWrap diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml index eb3a93f274..39528f6437 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml @@ -6,12 +6,13 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM -import Cura 1.0 as Cura +import Cura 1.1 as Cura Column { property bool canUpdate: false property bool canDowngrade: false + property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn width: UM.Theme.getSize("toolbox_action_button").width spacing: UM.Theme.getSize("narrow_margin").height @@ -40,11 +41,27 @@ Column onActiveAction: toolbox.cancelDownload() // Don't allow installing while another download is running - enabled: !(toolbox.isDownloading && toolbox.activePackage != model) + enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired opacity: enabled ? 1.0 : 0.5 visible: canUpdate } + Label + { + wrapMode: Text.WordWrap + text: catalog.i18nc("@label:The string between and is the highlighted link", "Log in is required to update") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + linkColor: UM.Theme.getColor("text_link") + visible: loginRequired + width: updateButton.width + MouseArea + { + anchors.fill: parent + onClicked: Cura.API.account.login() + } + } + Cura.SecondaryButton { id: removeButton From eddf4e7f3d96296576e8124fcfd3a86e01adf7e3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 6 Dec 2018 19:38:12 +0100 Subject: [PATCH 21/40] Simplify QML --- .../resources/qml/ToolboxInstalledPage.qml | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml index e683f89823..738cdde323 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml @@ -21,44 +21,39 @@ ScrollView Column { spacing: UM.Theme.getSize("default_margin").height + visible: toolbox.pluginsInstalledModel.items.length > 0 + height: childrenRect.height + 4 * UM.Theme.getSize("default_margin").height + anchors { right: parent.right left: parent.left - leftMargin: UM.Theme.getSize("wide_margin").width - topMargin: UM.Theme.getSize("wide_margin").height - bottomMargin: UM.Theme.getSize("wide_margin").height + margins: UM.Theme.getSize("default_margin").width top: parent.top } - height: childrenRect.height + 4 * UM.Theme.getSize("default_margin").height + Label { - visible: toolbox.pluginsInstalledModel.items.length > 0 - width: parent.width + width: page.width text: catalog.i18nc("@title:tab", "Plugins") color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("medium") } Rectangle { - visible: toolbox.pluginsInstalledModel.items.length > 0 color: "transparent" width: parent.width - height: childrenRect.height + 1 * UM.Theme.getSize("default_lining").width + height: childrenRect.height + UM.Theme.getSize("default_lining").width border.color: UM.Theme.getColor("lining") border.width: UM.Theme.getSize("default_lining").width Column { - height: childrenRect.height anchors { top: parent.top right: parent.right left: parent.left - leftMargin: UM.Theme.getSize("default_margin").width - rightMargin: UM.Theme.getSize("default_margin").width - topMargin: UM.Theme.getSize("default_lining").width - bottomMargin: UM.Theme.getSize("default_lining").width + margins: UM.Theme.getSize("default_margin").width } Repeater { @@ -70,32 +65,26 @@ ScrollView } Label { - visible: toolbox.materialsInstalledModel.items.length > 0 - width: page.width text: catalog.i18nc("@title:tab", "Materials") color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("medium") } + Rectangle { - visible: toolbox.materialsInstalledModel.items.length > 0 color: "transparent" width: parent.width - height: childrenRect.height + 1 * UM.Theme.getSize("default_lining").width + height: childrenRect.height + UM.Theme.getSize("default_lining").width border.color: UM.Theme.getColor("lining") border.width: UM.Theme.getSize("default_lining").width Column { - height: Math.max( UM.Theme.getSize("wide_margin").height, childrenRect.height) anchors { top: parent.top right: parent.right left: parent.left - leftMargin: UM.Theme.getSize("default_margin").width - rightMargin: UM.Theme.getSize("default_margin").width - topMargin: UM.Theme.getSize("default_lining").width - bottomMargin: UM.Theme.getSize("default_lining").width + margins: UM.Theme.getSize("default_margin").width } Repeater { From b33ce7a50f891165fe71e964815804e2a9427f6a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 7 Dec 2018 09:35:08 +0100 Subject: [PATCH 22/40] Fix alignment of download grid tile CURA-6006 --- plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml index 887140bbfa..d6eedbcde5 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml @@ -63,6 +63,8 @@ Item { width: parent.width - thumbnail.width - parent.spacing spacing: Math.floor(UM.Theme.getSize("narrow_margin").width) + anchors.top: parent.top + anchors.topMargin: UM.Theme.getSize("default_margin").height Label { id: name From 2a0954f24683066f28698b03b261721fc02847b0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 7 Dec 2018 09:35:53 +0100 Subject: [PATCH 23/40] Gracefully handle the conectionError when logging in --- cura/OAuth2/AuthorizationHelpers.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py index f75ad9c9f9..762d0db069 100644 --- a/cura/OAuth2/AuthorizationHelpers.py +++ b/cura/OAuth2/AuthorizationHelpers.py @@ -81,9 +81,14 @@ class AuthorizationHelpers: # \param access_token: The encoded JWT token. # \return: Dict containing some profile data. def parseJWT(self, access_token: str) -> Optional["UserProfile"]: - token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = { - "Authorization": "Bearer {}".format(access_token) - }) + try: + token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = { + "Authorization": "Bearer {}".format(access_token) + }) + except ConnectionError: + # Connection was suddenly dropped. Nothing we can do about that. + Logger.logException("e", "Something failed while attempting to parse the JWT token") + return None if token_request.status_code not in (200, 201): Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text) return None From 9eb09132d66d989120d7002b212bbcd4f5816fa3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 7 Dec 2018 09:38:02 +0100 Subject: [PATCH 24/40] Fix minor layout issue installed page CURA-6006 --- plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml index 738cdde323..3d5cd1c8d4 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml @@ -43,7 +43,7 @@ ScrollView { color: "transparent" width: parent.width - height: childrenRect.height + UM.Theme.getSize("default_lining").width + height: childrenRect.height + UM.Theme.getSize("default_margin").width border.color: UM.Theme.getColor("lining") border.width: UM.Theme.getSize("default_lining").width Column @@ -74,7 +74,7 @@ ScrollView { color: "transparent" width: parent.width - height: childrenRect.height + UM.Theme.getSize("default_lining").width + height: childrenRect.height + UM.Theme.getSize("default_margin").width border.color: UM.Theme.getColor("lining") border.width: UM.Theme.getSize("default_lining").width Column From eaf413997d0c5ca4b317a0d65b5aef5c44131627 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 7 Dec 2018 09:44:09 +0100 Subject: [PATCH 25/40] Changed the installed button to be a secondary button CURA-6006 --- .../qml/ToolboxDetailTileActions.qml | 40 ++++++++++++------- .../resources/qml/ToolboxProgressButton.qml | 1 + 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml index cc32ff032d..848acfbf4f 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml @@ -16,24 +16,36 @@ Column width: UM.Theme.getSize("toolbox_action_button").width spacing: UM.Theme.getSize("narrow_margin").height - ToolboxProgressButton + Item { - id: installButton - active: toolbox.isDownloading && toolbox.activePackage == model - complete: installed - onReadyAction: + width: installButton.width + height: installButton.height + ToolboxProgressButton { - toolbox.activePackage = model - toolbox.startDownload(model.download_url) + id: installButton + active: toolbox.isDownloading && toolbox.activePackage == model + onReadyAction: + { + toolbox.activePackage = model + toolbox.startDownload(model.download_url) + } + onActiveAction: toolbox.cancelDownload() + + // Don't allow installing while another download is running + enabled: installed || (!(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired) + opacity: enabled ? 1.0 : 0.5 + visible: !updateButton.visible && !installed// Don't show when the update button is visible } - onActiveAction: toolbox.cancelDownload() - onCompleteAction: toolbox.viewCategory = "installed" - - // Don't allow installing while another download is running - enabled: installed || (!(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired) - opacity: enabled ? 1.0 : 0.5 - visible: !updateButton.visible // Don't show when the update button is visible + Cura.SecondaryButton + { + visible: installed + onClicked: toolbox.viewCategory = "installed" + text: catalog.i18nc("@action:button", "Installed") + fixedWidthMode: true + width: installButton.width + height: installButton.height + } } Label diff --git a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml index 0b574e8653..3ca18a52ed 100644 --- a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml +++ b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml @@ -7,6 +7,7 @@ import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM import Cura 1.0 as Cura +// TODO; This is in quite some need for refactoring. Item { id: base From 1487af167b616a190e320e6d05c94fad3a3ff95b Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 10 Dec 2018 13:20:36 +0100 Subject: [PATCH 26/40] Disable async loading for ToolboxDetailTile CURA-6006 --- plugins/Toolbox/resources/qml/ToolboxDetailList.qml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailList.qml b/plugins/Toolbox/resources/qml/ToolboxDetailList.qml index 1700a58ebe..4e44ea7d0b 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailList.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailList.qml @@ -32,7 +32,11 @@ Item model: toolbox.packagesModel delegate: Loader { - asynchronous: true + // FIXME: When using asynchronous loading, on Mac and Windows, the tile may fail to load complete, + // leaving an empty space below the title part. We turn it off for now to make it work on Mac and + // Windows. + // Can be related to this QT bug: https://bugreports.qt.io/browse/QTBUG-50992 + asynchronous: false source: "ToolboxDetailTile.qml" } } From b745755a7d1a0b7b482add455cb20535bc265247 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 10 Dec 2018 13:22:28 +0100 Subject: [PATCH 27/40] Remove TODO in ToolboxProgressButton CURA-6006 This file has been refactored. --- plugins/Toolbox/resources/qml/ToolboxProgressButton.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml index 3ca18a52ed..933e3a5900 100644 --- a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml +++ b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml @@ -7,7 +7,7 @@ import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM import Cura 1.0 as Cura -// TODO; This is in quite some need for refactoring. + Item { id: base From c62cb84c75c158a21ea703fd853fb5f68f3d2789 Mon Sep 17 00:00:00 2001 From: Aleksei S Date: Mon, 10 Dec 2018 14:18:10 +0100 Subject: [PATCH 28/40] Added CuraDirve plugin to Cura build CURA-6005 --- .gitignore | 1 - plugins/CuraDrive/__init__.py | 14 ++ plugins/CuraDrive/plugin.json | 8 + plugins/CuraDrive/src/DriveApiService.py | 185 ++++++++++++++++ plugins/CuraDrive/src/DrivePluginExtension.py | 200 ++++++++++++++++++ plugins/CuraDrive/src/Settings.py | 36 ++++ plugins/CuraDrive/src/UploadBackupJob.py | 39 ++++ plugins/CuraDrive/src/__init__.py | 0 .../CuraDrive/src/models/BackupListModel.py | 38 ++++ plugins/CuraDrive/src/models/__init__.py | 0 .../src/qml/components/ActionButton.qml | 67 ++++++ .../src/qml/components/ActionCheckBox.qml | 49 +++++ .../src/qml/components/ActionToolTip.qml | 29 +++ .../src/qml/components/BackupList.qml | 31 +++ .../src/qml/components/BackupListFooter.qml | 42 ++++ .../src/qml/components/BackupListItem.qml | 112 ++++++++++ .../qml/components/BackupListItemDetails.qml | 61 ++++++ .../components/BackupListItemDetailsRow.qml | 52 +++++ .../CuraDrive/src/qml/components/Divider.qml | 11 + plugins/CuraDrive/src/qml/components/Icon.qml | 56 +++++ .../src/qml/components/RightSideScrollBar.qml | 13 ++ .../src/qml/images/avatar_default.png | Bin 0 -> 3115 bytes .../CuraDrive/src/qml/images/background.svg | 12 ++ plugins/CuraDrive/src/qml/images/backup.svg | 3 + plugins/CuraDrive/src/qml/images/cura.svg | 7 + .../CuraDrive/src/qml/images/cura_logo.jpg | Bin 0 -> 19308 bytes .../CuraDrive/src/qml/images/cura_logo.png | Bin 0 -> 13258 bytes plugins/CuraDrive/src/qml/images/delete.svg | 7 + plugins/CuraDrive/src/qml/images/folder.svg | 7 + plugins/CuraDrive/src/qml/images/home.svg | 3 + plugins/CuraDrive/src/qml/images/icon.png | Bin 0 -> 21924 bytes plugins/CuraDrive/src/qml/images/info.svg | 4 + .../src/qml/images/inverted_circle.png | Bin 0 -> 1608 bytes plugins/CuraDrive/src/qml/images/loading.gif | Bin 0 -> 6762 bytes plugins/CuraDrive/src/qml/images/material.svg | 7 + plugins/CuraDrive/src/qml/images/plugin.svg | 7 + .../src/qml/images/preview_banner.png | Bin 0 -> 8324 bytes plugins/CuraDrive/src/qml/images/printer.svg | 14 ++ plugins/CuraDrive/src/qml/images/profile.svg | 3 + plugins/CuraDrive/src/qml/images/restore.svg | 7 + plugins/CuraDrive/src/qml/main.qml | 42 ++++ .../CuraDrive/src/qml/pages/BackupsPage.qml | 73 +++++++ .../CuraDrive/src/qml/pages/WelcomePage.qml | 48 +++++ 43 files changed, 1287 insertions(+), 1 deletion(-) create mode 100644 plugins/CuraDrive/__init__.py create mode 100644 plugins/CuraDrive/plugin.json create mode 100644 plugins/CuraDrive/src/DriveApiService.py create mode 100644 plugins/CuraDrive/src/DrivePluginExtension.py create mode 100644 plugins/CuraDrive/src/Settings.py create mode 100644 plugins/CuraDrive/src/UploadBackupJob.py create mode 100644 plugins/CuraDrive/src/__init__.py create mode 100644 plugins/CuraDrive/src/models/BackupListModel.py create mode 100644 plugins/CuraDrive/src/models/__init__.py create mode 100644 plugins/CuraDrive/src/qml/components/ActionButton.qml create mode 100644 plugins/CuraDrive/src/qml/components/ActionCheckBox.qml create mode 100644 plugins/CuraDrive/src/qml/components/ActionToolTip.qml create mode 100644 plugins/CuraDrive/src/qml/components/BackupList.qml create mode 100644 plugins/CuraDrive/src/qml/components/BackupListFooter.qml create mode 100644 plugins/CuraDrive/src/qml/components/BackupListItem.qml create mode 100644 plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml create mode 100644 plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml create mode 100644 plugins/CuraDrive/src/qml/components/Divider.qml create mode 100644 plugins/CuraDrive/src/qml/components/Icon.qml create mode 100644 plugins/CuraDrive/src/qml/components/RightSideScrollBar.qml create mode 100644 plugins/CuraDrive/src/qml/images/avatar_default.png create mode 100644 plugins/CuraDrive/src/qml/images/background.svg create mode 100644 plugins/CuraDrive/src/qml/images/backup.svg create mode 100644 plugins/CuraDrive/src/qml/images/cura.svg create mode 100644 plugins/CuraDrive/src/qml/images/cura_logo.jpg create mode 100644 plugins/CuraDrive/src/qml/images/cura_logo.png create mode 100644 plugins/CuraDrive/src/qml/images/delete.svg create mode 100644 plugins/CuraDrive/src/qml/images/folder.svg create mode 100644 plugins/CuraDrive/src/qml/images/home.svg create mode 100644 plugins/CuraDrive/src/qml/images/icon.png create mode 100644 plugins/CuraDrive/src/qml/images/info.svg create mode 100644 plugins/CuraDrive/src/qml/images/inverted_circle.png create mode 100644 plugins/CuraDrive/src/qml/images/loading.gif create mode 100644 plugins/CuraDrive/src/qml/images/material.svg create mode 100644 plugins/CuraDrive/src/qml/images/plugin.svg create mode 100644 plugins/CuraDrive/src/qml/images/preview_banner.png create mode 100644 plugins/CuraDrive/src/qml/images/printer.svg create mode 100644 plugins/CuraDrive/src/qml/images/profile.svg create mode 100644 plugins/CuraDrive/src/qml/images/restore.svg create mode 100644 plugins/CuraDrive/src/qml/main.qml create mode 100644 plugins/CuraDrive/src/qml/pages/BackupsPage.qml create mode 100644 plugins/CuraDrive/src/qml/pages/WelcomePage.qml diff --git a/.gitignore b/.gitignore index 0a66b6eb33..60b59e6829 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,6 @@ plugins/cura-siemensnx-plugin plugins/CuraBlenderPlugin plugins/CuraCloudPlugin plugins/CuraDrivePlugin -plugins/CuraDrive plugins/CuraLiveScriptingPlugin plugins/CuraOpenSCADPlugin plugins/CuraPrintProfileCreator diff --git a/plugins/CuraDrive/__init__.py b/plugins/CuraDrive/__init__.py new file mode 100644 index 0000000000..6612a5d614 --- /dev/null +++ b/plugins/CuraDrive/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2017 Ultimaker B.V. +import os + +is_testing = os.getenv('ENV_NAME', "development") == "testing" + +# Only load the whole plugin when not running tests as __init__.py is automatically loaded by PyTest +if not is_testing: + from .src.DrivePluginExtension import DrivePluginExtension + + def getMetaData(): + return {} + + def register(app): + return {"extension": DrivePluginExtension(app)} diff --git a/plugins/CuraDrive/plugin.json b/plugins/CuraDrive/plugin.json new file mode 100644 index 0000000000..134cd31a77 --- /dev/null +++ b/plugins/CuraDrive/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "Cura Backups", + "author": "Ultimaker B.V.", + "description": "Backup and restore your configuration.", + "version": "1.2.1", + "api": 5, + "i18n-catalog": "cura_drive" +} \ No newline at end of file diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py new file mode 100644 index 0000000000..a677466838 --- /dev/null +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -0,0 +1,185 @@ +# Copyright (c) 2017 Ultimaker B.V. +import base64 +import hashlib +from datetime import datetime +from tempfile import NamedTemporaryFile +from typing import Optional, List, Dict + +import requests + +from UM.Logger import Logger +from UM.Message import Message +from UM.Signal import Signal + +from .UploadBackupJob import UploadBackupJob +from .Settings import Settings + + +class DriveApiService: + """ + The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling. + """ + + GET_BACKUPS_URL = "{}/backups".format(Settings.DRIVE_API_URL) + PUT_BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL) + DELETE_BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL) + + # Emit signal when restoring backup started or finished. + onRestoringStateChanged = Signal() + + # Emit signal when creating backup started or finished. + onCreatingStateChanged = Signal() + + def __init__(self, cura_api) -> None: + """Create a new instance of the Drive API service and set the cura_api object.""" + self._cura_api = cura_api + + def getBackups(self) -> List[Dict[str, any]]: + """Get all backups from the API.""" + access_token = self._cura_api.account.accessToken + if not access_token: + Logger.log("w", "Could not get access token.") + return [] + + backup_list_request = requests.get(self.GET_BACKUPS_URL, headers={ + "Authorization": "Bearer {}".format(access_token) + }) + if backup_list_request.status_code > 299: + Logger.log("w", "Could not get backups list from remote: %s", backup_list_request.text) + Message(Settings.translatable_messages["get_backups_error"], title = Settings.MESSAGE_TITLE, + lifetime = 10).show() + return [] + return backup_list_request.json()["data"] + + def createBackup(self) -> None: + """Create a backup and upload it to CuraDrive cloud storage.""" + self.onCreatingStateChanged.emit(is_creating=True) + + # Create the backup. + backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup() + if not backup_zip_file or not backup_meta_data: + self.onCreatingStateChanged.emit(is_creating=False, error_message="Could not create backup.") + return + + # Create an upload entry for the backup. + timestamp = datetime.now().isoformat() + backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"]) + backup_upload_url = self._requestBackupUpload(backup_meta_data, len(backup_zip_file)) + if not backup_upload_url: + self.onCreatingStateChanged.emit(is_creating=False, error_message="Could not upload backup.") + return + + # Upload the backup to storage. + upload_backup_job = UploadBackupJob(backup_upload_url, backup_zip_file) + upload_backup_job.finished.connect(self._onUploadFinished) + upload_backup_job.start() + + def _onUploadFinished(self, job: "UploadBackupJob") -> None: + """ + Callback handler for the upload job. + :param job: The executed job. + """ + if job.backup_upload_error_message != "": + # If the job contains an error message we pass it along so the UI can display it. + self.onCreatingStateChanged.emit(is_creating=False, error_message=job.backup_upload_error_message) + else: + self.onCreatingStateChanged.emit(is_creating=False) + + def restoreBackup(self, backup: Dict[str, any]) -> None: + """ + Restore a previously exported backup from cloud storage. + :param backup: A dict containing an entry from the API list response. + """ + self.onRestoringStateChanged.emit(is_restoring=True) + download_url = backup.get("download_url") + if not download_url: + # If there is no download URL, we can't restore the backup. + return self._emitRestoreError() + + download_package = requests.get(download_url, stream=True) + if download_package.status_code != 200: + # Something went wrong when attempting to download the backup. + Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text) + return self._emitRestoreError() + + # We store the file in a temporary path fist to ensure integrity. + temporary_backup_file = NamedTemporaryFile(delete=False) + with open(temporary_backup_file.name, "wb") as write_backup: + for chunk in download_package: + write_backup.write(chunk) + + if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash")): + # Don't restore the backup if the MD5 hashes do not match. + # This can happen if the download was interrupted. + Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.") + return self._emitRestoreError() + + # Tell Cura to place the backup back in the user data folder. + with open(temporary_backup_file.name, "rb") as read_backup: + self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("data")) + self.onRestoringStateChanged.emit(is_restoring=False) + + def _emitRestoreError(self, error_message: str = Settings.translatable_messages["backup_restore_error_message"]): + """Helper method for emitting a signal when restoring failed.""" + self.onRestoringStateChanged.emit( + is_restoring=False, + error_message=error_message + ) + + @staticmethod + def _verifyMd5Hash(file_path: str, known_hash: str) -> bool: + """ + Verify the MD5 hash of a file. + :param file_path: Full path to the file. + :param known_hash: The known MD5 hash of the file. + :return: Success or not. + """ + with open(file_path, "rb") as read_backup: + local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars=b"_-").decode("utf-8") + return known_hash == local_md5_hash + + def deleteBackup(self, backup_id: str) -> bool: + """ + Delete a backup from the server by ID. + :param backup_id: The ID of the backup to delete. + :return: Success bool. + """ + access_token = self._cura_api.account.accessToken + if not access_token: + Logger.log("w", "Could not get access token.") + return False + + delete_backup = requests.delete("{}/{}".format(self.DELETE_BACKUP_URL, backup_id), headers = { + "Authorization": "Bearer {}".format(access_token) + }) + if delete_backup.status_code > 299: + Logger.log("w", "Could not delete backup: %s", delete_backup.text) + return False + return True + + def _requestBackupUpload(self, backup_metadata: Dict[str, any], backup_size: int) -> Optional[str]: + """ + Request a backup upload slot from the API. + :param backup_metadata: A dict containing some meta data about the backup. + :param backup_size: The size of the backup file in bytes. + :return: The upload URL for the actual backup file if successful, otherwise None. + """ + access_token = self._cura_api.account.accessToken + if not access_token: + Logger.log("w", "Could not get access token.") + return None + + backup_upload_request = requests.put(self.PUT_BACKUP_URL, json={ + "data": { + "backup_size": backup_size, + "metadata": backup_metadata + } + }, headers={ + "Authorization": "Bearer {}".format(access_token) + }) + + if backup_upload_request.status_code > 299: + Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text) + return None + + return backup_upload_request.json()["data"]["upload_url"] diff --git a/plugins/CuraDrive/src/DrivePluginExtension.py b/plugins/CuraDrive/src/DrivePluginExtension.py new file mode 100644 index 0000000000..556fb187df --- /dev/null +++ b/plugins/CuraDrive/src/DrivePluginExtension.py @@ -0,0 +1,200 @@ +# Copyright (c) 2017 Ultimaker B.V. +import os +from datetime import datetime +from typing import Optional + +from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal + +from UM.Extension import Extension +from UM.Message import Message + +from .Settings import Settings +from .DriveApiService import DriveApiService +from .models.BackupListModel import BackupListModel + + +class DrivePluginExtension(QObject, Extension): + """ + The DivePluginExtension provides functionality to backup and restore your Cura configuration to Ultimaker's cloud. + """ + + # Signal emitted when the list of backups changed. + backupsChanged = pyqtSignal() + + # Signal emitted when restoring has started. Needed to prevent parallel restoring. + restoringStateChanged = pyqtSignal() + + # Signal emitted when creating has started. Needed to prevent parallel creation of backups. + creatingStateChanged = pyqtSignal() + + # Signal emitted when preferences changed (like auto-backup). + preferencesChanged = pyqtSignal() + + DATE_FORMAT = "%d/%m/%Y %H:%M:%S" + + def __init__(self, application): + super(DrivePluginExtension, self).__init__() + + # Re-usable instance of application. + self._application = application + + # Local data caching for the UI. + self._drive_window = None # type: Optional[QObject] + self._backups_list_model = BackupListModel() + self._is_restoring_backup = False + self._is_creating_backup = False + + # Initialize services. + self._preferences = self._application.getPreferences() + self._cura_api = self._application.getCuraAPI() + self._drive_api_service = DriveApiService(self._cura_api) + + # Attach signals. + self._cura_api.account.loginStateChanged.connect(self._onLoginStateChanged) + self._drive_api_service.onRestoringStateChanged.connect(self._onRestoringStateChanged) + self._drive_api_service.onCreatingStateChanged.connect(self._onCreatingStateChanged) + + # Register preferences. + self._preferences.addPreference(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, False) + self._preferences.addPreference(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, datetime.now() + .strftime(self.DATE_FORMAT)) + + # Register menu items. + self._updateMenuItems() + + # Make auto-backup on boot if required. + self._application.engineCreatedSignal.connect(self._autoBackup) + + def showDriveWindow(self) -> None: + """Show the Drive UI popup window.""" + if not self._drive_window: + self._drive_window = self.createDriveWindow() + self.refreshBackups() + self._drive_window.show() + + def createDriveWindow(self) -> Optional["QObject"]: + """ + Create an instance of the Drive UI popup window. + :return: The popup window object. + """ + path = os.path.join(os.path.dirname(__file__), "qml", "main.qml") + return self._application.createQmlComponent(path, {"CuraDrive": self}) + + def _updateMenuItems(self) -> None: + """Update the menu items.""" + self.addMenuItem(Settings.translatable_messages["extension_menu_entry"], self.showDriveWindow) + + def _autoBackup(self) -> None: + """Automatically make a backup on boot if enabled.""" + if self._preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._lastBackupTooLongAgo(): + self.createBackup() + + def _lastBackupTooLongAgo(self) -> bool: + """Check if the last backup was longer than 1 day ago.""" + current_date = datetime.now() + last_backup_date = self._getLastBackupDate() + date_diff = current_date - last_backup_date + return date_diff.days > 1 + + def _getLastBackupDate(self) -> "datetime": + """Get the last backup date as datetime object.""" + last_backup_date = self._preferences.getValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY) + return datetime.strptime(last_backup_date, self.DATE_FORMAT) + + def _storeBackupDate(self) -> None: + """Store the current date as last backup date.""" + backup_date = datetime.now().strftime(self.DATE_FORMAT) + self._preferences.setValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, backup_date) + + def _onLoginStateChanged(self, logged_in: bool = False) -> None: + """Callback handler for changes in the login state.""" + if logged_in: + self.refreshBackups() + + def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: str = None) -> None: + """Callback handler for changes in the restoring state.""" + self._is_restoring_backup = is_restoring + self.restoringStateChanged.emit() + if error_message: + Message(error_message, title = Settings.MESSAGE_TITLE, lifetime = 5).show() + + def _onCreatingStateChanged(self, is_creating: bool = False, error_message: str = None) -> None: + """Callback handler for changes in the creation state.""" + self._is_creating_backup = is_creating + self.creatingStateChanged.emit() + if error_message: + Message(error_message, title = Settings.MESSAGE_TITLE, lifetime = 5).show() + else: + self._storeBackupDate() + if not is_creating: + # We've finished creating a new backup, to the list has to be updated. + self.refreshBackups() + + @pyqtSlot(bool, name = "toggleAutoBackup") + def toggleAutoBackup(self, enabled: bool) -> None: + """Enable or disable the auto-backup feature.""" + self._preferences.setValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, enabled) + self.preferencesChanged.emit() + + @pyqtProperty(bool, notify = preferencesChanged) + def autoBackupEnabled(self) -> bool: + """Check if auto-backup is enabled or not.""" + return bool(self._preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY)) + + @pyqtProperty(QObject, notify = backupsChanged) + def backups(self) -> BackupListModel: + """ + Get a list of the backups. + :return: The backups as Qt List Model. + """ + return self._backups_list_model + + @pyqtSlot(name = "refreshBackups") + def refreshBackups(self) -> None: + """ + Forcefully refresh the backups list. + """ + self._backups_list_model.loadBackups(self._drive_api_service.getBackups()) + self.backupsChanged.emit() + + @pyqtProperty(bool, notify = restoringStateChanged) + def isRestoringBackup(self) -> bool: + """ + Get the current restoring state. + :return: Boolean if we are restoring or not. + """ + return self._is_restoring_backup + + @pyqtProperty(bool, notify = creatingStateChanged) + def isCreatingBackup(self) -> bool: + """ + Get the current creating state. + :return: Boolean if we are creating or not. + """ + return self._is_creating_backup + + @pyqtSlot(str, name = "restoreBackup") + def restoreBackup(self, backup_id: str) -> None: + """ + Download and restore a backup by ID. + :param backup_id: The ID of the backup. + """ + index = self._backups_list_model.find("backup_id", backup_id) + backup = self._backups_list_model.getItem(index) + self._drive_api_service.restoreBackup(backup) + + @pyqtSlot(name = "createBackup") + def createBackup(self) -> None: + """ + Create a new backup. + """ + self._drive_api_service.createBackup() + + @pyqtSlot(str, name = "deleteBackup") + def deleteBackup(self, backup_id: str) -> None: + """ + Delete a backup by ID. + :param backup_id: The ID of the backup. + """ + self._drive_api_service.deleteBackup(backup_id) + self.refreshBackups() diff --git a/plugins/CuraDrive/src/Settings.py b/plugins/CuraDrive/src/Settings.py new file mode 100644 index 0000000000..277a976cc7 --- /dev/null +++ b/plugins/CuraDrive/src/Settings.py @@ -0,0 +1,36 @@ +# Copyright (c) 2018 Ultimaker B.V. +from UM import i18nCatalog + + +class Settings: + """ + Keeps the application settings. + """ + UM_CLOUD_API_ROOT = "https://api.ultimaker.com" + DRIVE_API_VERSION = 1 + DRIVE_API_URL = "{}/cura-drive/v{}".format(UM_CLOUD_API_ROOT, str(DRIVE_API_VERSION)) + + AUTO_BACKUP_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled" + AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date" + + I18N_CATALOG_ID = "cura_drive" + I18N_CATALOG = i18nCatalog(I18N_CATALOG_ID) + + MESSAGE_TITLE = I18N_CATALOG.i18nc("@info:title", "Backups"), + + # Translatable messages for the entire plugin. + translatable_messages = { + + # Menu items. + "extension_menu_entry": I18N_CATALOG.i18nc("@item:inmenu", "Manage backups"), + + # Notification messages. + "backup_failed": I18N_CATALOG.i18nc("@info:backup_status", "There was an error while creating your backup."), + "uploading_backup": I18N_CATALOG.i18nc("@info:backup_status", "Uploading your backup..."), + "uploading_backup_success": I18N_CATALOG.i18nc("@info:backup_status", "Your backup has finished uploading."), + "uploading_backup_error": I18N_CATALOG.i18nc("@info:backup_status", + "There was an error while uploading your backup."), + "get_backups_error": I18N_CATALOG.i18nc("@info:backup_status", "There was an error listing your backups."), + "backup_restore_error_message": I18N_CATALOG.i18nc("@info:backup_status", + "There was an error trying to restore your backup.") + } diff --git a/plugins/CuraDrive/src/UploadBackupJob.py b/plugins/CuraDrive/src/UploadBackupJob.py new file mode 100644 index 0000000000..039e6d1a09 --- /dev/null +++ b/plugins/CuraDrive/src/UploadBackupJob.py @@ -0,0 +1,39 @@ +# Copyright (c) 2018 Ultimaker B.V. +import requests + +from UM.Job import Job +from UM.Logger import Logger +from UM.Message import Message + +from .Settings import Settings + + +class UploadBackupJob(Job): + """ + This job is responsible for uploading the backup file to cloud storage. + As it can take longer than some other tasks, we schedule this using a Cura Job. + """ + + def __init__(self, signed_upload_url: str, backup_zip: bytes): + super().__init__() + self._signed_upload_url = signed_upload_url + self._backup_zip = backup_zip + self._upload_success = False + self.backup_upload_error_message = "" + + def run(self): + Message(Settings.translatable_messages["uploading_backup"], title = Settings.MESSAGE_TITLE, + lifetime = 10).show() + + backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip) + if backup_upload.status_code not in (200, 201): + self.backup_upload_error_message = backup_upload.text + Logger.log("w", "Could not upload backup file: %s", backup_upload.text) + Message(Settings.translatable_messages["uploading_backup_error"], title = Settings.MESSAGE_TITLE, + lifetime = 10).show() + else: + self._upload_success = True + Message(Settings.translatable_messages["uploading_backup_success"], title = Settings.MESSAGE_TITLE, + lifetime = 10).show() + + self.finished.emit(self) diff --git a/plugins/CuraDrive/src/__init__.py b/plugins/CuraDrive/src/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/CuraDrive/src/models/BackupListModel.py b/plugins/CuraDrive/src/models/BackupListModel.py new file mode 100644 index 0000000000..9567b3d255 --- /dev/null +++ b/plugins/CuraDrive/src/models/BackupListModel.py @@ -0,0 +1,38 @@ +# Copyright (c) 2018 Ultimaker B.V. +from typing import List, Dict + +from UM.Qt.ListModel import ListModel + +from PyQt5.QtCore import Qt + + +class BackupListModel(ListModel): + """ + The BackupListModel transforms the backups data that came from the server so it can be served to the Qt UI. + """ + + def __init__(self, parent=None): + super().__init__(parent) + self.addRoleName(Qt.UserRole + 1, "backup_id") + self.addRoleName(Qt.UserRole + 2, "download_url") + self.addRoleName(Qt.UserRole + 3, "generated_time") + self.addRoleName(Qt.UserRole + 4, "md5_hash") + self.addRoleName(Qt.UserRole + 5, "data") + + def loadBackups(self, data: List[Dict[str, any]]) -> None: + """ + Populate the model with server data. + :param data: + """ + items = [] + for backup in data: + # We do this loop because we only want to append these specific fields. + # Without this, ListModel will break. + items.append({ + "backup_id": backup["backup_id"], + "download_url": backup["download_url"], + "generated_time": backup["generated_time"], + "md5_hash": backup["md5_hash"], + "data": backup["metadata"] + }) + self.setItems(items) diff --git a/plugins/CuraDrive/src/models/__init__.py b/plugins/CuraDrive/src/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/CuraDrive/src/qml/components/ActionButton.qml b/plugins/CuraDrive/src/qml/components/ActionButton.qml new file mode 100644 index 0000000000..843079ed88 --- /dev/null +++ b/plugins/CuraDrive/src/qml/components/ActionButton.qml @@ -0,0 +1,67 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.3 + +import UM 1.1 as UM + +Button +{ + id: button + property alias cursorShape: mouseArea.cursorShape + property var iconSource: "" + property var busy: false + property var color: UM.Theme.getColor("primary") + property var hoverColor: UM.Theme.getColor("primary_hover") + property var disabledColor: color + property var textColor: UM.Theme.getColor("button_text") + property var textHoverColor: UM.Theme.getColor("button_text_hover") + property var textDisabledColor: textColor + property var textFont: UM.Theme.getFont("action_button") + + contentItem: RowLayout + { + Icon + { + id: buttonIcon + iconSource: button.iconSource + width: 16 * screenScaleFactor + color: button.hovered ? button.textHoverColor : button.textColor + visible: button.iconSource != "" && !loader.visible + } + + Icon + { + id: loader + iconSource: "../images/loading.gif" + width: 16 * screenScaleFactor + color: button.hovered ? button.textHoverColor : button.textColor + visible: button.busy + animated: true + } + + Label + { + id: buttonText + text: button.text + color: button.enabled ? (button.hovered ? button.textHoverColor : button.textColor): button.textDisabledColor + font: button.textFont + visible: button.text != "" + renderType: Text.NativeRendering + } + } + + background: Rectangle + { + color: button.enabled ? (button.hovered ? button.hoverColor : button.color) : button.disabledColor + } + + MouseArea + { + id: mouseArea + anchors.fill: parent + onPressed: mouse.accepted = false + hoverEnabled: true + cursorShape: button.enabled ? (hovered ? Qt.PointingHandCursor : Qt.ArrowCursor) : Qt.ForbiddenCursor + } +} diff --git a/plugins/CuraDrive/src/qml/components/ActionCheckBox.qml b/plugins/CuraDrive/src/qml/components/ActionCheckBox.qml new file mode 100644 index 0000000000..71f5e6035d --- /dev/null +++ b/plugins/CuraDrive/src/qml/components/ActionCheckBox.qml @@ -0,0 +1,49 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.3 + +import UM 1.3 as UM + +CheckBox +{ + id: checkbox + hoverEnabled: true + + property var label: "" + + indicator: Rectangle { + implicitWidth: 30 * screenScaleFactor + implicitHeight: 30 * screenScaleFactor + x: 0 + y: Math.round(parent.height / 2 - height / 2) + color: UM.Theme.getColor("sidebar") + border.color: UM.Theme.getColor("text") + + Rectangle { + width: 14 * screenScaleFactor + height: 14 * screenScaleFactor + x: 8 * screenScaleFactor + y: 8 * screenScaleFactor + color: UM.Theme.getColor("primary") + visible: checkbox.checked + } + } + + contentItem: Label { + anchors + { + left: checkbox.indicator.right + leftMargin: 5 * screenScaleFactor + } + text: catalog.i18nc("@checkbox:description", "Auto Backup") + color: UM.Theme.getColor("text") + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + } + + ActionToolTip + { + text: checkbox.label + } +} diff --git a/plugins/CuraDrive/src/qml/components/ActionToolTip.qml b/plugins/CuraDrive/src/qml/components/ActionToolTip.qml new file mode 100644 index 0000000000..93b92bc2df --- /dev/null +++ b/plugins/CuraDrive/src/qml/components/ActionToolTip.qml @@ -0,0 +1,29 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.3 + +import UM 1.1 as UM + +ToolTip +{ + id: tooltip + visible: parent.hovered + opacity: 0.9 + delay: 500 + + background: Rectangle + { + color: UM.Theme.getColor("sidebar") + border.color: UM.Theme.getColor("primary") + border.width: 1 * screenScaleFactor + } + + contentItem: Label + { + text: tooltip.text + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("very_small") + renderType: Text.NativeRendering + } +} diff --git a/plugins/CuraDrive/src/qml/components/BackupList.qml b/plugins/CuraDrive/src/qml/components/BackupList.qml new file mode 100644 index 0000000000..231f25afc8 --- /dev/null +++ b/plugins/CuraDrive/src/qml/components/BackupList.qml @@ -0,0 +1,31 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.3 + +import UM 1.1 as UM + +ListView +{ + id: backupList + width: parent.width + clip: true + delegate: Item + { + width: parent.width + height: childrenRect.height + + BackupListItem + { + id: backupListItem + width: parent.width + } + + Divider + { + width: parent.width + anchors.top: backupListItem.bottom + } + } + ScrollBar.vertical: RightSideScrollBar {} +} diff --git a/plugins/CuraDrive/src/qml/components/BackupListFooter.qml b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml new file mode 100644 index 0000000000..80f47d6cba --- /dev/null +++ b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml @@ -0,0 +1,42 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.3 + +import UM 1.3 as UM + +import "../components" + +RowLayout +{ + id: backupListFooter + width: parent.width + property bool showInfoButton: false + + ActionButton + { + id: infoButton + text: catalog.i18nc("@button", "Want more?") + iconSource: "../images/info.svg" + onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2") + visible: backupListFooter.showInfoButton + } + + ActionButton + { + id: createBackupButton + text: catalog.i18nc("@button", "Backup Now") + iconSource: "../images/backup.svg" + enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup + onClicked: CuraDrive.createBackup() + busy: CuraDrive.isCreatingBackup + } + + ActionCheckBox + { + id: autoBackupEnabled + checked: CuraDrive.autoBackupEnabled + onClicked: CuraDrive.toggleAutoBackup(autoBackupEnabled.checked) + label: catalog.i18nc("@checkbox:description", "Automatically create a backup each day that Cura is started.") + } +} diff --git a/plugins/CuraDrive/src/qml/components/BackupListItem.qml b/plugins/CuraDrive/src/qml/components/BackupListItem.qml new file mode 100644 index 0000000000..abe9a1acf9 --- /dev/null +++ b/plugins/CuraDrive/src/qml/components/BackupListItem.qml @@ -0,0 +1,112 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.3 +import QtQuick.Dialogs 1.1 + +import UM 1.1 as UM + +Item +{ + id: backupListItem + width: parent.width + height: showDetails ? dataRow.height + backupDetails.height : dataRow.height + property bool showDetails: false + + // Backup details toggle animation. + Behavior on height + { + PropertyAnimation + { + duration: 70 + } + } + + RowLayout + { + id: dataRow + spacing: UM.Theme.getSize("default_margin").width * 2 + width: parent.width + height: 50 * screenScaleFactor + + ActionButton + { + color: "transparent" + hoverColor: "transparent" + textColor: UM.Theme.getColor("text") + textHoverColor: UM.Theme.getColor("primary") + iconSource: "../images/info.svg" + onClicked: backupListItem.showDetails = !backupListItem.showDetails + } + + Label + { + text: new Date(model["generated_time"]).toLocaleString(UM.Preferences.getValue("general/language")) + color: UM.Theme.getColor("text") + elide: Text.ElideRight + Layout.minimumWidth: 100 * screenScaleFactor + Layout.maximumWidth: 500 * screenScaleFactor + Layout.fillWidth: true + renderType: Text.NativeRendering + } + + Label + { + text: model["data"]["description"] + color: UM.Theme.getColor("text") + elide: Text.ElideRight + Layout.minimumWidth: 100 * screenScaleFactor + Layout.maximumWidth: 500 * screenScaleFactor + Layout.fillWidth: true + renderType: Text.NativeRendering + } + + ActionButton + { + text: catalog.i18nc("@button", "Restore") + color: "transparent" + hoverColor: "transparent" + textColor: UM.Theme.getColor("text") + textHoverColor: UM.Theme.getColor("text_link") + enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup + onClicked: confirmRestoreDialog.visible = true + } + + ActionButton + { + color: "transparent" + hoverColor: "transparent" + textColor: UM.Theme.getColor("setting_validation_error") + textHoverColor: UM.Theme.getColor("setting_validation_error") + iconSource: "../images/delete.svg" + onClicked: confirmDeleteDialog.visible = true + } + } + + BackupListItemDetails + { + id: backupDetails + backupDetailsData: model + width: parent.width + visible: parent.showDetails + anchors.top: dataRow.bottom + } + + MessageDialog + { + id: confirmDeleteDialog + title: catalog.i18nc("@dialog:title", "Delete Backup") + text: catalog.i18nc("@dialog:info", "Are you sure you want to delete this backup? This cannot be undone.") + standardButtons: StandardButton.Yes | StandardButton.No + onYes: CuraDrive.deleteBackup(model["backup_id"]) + } + + MessageDialog + { + id: confirmRestoreDialog + title: catalog.i18nc("@dialog:title", "Restore Backup") + text: catalog.i18nc("@dialog:info", "You will need to restart Cura before your backup is restored. Do you want to close Cura now?") + standardButtons: StandardButton.Yes | StandardButton.No + onYes: CuraDrive.restoreBackup(model["backup_id"]) + } +} diff --git a/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml b/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml new file mode 100644 index 0000000000..74d4c5ab57 --- /dev/null +++ b/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml @@ -0,0 +1,61 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.3 + +import UM 1.1 as UM + +ColumnLayout +{ + id: backupDetails + width: parent.width + spacing: 10 * screenScaleFactor + property var backupDetailsData + + // Cura version + BackupListItemDetailsRow + { + iconSource: "../images/cura.svg" + label: catalog.i18nc("@backuplist:label", "Cura Version") + value: backupDetailsData["data"]["cura_release"] + } + + // Machine count. + BackupListItemDetailsRow + { + iconSource: "../images/printer.svg" + label: catalog.i18nc("@backuplist:label", "Machines") + value: backupDetailsData["data"]["machine_count"] + } + + // Meterial count. + BackupListItemDetailsRow + { + iconSource: "../images/material.svg" + label: catalog.i18nc("@backuplist:label", "Materials") + value: backupDetailsData["data"]["material_count"] + } + + // Meterial count. + BackupListItemDetailsRow + { + iconSource: "../images/profile.svg" + label: catalog.i18nc("@backuplist:label", "Profiles") + value: backupDetailsData["data"]["profile_count"] + } + + // Meterial count. + BackupListItemDetailsRow + { + iconSource: "../images/plugin.svg" + label: catalog.i18nc("@backuplist:label", "Plugins") + value: backupDetailsData["data"]["plugin_count"] + } + + // Spacer. + Item + { + width: parent.width + height: 10 * screenScaleFactor + } +} diff --git a/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml b/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml new file mode 100644 index 0000000000..dad1674fe7 --- /dev/null +++ b/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml @@ -0,0 +1,52 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.3 + +import UM 1.3 as UM + +RowLayout +{ + id: detailsRow + width: parent.width + height: 40 * screenScaleFactor + + property var iconSource + property var label + property var value + + // Spacing. + Item + { + width: 40 * screenScaleFactor + } + + Icon + { + width: 18 * screenScaleFactor + iconSource: detailsRow.iconSource + color: UM.Theme.getColor("text") + } + + Label + { + text: detailsRow.label + color: UM.Theme.getColor("text") + elide: Text.ElideRight + Layout.minimumWidth: 50 * screenScaleFactor + Layout.maximumWidth: 100 * screenScaleFactor + Layout.fillWidth: true + renderType: Text.NativeRendering + } + + Label + { + text: detailsRow.value + color: UM.Theme.getColor("text") + elide: Text.ElideRight + Layout.minimumWidth: 50 * screenScaleFactor + Layout.maximumWidth: 100 * screenScaleFactor + Layout.fillWidth: true + renderType: Text.NativeRendering + } +} diff --git a/plugins/CuraDrive/src/qml/components/Divider.qml b/plugins/CuraDrive/src/qml/components/Divider.qml new file mode 100644 index 0000000000..bba2f2f29c --- /dev/null +++ b/plugins/CuraDrive/src/qml/components/Divider.qml @@ -0,0 +1,11 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 + +import UM 1.3 as UM + +Rectangle +{ + id: divider + color: UM.Theme.getColor("lining") + height: UM.Theme.getSize("default_lining").height +} diff --git a/plugins/CuraDrive/src/qml/components/Icon.qml b/plugins/CuraDrive/src/qml/components/Icon.qml new file mode 100644 index 0000000000..3cb822bf82 --- /dev/null +++ b/plugins/CuraDrive/src/qml/components/Icon.qml @@ -0,0 +1,56 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtGraphicalEffects 1.0 + +Item +{ + id: icon + width: parent.height + height: width + property var color: "transparent" + property var iconSource + property bool animated: false + + Image + { + id: iconImage + width: parent.height + height: width + smooth: true + source: icon.iconSource + sourceSize.width: width + sourceSize.height: height + antialiasing: true + visible: !icon.animated + } + + AnimatedImage + { + id: animatedIconImage + width: parent.height + height: width + smooth: true + antialiasing: true + source: "../images/loading.gif" + visible: icon.animated + } + + ColorOverlay + { + anchors.fill: iconImage + source: iconImage + color: icon.color + antialiasing: true + visible: !icon.animated + } + + ColorOverlay + { + anchors.fill: animatedIconImage + source: animatedIconImage + color: icon.color + antialiasing: true + visible: icon.animated + } +} diff --git a/plugins/CuraDrive/src/qml/components/RightSideScrollBar.qml b/plugins/CuraDrive/src/qml/components/RightSideScrollBar.qml new file mode 100644 index 0000000000..5ac5df15ff --- /dev/null +++ b/plugins/CuraDrive/src/qml/components/RightSideScrollBar.qml @@ -0,0 +1,13 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.3 + +ScrollBar +{ + active: true + size: parent.height + anchors.top: parent.top + anchors.right: parent.right + anchors.bottom: parent.bottom +} diff --git a/plugins/CuraDrive/src/qml/images/avatar_default.png b/plugins/CuraDrive/src/qml/images/avatar_default.png new file mode 100644 index 0000000000000000000000000000000000000000..0c306680f721dba0de73fb7be90fe5c548608692 GIT binary patch literal 3115 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 + + + Polygon 18 + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/backup.svg b/plugins/CuraDrive/src/qml/images/backup.svg new file mode 100644 index 0000000000..51f6be4cba --- /dev/null +++ b/plugins/CuraDrive/src/qml/images/backup.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/cura.svg b/plugins/CuraDrive/src/qml/images/cura.svg new file mode 100644 index 0000000000..6b1b6c0c79 --- /dev/null +++ b/plugins/CuraDrive/src/qml/images/cura.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/cura_logo.jpg b/plugins/CuraDrive/src/qml/images/cura_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..621c03f035cddb3c9337099c25bccf19a5866968 GIT binary patch literal 19308 zcmeHuc|6qJ_xRghD5)k|NlGdEPD~*si7GS!1Uf=KQ`TXHjaw?*`Ezs@` z7+>rjh?!NFRZy3e7nM_1S5i|~P=p|k+idfFGZ+wbn|-klw&L3yi)knZ^vAro**F&H zr;!#fp_H_*bsv7v$sR6NCxy^9#U)*R6xCTPG~Y&$<>K^O?Wi*;euK zty;Zm&Fa-_)~{Z@dOh>Ddi{JA!T+QI#-EVj3P>5c&A}!Hu?w{jG4os zXZLW*LQRNn6gmIG^JecJik`&v)QbhNFIqm2rxjMTewi?L^tlvwzvyLK@1)9M3n$;u z`1In+H+@rr5IZ}dmXk#j4>y+*i-hfRYdHZ0o?`2`@LMY=<-Ag3=YYHU3lfY<)_&Hd83PNyb!Yo>U=boL` z8kyrGw@wykpiD1YM<`9bZGXCd82zvq8)r+KnMsC^+6vL%uiVqAk3`OS%@AU**(k%! zD)VMaw zyzK3qiuz4L!gD0yL6Z&}zYIO6uMjPsCZ2~aFABCEpc*}YJ*bHM+`28zTz!iQ8`1&4 zC+bXGoaXrFMjJzc4W`)5D03C#UR4aTW6tsV^=sn$Bf{k}L-Y(I?kBz+lFEG=;XKz@ z_N^_)eRS-`#I~fZrZe{p5`T+Bct_IfVohec=HkWEGxSXcwmRLPDBYgYEIq5IaMNmE z`=m?f*5Q&u5f1v> zI@T0lPt0UMR7@n@PyhJfk(2KfQOGwjSI69h6fF^vEowQN6MGquWk%*6eevj{ct*nn zB4#3;h?oexgYvfKo-%1@KhxZGras+3D@HS^v%E8z0Ud6!s;MVc+Kf_jFVB+P9caP( zZe{Kv=QAMHkYeu`_7C@3FvGJCQZx&ZJ#U`SS0}c;WkBn@PKR-Jn@i+ACViY}!iTjX z_bSbT3+-UYz4GoD+MU9a$r{f-- zdZx4KW8afAr%OJxzQ;%6X}XRK$R>(5Yc=#{J6-84`~)Rzi`F^(@L;s*bNppE&7>Fq zNdn*Z-g}s@g7o5s2qj6@cb1OA1m%jss%W z<&V)%ni@={ifN!&-k8I%S3=icukJLlrzKrJ`^~Y!3k{`_^ znYj`Ome%x&A&2%EzMGxr+$IAe(q4x&&Jtc~Jq-SB$kzJtqr8UMQ&VwvzrB%xg^IYD zwKb0B&RPyy<_FijBfa!|7_2vVsid>$59E~LcqjvEu<@ktdKWSJpx*n_{g?JuU+qT@ zC1oMY;9ur$O);RhIo<0mPSY7PBfX(plx0)uyjzbjAf*wPKK05Yb^T>}ACi(AYO|DQ zBF~)v{X^UPdTcOB`z0|wzMcW~7%(8AU?X}i11cI9Pj3%NXF%Pi_ZZNpEqK~^MNXd9 za=>Ionvc1(8v86E7qT$*xQAc0PBoiYXU*Vtavq;FK`r#2Tj z7-fYW=(^YuM`3{j>$$&b1P3fyEfJF4XnIO=+KjJ%l@uMaa$+;8Q zJKjay6Q?Be!ZuqYFPvSp#nPrLG=FOmzFFTW7MGLMN-v(XTYqjSFW*)9%V)PWFXIiV%1Y(8Tyx60 zbjI-?`w6s7GaB2%b2EtB;=X3ZDL-!4P`ZcQOm{Y$fgkLj6WG#&%Qm-f>yhc$`C1Iw z`uN$bTgQ`kgPP9b>Al1({B%}-*EC*;0m047V^6)7n0_=9e=+vBqssAdvRt-X0jaHY zIB-Q?v0Ly6vZ#!7^RdJA*1=m^S2LTv9{U`osCjF}U*r6JtT<4GMzEg{g}O-?wmum!XFy-tqEl37;^_+XhZDqXVNDU`Y+Jp#;k-7v;fG&yYJBngUEcRu z&@Xz~{;&*WYzI<|Lo#tU2OozT{Wy+tM~*j5YZ zQjg%ECzCUA0mbq@N`(sV5Vb^Vkpg9{snZ~yzEd4e7X)Hk?|MqcxVGwbZT^m(9bd%@ z{FH9r%0}XqB5mSBFGrofe0%2}2>J5D~$jeJiAYV>g5zMcuA+iUeJT5iBo zi%D~y)}N^ne#au7iZ?{95c#C2wWAu)m5Ehuh%1~b zsTDuq(P6EkGF4WGwa~u%Qocwj!Wk*dVbvA}t>Um^k0!=Tlti0mb*mWW*a{Vf3y)s; z9YJC=O_;@3XNso}rv9HEjCYLTb+X@&xeC&|L=- zCC8m`K1-QHZmzmZ8NkQyO=~01I4v+@o=cF)VQn2i4bxtNOdLc-9WdVZ2QVL*S=M#f z%l&UKZP&l!X?r=CA>46#J4{@ebGvkDyE=&e3~qvT#j)VVey#^hme6^9P)HocOV+!WC!@C-7tPKpa z##}15!A}_^KSB`d0D$j-gn>)ym&@vx%j%cQ>X*yvm&@vx%j%cQ>X*yvm&@vx%j)kw zF2=k0^X7JNhJ>Kg;0Ml}z=;GMgdBhYXAj&yqQDX44AMC8!?IGqR0Xb?e~%#w>3}~` zNFI`fiX_joSi!orLU_g2F5LoMmwl0DLEpQk@9jcnX}8RKQHJG7$j)pd#|cWN@{k>N{&*Fj%o^0a&jnTDOF`h zJ1JFJJ7q_EM`cx2hb3y}eG+!A%Ufkq{X={ z>>cfuWff)ZWmQy}63r44^&@C3m_Gl7HDZeLTu8a1MOkv8Zii%gdo{(7%!x)hh%JE8 zKjFuJ)bm1pU*Mzsx3fRrI~L=J^R@Fr9drV``%eT+=3ml}acBA=zK_7e&I`EOaVRgW zrkJA_#!Yly7(CpaL~)pf3`;WpWs_JtpYP56j3NJ-fXt6{IYWLbseao`sResSRc zBMyAG{ZQ`UXyFSiyJexbz@}UXz2)ZQ?CpB_Yj=2H^P#Vw z6gJp@6Zbkdwo^yZH2Q8H68lU^S0d(KzOs;+1MYcK_ljNTSMmyPym0-HUTT4PGh+89 zau0_6tkMcvSVae|nFaz9riK@{P=rSMLU>lIlmTTmr%q zvmRHs56;Yo>T+;#f$(9@6&xIHtalL1XyJogCjiCVo@cMg-^u;9{%PZ<9ojGxd#^DC zaYdbT8=_5j-bLm;n#f#ULH041I_=2Y$wudAm2URTY3 z?dnA0G!y@H-6X7zmmqcVZaO4%31O@X@INkK>AM$fgDh6Jgc-qWSqR4oM>-ezs4U%E$Dpx@uX zfKvC4&fE#VUk>60Z~jh8ryMUkbGtvF`Ls)MT15eO+lZE)B6@RAwD>_?J@`6%qRvA1 z-3MU1e)m?*f)%<+t=dNB(&E+?sf1boZ<&1R;Qo-*wc@#LHyh9MMtO!4mqxj?`bdnH z?^8@b#i`!&M|o~Jaqjik0|;YDeTmz`gg&D>IJIU@Nbgb7QL^FVGeWs?`rc|(KBeCd z>AIBK;M=FE;z9ABCQM;LUAijP((3lVxzR=L@A=%mXaVsl|X?I;)_HYCcIo2 z5KlTrk^%y8nrcmdw?SlL)i%j0mKL#v<6ob_e06I<$3=xS}G zu}kpju!M0S!sSxmmOfCb&6o(HlP+81o&?-rECAFw)crP(8t<*4}SO0n0P zy9twzQiGc3X5JDiyh*D5`8F}hh1nkl4H=M(#oB7_mbV&f>q%^{5y(22?i0xq_Q!R$y z7*M=K1ucQl`+m7D|8LHw#j`F-4E$#CHXKFw@#GK#lj;GxRq z2j+dz9rsL14?+b|%^wfW+6uv@b1fRx;j~XO(PC%u$Ii6Jc2=6W>27GGJ$igfg{CEW z1AGsV5Mm0nt9w2J(gzxB(kT21{lar{3fhe&fV z-8l^Cjw}s?^T%~M(WAlQ{F(v_sQ)7<>K;|3da~TQ@pG~@pK|Qoz&MNRH&W47*;h8r zr7KQ*n(Ks@8ql#E3`qPdX|kb^0Y#2kJO@kEVj%ttiEcpJi|-J7PQP=n!g2%ac+fXW zt;5h_XN=~GJ~E&TgKWf@BPf`;ai0NIs=uMjf{18A;jA4n|8&}Ci(5v3Oi86H84hw? zgOZ9J8(crl)AxW)YVvdpX;G8!OOpM$x zdiju>OIzk0I_Xp2x4R^vkA%U~Jao@KGWO6#WN3|YaWoe%rR%)U$bodRT~eEJM+ffO zXI9d(1gcYJVhU_$4iqtM5t zv##ovh?U+_j+PoEqU*hW_AZ#&Pi9V^_5~>8NGkH4zs{*PDH$cl$IZA|!V~XkKi-X^ z7gG=Xc_5XzbGm#VVr&GkZ_1X|Ekf5PV5nE0Frdp3h+);gpXwNImc+XnPr0ey=|Il8 z6utFg>(_7Ri@u0m44+$f5k6#3j2$zpsj-56AoXrl(J9~^?edC!~3Y%3o zv5B~wYN1ai#_z*z-L-w|tq6lniO}mmgS<3%t5tQav(J19C|cUr{vqKCDgjFQIU(#McJcgt}|jXPthI-n{K>`X8baUus|> zH~IslZ{6viw9-miaqii(^~FVDK1be!5wG|<&fTv#u|T^mSc^9{bq#dY3dju(ZI9FF z)EhjxB^r-I*{$h%7!@W=*j!;V2d3#F6^rhbQMK`pF$pt7erb*9(EYeR@Ks5G!?{(# zFuj;Lya6ou0ZSphBq`v@*8chQx9y>G+ONV%ip=ftKU7cO578BPN@7kW<*Hp-YZ~&&^)?*(Fgbs|R*->BdX5(H-({G}AZgNzQTosPc$wfzSvuAlus0+Ie2(=9KDc%W7Gw!Z zLhhqUW3PeUh>&AIFZ3kCz{U->Z4CJ4XuUrwkZ)tvjH5_1ET}m=zpKi*$>_Drp1m2r zDXns+VD4r{x7*isVsY*CW7#5*E3%e&0XH|Mz z;6!02A}VAr+SP(C{+7_cpDAeOO4KAkaz|atK)Kz@Eb5Up`gH}gN_91 zu^R~s{D~C;zFt~{R;5sqX5^x3wJh7twhuN1DsK&KOFR+n1;wfx~#QcwcnBXi}9N0G6JqE@_QX}Z7r)0pwo z+4~MY5HF6<5hYUP^=iWNx3HF1T^A)Ou~d!WFK@IMSzm#Wv#0{!vh!X_RY6^Urotp8Q@G$H2?7jU!F|5$K5jP-!JyVQQ{T0Ct2oZF0r$0+HL4jVdp{eh9OM zAUA0A8;_5g<35vD+l8;}Oh1uvNqA%Jx0m~5=oU`b@m)eBvNU`qGs)=lvCePQl<9aK zdbnceJ9twiU8;-xWdA+-bQv&*JQ+~yG5j?J-{-yfZi~6~=qGvjv?!YOPI7UQV!eF3 zp?7*}<`(&Y1HDPX#^8I>O6ZH45Wk--Gr*qC1cn;o0t(Ls}X+pv>yl>uo2H)2# zN>e2MwS~VpHHw_qP5&TcYD}gP@B%PGLP*84C~~m!L=s)Ln|Oh_{_o($;C;Kr$&nFS zRunITz&pBV6ke!OGb`mSN<84<`API_6*v)0uY(0^riOThAY9sNJe_*W6waG|Ozze2 z=L$mJtQ4Eu8-8DeoJ*3y4>~pB2h=<0XDUuJpuL;PQ}p}667(2w=qyEa{f`)`&4rpO zrUeFd$|IXQ)FgM56kkI{h9^rM)~?)MDmE(1=oXvt0mhcXEf-ZPLwx>Ns+Wq)(Vy%idKjL2`0x4yTQ&vf#$qco7B z;2gLU?6;Ey4O#^GMY#j*4f(^(%-6yrO(TP4oXRPr0PqRky5AYlo?MIm*FX!Bd1$>; zv@{zl29&Uo^m$T}N`M9Md={b~H`>F1YQ2D_vxbF)vjmwr@zd6HubNjpV8tY^bBP=C zXr?_EF4IG7KlJCc^oBCNd}4OjSQ52OJ)p3hX{PkrX0y=CaJ*qe!V@! z+~`if$~Ii9ot(?2_x4ii5?q%1&T~s2gpYk2%uW%Qa(*z0s<74V1{Fl{uQB*)#1|s< zY!cnbo|u?xaXaK7{_|&Y3awIxb_j_1{l!xnQ4^R{ImNUWWX+d1Hf0Sd$CfmXtbkdz zU^XI;iVw{eO+T8u4Da+KP&!PTF$HzneaA@`hly_D&hDbujy_i3Bp<7|BM7mIwN1=m z=^lY}jbvcCCs-R%)pkkF82#rWcbR?$U(E%45cL8xVNuGOF?2Z*3hBLmbKpU`1e^jN zNVU~;a-cHLwnQ{7k^x=i23v<5o+5=!815R~F=yK}WmApE!iN}8nZS{{%`FRBd6{n| zQzeO(@?}(|U!gx>;%?_#Bb^st8>>i;M>;0Ojox7FkeGTPzzI0a(w%6{REf>Ek_S?#Fhpw;1?d~=yN)v>Adbm_(Kz00R4g0bM85*itxDXvr0}O7_G9v~%FJ`05LsdewIN ziaoLM80#3q(t)K>lzd>70sWVdTa@Yi>b`%L>hxi}&cN~YK`XroC26YJ&!@stMqlsF zBJ{}Y53%qcdL?wJt5zL+9N!W~TptkEWNa~ol`6v9ud|v$+!u~*ajxKFOdQD zZfK?-*AFj`q16E@+udND8^e8Zg(LvRexoSNHNJkH$OwJ%bEHs3eZ zH6GX14D+4i75dyo%kRQBS>ulZ7r}1Z7(BT?7`PGK=Hx8W{Uu%{O2q6har-hX=wPVa zP}Pu~;}B4@<47x#Zh8~3AJFy$X8NeOxa6EcU|94S|Lds_tL|KDya+a9ojqF*R@kWY z;i)f1t4HNFx+}g-BTwFu*fIESo3Rkms<1TH3LnDp%SQF*09g`efy z8+<+}c%MS-HljWw)`_(Qo9ZG)bf<0dig%!s^dr7-a zhWElIZjC1NJcMd{IX-Coc1Shjr(H$tgMUZ%27+_`|nuG%~Jk z<+LZl-{AYUk#Gccee%<+TxK*UWBjYyU6je~ zr#SapH{asNyqMf?4f7ZH_j8>}U00RJbB$J>xcWlg{p9HV)%j}Is7GA71UX<1s#!eN zjoF2_}U zWk6Lua|(8}{FE8E#0mPPg0d#?USO1JT5iCAg6qMnfvE9HsuO%X#9*=T6;42UKz&tw ztK!>W)mNXt8c86#>$+sUH{IxsUw@^r$vJo6)AI=9V|k%_7L9JkcPAW6v;329rM^2v V{_(E9+wT8(SKsZ~f4r-A{|^$iZYBT# literal 0 HcmV?d00001 diff --git a/plugins/CuraDrive/src/qml/images/cura_logo.png b/plugins/CuraDrive/src/qml/images/cura_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f846f2a0f0934f4843c3279ed741c149c77b2e1e GIT binary patch literal 13258 zcmeHu;4V*)BSRu^XaTvbLN?uz4uJ)2XzHpY!LRVSFdoD6lJwuy+WyZdCve~ygc*u(2ZXX z6n8BJ=~tEGlzXpU@kS`gN`3M{In2g&`K-Ng?48+YOUK6%Q!&j{8UO%f(B`2OjGe6E z=pFl$ws^S=Htqi&Eu+nt`f5zkAyuM|<9+lUC7ObBbhAJ)l>)Q49oq{+g+>xugU({Y z!PhW}n#xpMueQi{{k(Xgq^qIfb|t>U@SfRMr>zd5Z{lC^?1wlPJ#bg}3RdVQbU~*? z`TxiNdl#7g_x2?FIG|)>n5~LZ=;-U{r$9xWcyi_~xxLz49$qf=d`tG6X zIStGm9v<#dC-s0$b#qRMgsHFQp^FeRDg%|8>pO3^vnue#Z%ltH62Zo)abYu)lLUvz zhB9bh6s{bH^OfNC;bPHuwsUnckKTeN_+-^P3q;Rl_BXG`4v(a2lIvSvy%OYFLvt*Z39TCgr_a$F=Mp8M>*tj%kWC~v)94VFN5 z1qXZl?GEiRi;f`o`qcv{-e76%eFdNVsQd3-SZTWP)QI7xn+|JN!Q5xxAATLlA*Y)u z8pe!KMjd{a`|_;R{4@b4QtZ`tcX#VkMd;L<4<)IL{^KurzKG;6p8r3E0*#`BRc1%Z4{vHItk9179tO)8Rr)A_% z^@S+;h%bJ(TWOfDQ?c0pN|yDfn~1yLxnevG5wggjcb=tILHvFOzr@qF%`oJMOSJ+{ zvx|e^`kBeW*=VvqpXlg0-=GhY0MM;vt$z~ATSwB$WVr;veV2t$H*iB= z9Qz*R+wd#7o~OW+AvB*R9cy`8kkDbTmzQzWzP^*cskOBg%~EfKP~j8Ris_&aPFLcN zUi#uzboWne?!(MqzJEh8^?ZVidSPC0n3@71p>*8%Z)6$;fFKy%0QQAvykQl%ipl86D zp6LP5w;D0G`6p$=CEh&yyO5|8W$0;Ch)sve$3xGmuBW@dNk;N!XYs*xus)kGap#&jjLnRa>E$AcC6{{L0hB2p z(d5UzFVqZCgKNN3ck+4~>(E_#s08-L_qzM?y77I))HmP8By}+g-%woX6(1QP(8wUT z8_$T)hmPi*wF8sEf`U=ZECL~5A8}a?!?V`I{7*d4ZwX7Lx}uJP?o@<9_<=V#dT;p< zkU)+;E_S^HTK+lpxLcUa<4?$|o6Zp!vVcu&0D#Dwt;O`Ed(S5i;&3`TIyU^WPfJ>o zMJ{QDzv6}BI!lIRtO*usuLGIrp1YIl0XL^Co4C=jrpf69aHA(>N7tQyTpu5(GS~d5%UqzuqpMu?ZR; zn5YT+op(z-ohE~l$Y}oqN`(%5@MOS)F7%$_xUvh1wzo$U0txB7mObYxX2K4(z=BQ| zLm$e;sDrk?iu{re_7*`Ura)P?pJy5L-*KN1(;Z=9a~CixFsrkfDi}Z}`u3eqNJx

{tidI^DPfL*th*F*+_ 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 literal 0 HcmV?d00001 diff --git a/plugins/CuraDrive/src/qml/images/delete.svg b/plugins/CuraDrive/src/qml/images/delete.svg new file mode 100644 index 0000000000..2f6190ad43 --- /dev/null +++ b/plugins/CuraDrive/src/qml/images/delete.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/folder.svg b/plugins/CuraDrive/src/qml/images/folder.svg new file mode 100644 index 0000000000..f66f83a888 --- /dev/null +++ b/plugins/CuraDrive/src/qml/images/folder.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/home.svg b/plugins/CuraDrive/src/qml/images/home.svg new file mode 100644 index 0000000000..9d0e4d802c --- /dev/null +++ b/plugins/CuraDrive/src/qml/images/home.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/icon.png b/plugins/CuraDrive/src/qml/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3f75491786dcc0939175ffbab5e791e27eaefc2c GIT binary patch literal 21924 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�>#6>;@A&7;Ub9*u@xRReL>D;wp-rSVGg8b(Ftu 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 d{(EOoFjyv&`}RG)N<`qZ!PU# + + + \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/inverted_circle.png b/plugins/CuraDrive/src/qml/images/inverted_circle.png new file mode 100644 index 0000000000000000000000000000000000000000..3612b37d4d38cd7b1218be42c943932a79f7b465 GIT binary patch literal 1608 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 F;r|kGDY5_n literal 0 HcmV?d00001 diff --git a/plugins/CuraDrive/src/qml/images/material.svg b/plugins/CuraDrive/src/qml/images/material.svg new file mode 100644 index 0000000000..eac724e471 --- /dev/null +++ b/plugins/CuraDrive/src/qml/images/material.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/plugin.svg b/plugins/CuraDrive/src/qml/images/plugin.svg new file mode 100644 index 0000000000..674eb99a54 --- /dev/null +++ b/plugins/CuraDrive/src/qml/images/plugin.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/preview_banner.png b/plugins/CuraDrive/src/qml/images/preview_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..414019531beed2e503e05cc394fc7e6de246830a GIT binary patch literal 8324 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 E2O#6GiU0rr literal 0 HcmV?d00001 diff --git a/plugins/CuraDrive/src/qml/images/printer.svg b/plugins/CuraDrive/src/qml/images/printer.svg new file mode 100644 index 0000000000..f7dc83987d --- /dev/null +++ b/plugins/CuraDrive/src/qml/images/printer.svg @@ -0,0 +1,14 @@ + + + + icn_singlePrinter + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/profile.svg b/plugins/CuraDrive/src/qml/images/profile.svg new file mode 100644 index 0000000000..ec2130f3d6 --- /dev/null +++ b/plugins/CuraDrive/src/qml/images/profile.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/restore.svg b/plugins/CuraDrive/src/qml/images/restore.svg new file mode 100644 index 0000000000..803215eada --- /dev/null +++ b/plugins/CuraDrive/src/qml/images/restore.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/main.qml b/plugins/CuraDrive/src/qml/main.qml new file mode 100644 index 0000000000..4a2219cf1f --- /dev/null +++ b/plugins/CuraDrive/src/qml/main.qml @@ -0,0 +1,42 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Window 2.2 + +import UM 1.3 as UM +import Cura 1.1 as Cura + +import "components" +import "pages" + +Window +{ + id: curaDriveDialog + minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width) + minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height) + maximumWidth: minimumWidth * 1.2 + maximumHeight: minimumHeight * 1.2 + width: minimumWidth + height: minimumHeight + color: UM.Theme.getColor("sidebar") + title: catalog.i18nc("@title:window", "Cura Backups") + + // Globally available. + UM.I18nCatalog + { + id: catalog + name: "cura_drive" + } + + WelcomePage + { + id: welcomePage + visible: !Cura.API.account.isLoggedIn + } + + BackupsPage + { + id: backupsPage + visible: Cura.API.account.isLoggedIn + } +} diff --git a/plugins/CuraDrive/src/qml/pages/BackupsPage.qml b/plugins/CuraDrive/src/qml/pages/BackupsPage.qml new file mode 100644 index 0000000000..88ce766383 --- /dev/null +++ b/plugins/CuraDrive/src/qml/pages/BackupsPage.qml @@ -0,0 +1,73 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.3 + +import UM 1.3 as UM +import Cura 1.1 as Cura + +import "../components" + +Item +{ + id: backupsPage + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_margin").width * 3 + + ColumnLayout + { + spacing: UM.Theme.getSize("default_margin").height * 2 + width: parent.width + anchors.fill: parent + + Label + { + id: backupTitle + text: catalog.i18nc("@title", "My Backups") + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + Layout.fillWidth: true + renderType: Text.NativeRendering + } + + Label + { + text: catalog.i18nc("@empty_state", + "You don't have any backups currently. Use the 'Backup Now' button to create one.") + width: parent.width + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + wrapMode: Label.WordWrap + visible: backupList.count == 0 + Layout.fillWidth: true + Layout.fillHeight: true + renderType: Text.NativeRendering + } + + BackupList + { + id: backupList + model: CuraDrive.backups + Layout.fillWidth: true + Layout.fillHeight: true + } + + Label + { + text: catalog.i18nc("@backup_limit_info", + "During the preview phase, you'll be limited to 5 visible backups. Remove a backup to see older ones.") + width: parent.width + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + wrapMode: Label.WordWrap + visible: backupList.count > 4 + renderType: Text.NativeRendering + } + + BackupListFooter + { + id: backupListFooter + showInfoButton: backupList.count > 4 + } + } +} diff --git a/plugins/CuraDrive/src/qml/pages/WelcomePage.qml b/plugins/CuraDrive/src/qml/pages/WelcomePage.qml new file mode 100644 index 0000000000..882656dc4a --- /dev/null +++ b/plugins/CuraDrive/src/qml/pages/WelcomePage.qml @@ -0,0 +1,48 @@ +// Copyright (c) 2018 Ultimaker B.V. +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Window 2.2 + +import UM 1.3 as UM +import Cura 1.1 as Cura + +import "../components" + +Column +{ + id: welcomePage + spacing: UM.Theme.getSize("wide_margin").height + width: parent.width + topPadding: 150 * screenScaleFactor + + Image + { + id: profileImage + fillMode: Image.PreserveAspectFit + source: "../images/icon.png" + anchors.horizontalCenter: parent.horizontalCenter + width: Math.round(parent.width / 4) + } + + Label + { + id: welcomeTextLabel + text: catalog.i18nc("@description", "Backup and synchronize your Cura settings.") + width: Math.round(parent.width / 2) + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + wrapMode: Label.WordWrap + renderType: Text.NativeRendering + } + + ActionButton + { + id: loginButton + onClicked: Cura.API.account.login() + text: catalog.i18nc("@button", "Sign In") + anchors.horizontalCenter: parent.horizontalCenter + } +} From 861deaa9f74f4e3bc32db5f6c8e0628406c5e2e1 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 10 Dec 2018 14:20:19 +0100 Subject: [PATCH 29/40] Add renderType native for toolbox QML Labels CURA-6006 --- plugins/Toolbox/resources/qml/ToolboxAuthorPage.qml | 8 +++++++- plugins/Toolbox/resources/qml/ToolboxBackColumn.qml | 3 ++- .../resources/qml/ToolboxCompatibilityChart.qml | 8 +++++++- .../qml/ToolboxConfirmUninstallResetDialog.qml | 3 ++- plugins/Toolbox/resources/qml/ToolboxDetailPage.qml | 11 ++++++++++- plugins/Toolbox/resources/qml/ToolboxDetailTile.qml | 4 +++- .../resources/qml/ToolboxDetailTileActions.qml | 4 +++- .../Toolbox/resources/qml/ToolboxDownloadsGrid.qml | 3 ++- .../resources/qml/ToolboxDownloadsGridTile.qml | 4 +++- .../resources/qml/ToolboxDownloadsShowcase.qml | 3 ++- .../resources/qml/ToolboxDownloadsShowcaseTile.qml | 3 ++- plugins/Toolbox/resources/qml/ToolboxErrorPage.qml | 3 ++- plugins/Toolbox/resources/qml/ToolboxFooter.qml | 5 +++-- .../Toolbox/resources/qml/ToolboxInstalledPage.qml | 4 +++- .../Toolbox/resources/qml/ToolboxInstalledTile.qml | 6 +++++- .../resources/qml/ToolboxInstalledTileActions.qml | 5 ++++- .../Toolbox/resources/qml/ToolboxLicenseDialog.qml | 3 ++- plugins/Toolbox/resources/qml/ToolboxLoadingPage.qml | 3 ++- plugins/Toolbox/resources/qml/ToolboxTabButton.qml | 5 +++-- 19 files changed, 67 insertions(+), 21 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxAuthorPage.qml b/plugins/Toolbox/resources/qml/ToolboxAuthorPage.qml index 9c1df0c49e..7b026566c3 100644 --- a/plugins/Toolbox/resources/qml/ToolboxAuthorPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxAuthorPage.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.3 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM @@ -59,6 +59,7 @@ Item wrapMode: Text.WordWrap width: parent.width height: UM.Theme.getSize("toolbox_property_label").height + renderType: Text.NativeRendering } Label { @@ -70,6 +71,7 @@ Item left: title.left topMargin: UM.Theme.getSize("default_margin").height } + renderType: Text.NativeRendering } Column { @@ -88,12 +90,14 @@ Item text: catalog.i18nc("@label", "Website") + ":" font: UM.Theme.getFont("default") color: UM.Theme.getColor("text_medium") + renderType: Text.NativeRendering } Label { text: catalog.i18nc("@label", "Email") + ":" font: UM.Theme.getFont("default") color: UM.Theme.getColor("text_medium") + renderType: Text.NativeRendering } } Column @@ -122,6 +126,7 @@ Item color: UM.Theme.getColor("text") linkColor: UM.Theme.getColor("text_link") onLinkActivated: Qt.openUrlExternally(link) + renderType: Text.NativeRendering } Label @@ -138,6 +143,7 @@ Item color: UM.Theme.getColor("text") linkColor: UM.Theme.getColor("text_link") onLinkActivated: Qt.openUrlExternally(link) + renderType: Text.NativeRendering } } Rectangle diff --git a/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml b/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml index 8524b7d1e5..edb1967fee 100644 --- a/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml +++ b/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.2 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM @@ -64,6 +64,7 @@ Item font: UM.Theme.getFont("default_bold") horizontalAlignment: Text.AlignRight width: control.width + renderType: Text.NativeRendering } } } diff --git a/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml b/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml index d4c0ae14eb..db4e8c628f 100644 --- a/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml +++ b/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.7 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM @@ -67,6 +67,7 @@ Item wrapMode: Text.WordWrap color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("medium") + renderType: Text.NativeRendering } TableView @@ -99,6 +100,7 @@ Item text: styleData.value || "" color: UM.Theme.getColor("text") font: UM.Theme.getFont("default_bold") + renderType: Text.NativeRendering } Rectangle { @@ -118,6 +120,7 @@ Item text: styleData.value || "" color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("default") + renderType: Text.NativeRendering } } itemDelegate: Item @@ -130,6 +133,7 @@ Item text: styleData.value || "" color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("default") + renderType: Text.NativeRendering } } @@ -144,6 +148,7 @@ Item elide: Text.ElideRight color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("default") + renderType: Text.NativeRendering } } @@ -232,5 +237,6 @@ Item color: UM.Theme.getColor("text") linkColor: UM.Theme.getColor("text_link") onLinkActivated: Qt.openUrlExternally(link) + renderType: Text.NativeRendering } } diff --git a/plugins/Toolbox/resources/qml/ToolboxConfirmUninstallResetDialog.qml b/plugins/Toolbox/resources/qml/ToolboxConfirmUninstallResetDialog.qml index 2c5d08aa72..e238132680 100644 --- a/plugins/Toolbox/resources/qml/ToolboxConfirmUninstallResetDialog.qml +++ b/plugins/Toolbox/resources/qml/ToolboxConfirmUninstallResetDialog.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. -import QtQuick 2.2 +import QtQuick 2.10 import QtQuick.Controls 1.1 import QtQuick.Controls.Styles 1.1 import QtQuick.Layouts 1.1 @@ -66,6 +66,7 @@ UM.Dialog anchors.right: parent.right font: UM.Theme.getFont("default") wrapMode: Text.WordWrap + renderType: Text.NativeRendering } // Buttons diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml b/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml index 9e2e178b71..7983be8aef 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.3 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM @@ -65,6 +65,7 @@ Item wrapMode: Text.WordWrap width: parent.width height: UM.Theme.getSize("toolbox_property_label").height + renderType: Text.NativeRendering } Column @@ -84,24 +85,28 @@ Item text: catalog.i18nc("@label", "Version") + ":" font: UM.Theme.getFont("default") color: UM.Theme.getColor("text_medium") + renderType: Text.NativeRendering } Label { text: catalog.i18nc("@label", "Last updated") + ":" font: UM.Theme.getFont("default") color: UM.Theme.getColor("text_medium") + renderType: Text.NativeRendering } Label { text: catalog.i18nc("@label", "Author") + ":" font: UM.Theme.getFont("default") color: UM.Theme.getColor("text_medium") + renderType: Text.NativeRendering } Label { text: catalog.i18nc("@label", "Downloads") + ":" font: UM.Theme.getFont("default") color: UM.Theme.getColor("text_medium") + renderType: Text.NativeRendering } } Column @@ -121,6 +126,7 @@ Item text: details === null ? "" : (details.version || catalog.i18nc("@label", "Unknown")) font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") + renderType: Text.NativeRendering } Label { @@ -135,6 +141,7 @@ Item } font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") + renderType: Text.NativeRendering } Label { @@ -153,12 +160,14 @@ Item color: UM.Theme.getColor("text") linkColor: UM.Theme.getColor("text_link") onLinkActivated: Qt.openUrlExternally(link) + renderType: Text.NativeRendering } Label { text: details === null ? "" : (details.download_count || catalog.i18nc("@label", "Unknown")) font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") + renderType: Text.NativeRendering } } Rectangle diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml index 1d701543ce..43f97baf3f 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.7 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM @@ -31,6 +31,7 @@ Item wrapMode: Text.WordWrap color: UM.Theme.getColor("text") font: UM.Theme.getFont("medium_bold") + renderType: Text.NativeRendering } Label { @@ -42,6 +43,7 @@ Item wrapMode: Text.WordWrap color: UM.Theme.getColor("text") font: UM.Theme.getFont("default") + renderType: Text.NativeRendering } } diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml index 848acfbf4f..7160dafa2d 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.7 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM @@ -57,6 +57,8 @@ Column linkColor: UM.Theme.getColor("text_link") visible: loginRequired width: installButton.width + renderType: Text.NativeRendering + MouseArea { anchors.fill: parent diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml index 3e2643938b..8e15882ae1 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.7 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 @@ -23,6 +23,7 @@ Column width: parent.width color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("medium") + renderType: Text.NativeRendering } Grid { diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml index cee3f0fd20..357e9e9a72 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.3 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 @@ -72,6 +72,7 @@ Item wrapMode: Text.WordWrap color: UM.Theme.getColor("text") font: UM.Theme.getFont("default_bold") + renderType: Text.NativeRendering } Label { @@ -83,6 +84,7 @@ Item wrapMode: Text.WordWrap color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("default") + renderType: Text.NativeRendering } } } diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml index 9851128076..820b74554a 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.7 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM @@ -24,6 +24,7 @@ Rectangle width: parent.width color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("medium") + renderType: Text.NativeRendering } Grid { diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml index 8a2fdc8bc8..d1130cf63f 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. -import QtQuick 2.7 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtGraphicalEffects 1.0 @@ -79,6 +79,7 @@ Rectangle wrapMode: Text.WordWrap color: UM.Theme.getColor("button_text") font: UM.Theme.getFont("medium_bold") + renderType: Text.NativeRendering } } MouseArea diff --git a/plugins/Toolbox/resources/qml/ToolboxErrorPage.qml b/plugins/Toolbox/resources/qml/ToolboxErrorPage.qml index 600ae2b39f..e57e63dbb9 100644 --- a/plugins/Toolbox/resources/qml/ToolboxErrorPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxErrorPage.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.7 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 @@ -18,5 +18,6 @@ Rectangle { centerIn: parent } + renderType: Text.NativeRendering } } diff --git a/plugins/Toolbox/resources/qml/ToolboxFooter.qml b/plugins/Toolbox/resources/qml/ToolboxFooter.qml index 5c2a6577ad..2d42ca7269 100644 --- a/plugins/Toolbox/resources/qml/ToolboxFooter.qml +++ b/plugins/Toolbox/resources/qml/ToolboxFooter.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.2 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM @@ -26,7 +26,7 @@ Item right: restartButton.right rightMargin: UM.Theme.getSize("default_margin").width } - + renderType: Text.NativeRendering } Button { @@ -56,6 +56,7 @@ Item text: control.text verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter + renderType: Text.NativeRendering } } } diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml index 3d5cd1c8d4..e1d01db59a 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.7 +import QtQuick 2.10 import QtQuick.Dialogs 1.1 import QtQuick.Window 2.2 import QtQuick.Controls 1.4 @@ -38,6 +38,7 @@ ScrollView text: catalog.i18nc("@title:tab", "Plugins") color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("medium") + renderType: Text.NativeRendering } Rectangle { @@ -68,6 +69,7 @@ ScrollView text: catalog.i18nc("@title:tab", "Materials") color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("medium") + renderType: Text.NativeRendering } Rectangle diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml index b16564fdd2..593e024309 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.7 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM @@ -51,6 +51,7 @@ Item wrapMode: Text.WordWrap font: UM.Theme.getFont("default_bold") color: pluginInfo.color + renderType: Text.NativeRendering } Label { @@ -60,6 +61,7 @@ Item width: parent.width wrapMode: Text.WordWrap color: pluginInfo.color + renderType: Text.NativeRendering } } Column @@ -88,6 +90,7 @@ Item onLinkActivated: Qt.openUrlExternally("mailto:" + model.author_email + "?Subject=Cura: " + model.name + " Plugin") color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining") linkColor: UM.Theme.getColor("text_link") + renderType: Text.NativeRendering } Label @@ -98,6 +101,7 @@ Item color: UM.Theme.getColor("text") verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignLeft + renderType: Text.NativeRendering } } ToolboxInstalledTileActions diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml index 39528f6437..61af84fbe5 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.7 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM @@ -24,6 +24,7 @@ Column font: UM.Theme.getFont("default") wrapMode: Text.WordWrap width: parent.width + renderType: Text.NativeRendering } ToolboxProgressButton @@ -55,6 +56,8 @@ Column linkColor: UM.Theme.getColor("text_link") visible: loginRequired width: updateButton.width + renderType: Text.NativeRendering + MouseArea { anchors.fill: parent diff --git a/plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml b/plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml index b8baf7bc83..40b22c268d 100644 --- a/plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml +++ b/plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.2 +import QtQuick 2.10 import QtQuick.Dialogs 1.1 import QtQuick.Window 2.2 import QtQuick.Controls 1.4 @@ -32,6 +32,7 @@ UM.Dialog anchors.right: parent.right text: licenseDialog.pluginName + catalog.i18nc("@label", "This plugin contains a license.\nYou need to accept this license to install this plugin.\nDo you agree with the terms below?") wrapMode: Text.Wrap + renderType: Text.NativeRendering } TextArea { diff --git a/plugins/Toolbox/resources/qml/ToolboxLoadingPage.qml b/plugins/Toolbox/resources/qml/ToolboxLoadingPage.qml index 1ba271dcab..025239bd43 100644 --- a/plugins/Toolbox/resources/qml/ToolboxLoadingPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxLoadingPage.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.7 +import QtQuick 2.10 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 @@ -18,5 +18,6 @@ Rectangle { centerIn: parent } + renderType: Text.NativeRendering } } diff --git a/plugins/Toolbox/resources/qml/ToolboxTabButton.qml b/plugins/Toolbox/resources/qml/ToolboxTabButton.qml index fa4f75d6fe..5e1aeaa636 100644 --- a/plugins/Toolbox/resources/qml/ToolboxTabButton.qml +++ b/plugins/Toolbox/resources/qml/ToolboxTabButton.qml @@ -1,8 +1,8 @@ // Copyright (c) 2018 Ultimaker B.V. // Toolbox is released under the terms of the LGPLv3 or higher. -import QtQuick 2.2 -import QtQuick.Controls 2.0 +import QtQuick 2.10 +import QtQuick.Controls 2.3 import UM 1.1 as UM Button @@ -46,5 +46,6 @@ Button font: control.enabled ? (control.active ? UM.Theme.getFont("medium_bold") : UM.Theme.getFont("medium")) : UM.Theme.getFont("default_italic") verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter + renderType: Text.NativeRendering } } \ No newline at end of file From 69744282e6208ef959035c7237a9685d9d85b819 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 10 Dec 2018 14:37:44 +0100 Subject: [PATCH 30/40] Fix rounding issue in toolbox QML widget size CURA-6006 --- plugins/Toolbox/resources/qml/Toolbox.qml | 4 ++-- plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/Toolbox/resources/qml/Toolbox.qml b/plugins/Toolbox/resources/qml/Toolbox.qml index 7cc5a730f2..9ede2a6bda 100644 --- a/plugins/Toolbox/resources/qml/Toolbox.qml +++ b/plugins/Toolbox/resources/qml/Toolbox.qml @@ -14,8 +14,8 @@ Window modality: Qt.ApplicationModal flags: Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint - width: 720 * screenScaleFactor - height: 640 * screenScaleFactor + width: Math.floor(720 * screenScaleFactor) + height: Math.floor(640 * screenScaleFactor) minimumWidth: width maximumWidth: minimumWidth minimumHeight: height diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml index 8e15882ae1..85f0ff8be4 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml @@ -38,7 +38,7 @@ Column delegate: Loader { asynchronous: true - width: (grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns + width: Math.round((grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns) height: UM.Theme.getSize("toolbox_thumbnail_small").height source: "ToolboxDownloadsGridTile.qml" } From badf2962bc90c910fc10b09ecb309b10604d5090 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 11 Dec 2018 10:34:00 +0100 Subject: [PATCH 31/40] Change CuraDrive version to 1.2.0 CURA-6005 Because the previously version is 1.1.1 --- plugins/CuraDrive/plugin.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/CuraDrive/plugin.json b/plugins/CuraDrive/plugin.json index 134cd31a77..44bf955f41 100644 --- a/plugins/CuraDrive/plugin.json +++ b/plugins/CuraDrive/plugin.json @@ -2,7 +2,7 @@ "name": "Cura Backups", "author": "Ultimaker B.V.", "description": "Backup and restore your configuration.", - "version": "1.2.1", + "version": "1.2.0", "api": 5, "i18n-catalog": "cura_drive" -} \ No newline at end of file +} From 9618e8d4e096aaaeaf6e510a00b7436bd4cc852f Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 11 Dec 2018 10:34:49 +0100 Subject: [PATCH 32/40] Add CuraDrive 1.2.0 to bundled_packages CURA-6005 --- resources/bundled_packages/cura.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/resources/bundled_packages/cura.json b/resources/bundled_packages/cura.json index d8a7df2478..33c6304fc5 100644 --- a/resources/bundled_packages/cura.json +++ b/resources/bundled_packages/cura.json @@ -50,6 +50,23 @@ } } }, + "CuraDrive": { + "package_info": { + "package_id": "CuraDrive", + "package_type": "plugin", + "display_name": "Cura Backups", + "description": "Backup and restore your configuration.", + "package_version": "1.2.0", + "sdk_version": 5, + "website": "https://ultimaker.com", + "author": { + "author_id": "UltimakerPackages", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } + }, "CuraEngineBackend": { "package_info": { "package_id": "CuraEngineBackend", From 76b9fa79986d2622eca7a7316ba65a05aaaa95bd Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 11 Dec 2018 11:53:12 +0100 Subject: [PATCH 33/40] Use "cura" as i18n catalog for CuraDrive CURA-6005 --- plugins/CuraDrive/plugin.json | 2 +- plugins/CuraDrive/src/Settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/CuraDrive/plugin.json b/plugins/CuraDrive/plugin.json index 44bf955f41..6cf1fa273c 100644 --- a/plugins/CuraDrive/plugin.json +++ b/plugins/CuraDrive/plugin.json @@ -4,5 +4,5 @@ "description": "Backup and restore your configuration.", "version": "1.2.0", "api": 5, - "i18n-catalog": "cura_drive" + "i18n-catalog": "cura" } diff --git a/plugins/CuraDrive/src/Settings.py b/plugins/CuraDrive/src/Settings.py index 277a976cc7..f10a7d3bf7 100644 --- a/plugins/CuraDrive/src/Settings.py +++ b/plugins/CuraDrive/src/Settings.py @@ -13,7 +13,7 @@ class Settings: AUTO_BACKUP_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled" AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date" - I18N_CATALOG_ID = "cura_drive" + I18N_CATALOG_ID = "cura" I18N_CATALOG = i18nCatalog(I18N_CATALOG_ID) MESSAGE_TITLE = I18N_CATALOG.i18nc("@info:title", "Backups"), From 8e4ad23da104ead8ee5ce3658e7132d3d8e9eaec Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 11 Dec 2018 12:12:32 +0100 Subject: [PATCH 34/40] Add CuraConstants CURA-6005 Put constant values into a separate file CuraConstants.py --- cura/CuraConstants.py | 54 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 cura/CuraConstants.py diff --git a/cura/CuraConstants.py b/cura/CuraConstants.py new file mode 100644 index 0000000000..331937a0c2 --- /dev/null +++ b/cura/CuraConstants.py @@ -0,0 +1,54 @@ +# +# This file contains all constant values in Cura +# + +# ------------- +# Cura Versions +# ------------- +DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura" +DEFAULT_CURA_VERSION = "master" +DEFAULT_CURA_BUILD_TYPE = "" +DEFAULT_CURA_DEBUG_MODE = False +DEFAULT_CURA_SDK_VERSION = "5.0.0" + +try: + from cura.CuraVersion import CuraAppDisplayName # type: ignore +except ImportError: + CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME + +try: + from cura.CuraVersion import CuraVersion # type: ignore +except ImportError: + CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value] + +try: + from cura.CuraVersion import CuraBuildType # type: ignore +except ImportError: + CuraBuildType = DEFAULT_CURA_BUILD_TYPE + +try: + from cura.CuraVersion import CuraDebugMode # type: ignore +except ImportError: + CuraDebugMode = DEFAULT_CURA_DEBUG_MODE + +try: + from cura.CuraVersion import CuraSDKVersion # type: ignore +except ImportError: + CuraSDKVersion = DEFAULT_CURA_SDK_VERSION + + +# --------- +# Cloud API +# --------- +DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str +DEFAULT_CLOUD_API_VERSION = 1 # type: int + +try: + from cura.CuraVersion import CuraCloudAPIRoot # type: ignore +except ImportError: + CuraCloudAPIRoot = DEFAULT_CLOUD_API_ROOT + +try: + from cura.CuraVersion import CuraCloudAPIVersion # type: ignore +except ImportError: + CuraCloudAPIVersion = DEFAULT_CLOUD_API_VERSION From 2275e5c71f2ddd2181bdb54bb7c775165f14f1a6 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 11 Dec 2018 12:13:34 +0100 Subject: [PATCH 35/40] Refactor code to use constants in CuraConstants CURA-6005 --- cura/CuraApplication.py | 23 +++++++----------- plugins/CuraDrive/src/Settings.py | 9 +++---- plugins/Toolbox/src/Toolbox.py | 40 ++++--------------------------- 3 files changed, 17 insertions(+), 55 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index dfdb50515f..95b94c01c7 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -115,6 +115,8 @@ from cura.ObjectsModel import ObjectsModel from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage +from cura import CuraConstants + from UM.FlameProfiler import pyqtSlot from UM.Decorators import override @@ -127,15 +129,6 @@ if TYPE_CHECKING: numpy.seterr(all = "ignore") -try: - from cura.CuraVersion import CuraAppDisplayName, CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion # type: ignore -except ImportError: - CuraAppDisplayName = "Ultimaker Cura" - CuraVersion = "master" # [CodeStyle: Reflecting imported value] - CuraBuildType = "" - CuraDebugMode = False - CuraSDKVersion = "5.0.0" - class CuraApplication(QtApplication): # SettingVersion represents the set of settings available in the machine/extruder definitions. @@ -162,11 +155,11 @@ class CuraApplication(QtApplication): def __init__(self, *args, **kwargs): super().__init__(name = "cura", - app_display_name = CuraAppDisplayName, - version = CuraVersion, - api_version = CuraSDKVersion, - buildtype = CuraBuildType, - is_debug_mode = CuraDebugMode, + app_display_name = CuraConstants.CuraAppDisplayName, + version = CuraConstants.CuraVersion, + api_version = CuraConstants.CuraSDKVersion, + buildtype = CuraConstants.CuraBuildType, + is_debug_mode = CuraConstants.CuraDebugMode, tray_icon_name = "cura-icon-32.png", **kwargs) @@ -937,7 +930,7 @@ class CuraApplication(QtApplication): engine.rootContext().setContextProperty("CuraApplication", self) engine.rootContext().setContextProperty("PrintInformation", self._print_information) engine.rootContext().setContextProperty("CuraActions", self._cura_actions) - engine.rootContext().setContextProperty("CuraSDKVersion", CuraSDKVersion) + engine.rootContext().setContextProperty("CuraSDKVersion", CuraConstants.CuraSDKVersion) qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type") diff --git a/plugins/CuraDrive/src/Settings.py b/plugins/CuraDrive/src/Settings.py index f10a7d3bf7..c0df66b950 100644 --- a/plugins/CuraDrive/src/Settings.py +++ b/plugins/CuraDrive/src/Settings.py @@ -1,21 +1,22 @@ # Copyright (c) 2018 Ultimaker B.V. from UM import i18nCatalog +from cura import CuraConstants + class Settings: """ Keeps the application settings. """ - UM_CLOUD_API_ROOT = "https://api.ultimaker.com" DRIVE_API_VERSION = 1 - DRIVE_API_URL = "{}/cura-drive/v{}".format(UM_CLOUD_API_ROOT, str(DRIVE_API_VERSION)) - + DRIVE_API_URL = "{}/cura-drive/v{}".format(CuraConstants.CuraCloudAPIRoot, str(DRIVE_API_VERSION)) + AUTO_BACKUP_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled" AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date" I18N_CATALOG_ID = "cura" I18N_CATALOG = i18nCatalog(I18N_CATALOG_ID) - + MESSAGE_TITLE = I18N_CATALOG.i18nc("@info:title", "Backups"), # Translatable messages for the entire plugin. diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 562a964f01..667b28c018 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -18,6 +18,7 @@ from UM.i18n import i18nCatalog from UM.Version import Version import cura +from cura import CuraConstants from cura.CuraApplication import CuraApplication from .AuthorsModel import AuthorsModel @@ -39,9 +40,9 @@ class Toolbox(QObject, Extension): self._application = application # type: CuraApplication - self._sdk_version = None # type: Optional[Union[str, int]] - self._cloud_api_version = None # type: Optional[int] - self._cloud_api_root = None # type: Optional[str] + self._sdk_version = CuraConstants.CuraSDKVersion # type: Union[str, int] + self._cloud_api_version = CuraConstants.CuraCloudAPIVersion # type: int + self._cloud_api_root = CuraConstants.CuraCloudAPIRoot # type: str self._api_url = None # type: Optional[str] # Network: @@ -168,9 +169,6 @@ class Toolbox(QObject, Extension): def _onAppInitialized(self) -> None: self._plugin_registry = self._application.getPluginRegistry() self._package_manager = self._application.getPackageManager() - self._sdk_version = self._getSDKVersion() - self._cloud_api_version = self._getCloudAPIVersion() - self._cloud_api_root = self._getCloudAPIRoot() self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format( cloud_api_root = self._cloud_api_root, cloud_api_version = self._cloud_api_version, @@ -186,36 +184,6 @@ class Toolbox(QObject, Extension): "materials_generic": QUrl("{base_url}/packages?package_type=material&tags=generic".format(base_url = self._api_url)) } - # Get the API root for the packages API depending on Cura version settings. - def _getCloudAPIRoot(self) -> str: - if not hasattr(cura, "CuraVersion"): - return self.DEFAULT_CLOUD_API_ROOT - if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): # type: ignore - return self.DEFAULT_CLOUD_API_ROOT - if not cura.CuraVersion.CuraCloudAPIRoot: # type: ignore - return self.DEFAULT_CLOUD_API_ROOT - return cura.CuraVersion.CuraCloudAPIRoot # type: ignore - - # Get the cloud API version from CuraVersion - def _getCloudAPIVersion(self) -> int: - if not hasattr(cura, "CuraVersion"): - return self.DEFAULT_CLOUD_API_VERSION - if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): # type: ignore - return self.DEFAULT_CLOUD_API_VERSION - if not cura.CuraVersion.CuraCloudAPIVersion: # type: ignore - return self.DEFAULT_CLOUD_API_VERSION - return cura.CuraVersion.CuraCloudAPIVersion # type: ignore - - # Get the packages version depending on Cura version settings. - def _getSDKVersion(self) -> Union[int, str]: - if not hasattr(cura, "CuraVersion"): - return self._application.getAPIVersion().getMajor() - if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore - return self._application.getAPIVersion().getMajor() - if not cura.CuraVersion.CuraSDKVersion: # type: ignore - return self._application.getAPIVersion().getMajor() - return cura.CuraVersion.CuraSDKVersion # type: ignore - @pyqtSlot() def browsePackages(self) -> None: # Create the network manager: From 8c07a6e89b97c6a545720e481a606f4f2a7e29f7 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 12 Dec 2018 10:35:36 +0100 Subject: [PATCH 36/40] Fix typing issues CURA-6005 --- plugins/CuraDrive/src/DriveApiService.py | 8 ++++---- plugins/CuraDrive/src/DrivePluginExtension.py | 3 ++- plugins/CuraDrive/src/UploadBackupJob.py | 4 ++-- plugins/CuraDrive/src/models/BackupListModel.py | 6 +++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index a677466838..6963e595b5 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -3,7 +3,7 @@ import base64 import hashlib from datetime import datetime from tempfile import NamedTemporaryFile -from typing import Optional, List, Dict +from typing import Any, Optional, List, Dict import requests @@ -34,7 +34,7 @@ class DriveApiService: """Create a new instance of the Drive API service and set the cura_api object.""" self._cura_api = cura_api - def getBackups(self) -> List[Dict[str, any]]: + def getBackups(self) -> List[Dict[str, Any]]: """Get all backups from the API.""" access_token = self._cura_api.account.accessToken if not access_token: @@ -85,7 +85,7 @@ class DriveApiService: else: self.onCreatingStateChanged.emit(is_creating=False) - def restoreBackup(self, backup: Dict[str, any]) -> None: + def restoreBackup(self, backup: Dict[str, Any]) -> None: """ Restore a previously exported backup from cloud storage. :param backup: A dict containing an entry from the API list response. @@ -157,7 +157,7 @@ class DriveApiService: return False return True - def _requestBackupUpload(self, backup_metadata: Dict[str, any], backup_size: int) -> Optional[str]: + def _requestBackupUpload(self, backup_metadata: Dict[str, Any], backup_size: int) -> Optional[str]: """ Request a backup upload slot from the API. :param backup_metadata: A dict containing some meta data about the backup. diff --git a/plugins/CuraDrive/src/DrivePluginExtension.py b/plugins/CuraDrive/src/DrivePluginExtension.py index 556fb187df..7e1472b988 100644 --- a/plugins/CuraDrive/src/DrivePluginExtension.py +++ b/plugins/CuraDrive/src/DrivePluginExtension.py @@ -70,7 +70,8 @@ class DrivePluginExtension(QObject, Extension): if not self._drive_window: self._drive_window = self.createDriveWindow() self.refreshBackups() - self._drive_window.show() + if self._drive_window: + self._drive_window.show() def createDriveWindow(self) -> Optional["QObject"]: """ diff --git a/plugins/CuraDrive/src/UploadBackupJob.py b/plugins/CuraDrive/src/UploadBackupJob.py index 039e6d1a09..bcecce554a 100644 --- a/plugins/CuraDrive/src/UploadBackupJob.py +++ b/plugins/CuraDrive/src/UploadBackupJob.py @@ -14,14 +14,14 @@ class UploadBackupJob(Job): As it can take longer than some other tasks, we schedule this using a Cura Job. """ - def __init__(self, signed_upload_url: str, backup_zip: bytes): + def __init__(self, signed_upload_url: str, backup_zip: bytes) -> None: super().__init__() self._signed_upload_url = signed_upload_url self._backup_zip = backup_zip self._upload_success = False self.backup_upload_error_message = "" - def run(self): + def run(self) -> None: Message(Settings.translatable_messages["uploading_backup"], title = Settings.MESSAGE_TITLE, lifetime = 10).show() diff --git a/plugins/CuraDrive/src/models/BackupListModel.py b/plugins/CuraDrive/src/models/BackupListModel.py index 9567b3d255..93b0c4c48c 100644 --- a/plugins/CuraDrive/src/models/BackupListModel.py +++ b/plugins/CuraDrive/src/models/BackupListModel.py @@ -1,5 +1,5 @@ # Copyright (c) 2018 Ultimaker B.V. -from typing import List, Dict +from typing import Any, List, Dict from UM.Qt.ListModel import ListModel @@ -11,7 +11,7 @@ class BackupListModel(ListModel): The BackupListModel transforms the backups data that came from the server so it can be served to the Qt UI. """ - def __init__(self, parent=None): + def __init__(self, parent = None) -> None: super().__init__(parent) self.addRoleName(Qt.UserRole + 1, "backup_id") self.addRoleName(Qt.UserRole + 2, "download_url") @@ -19,7 +19,7 @@ class BackupListModel(ListModel): self.addRoleName(Qt.UserRole + 4, "md5_hash") self.addRoleName(Qt.UserRole + 5, "data") - def loadBackups(self, data: List[Dict[str, any]]) -> None: + def loadBackups(self, data: List[Dict[str, Any]]) -> None: """ Populate the model with server data. :param data: From 31331e36d2fa6468973f1f1d26ae7df3d8c0d6a9 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 12 Dec 2018 09:19:43 +0100 Subject: [PATCH 37/40] Revert "Use the capitalized version of the buildplate name" This reverts commit 11d8831d7a9a15e3e916d5d9762bfe1f755042e5. Contributes to CURA-6021. --- cura/PrinterOutput/ConfigurationModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/PrinterOutput/ConfigurationModel.py b/cura/PrinterOutput/ConfigurationModel.py index 6f55aa3b1f..89e609c913 100644 --- a/cura/PrinterOutput/ConfigurationModel.py +++ b/cura/PrinterOutput/ConfigurationModel.py @@ -44,7 +44,7 @@ class ConfigurationModel(QObject): @pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged) def buildplateConfiguration(self) -> str: - return self._buildplate_configuration.capitalize() + return self._buildplate_configuration ## This method is intended to indicate whether the configuration is valid or not. # The method checks if the mandatory fields are or not set From f302a76d3aedf38e051e692f59246d406a1295d9 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 12 Dec 2018 10:47:15 +0100 Subject: [PATCH 38/40] Fix typing issue in Toolbox CURA-6006 --- plugins/Toolbox/src/Toolbox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index ab975548ce..d957b7aae1 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -670,7 +670,8 @@ class Toolbox(QObject, Extension): self.setDownloadProgress(new_progress) if bytes_sent == bytes_total: self.setIsDownloading(False) - cast(QNetworkReply, self._download_reply).downloadProgress.disconnect(self._onDownloadProgress) + self._download_reply = cast(QNetworkReply, self._download_reply) + self._download_reply.downloadProgress.disconnect(self._onDownloadProgress) # Check if the download was sucessfull if self._download_reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: From a6663ea0e8cf80cfc6bcd4d71db593009b83f514 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 12 Dec 2018 10:51:11 +0100 Subject: [PATCH 39/40] Fix typing issues CURA-6005 --- cura/CuraConstants.py | 2 +- plugins/CuraDrive/src/DriveApiService.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cura/CuraConstants.py b/cura/CuraConstants.py index 331937a0c2..c573d550c5 100644 --- a/cura/CuraConstants.py +++ b/cura/CuraConstants.py @@ -41,7 +41,7 @@ except ImportError: # Cloud API # --------- DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str -DEFAULT_CLOUD_API_VERSION = 1 # type: int +DEFAULT_CLOUD_API_VERSION = "1" # type: str try: from cura.CuraVersion import CuraCloudAPIRoot # type: ignore diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index 6963e595b5..98199c91cf 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -108,7 +108,7 @@ class DriveApiService: for chunk in download_package: write_backup.write(chunk) - if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash")): + if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash", "")): # Don't restore the backup if the MD5 hashes do not match. # This can happen if the download was interrupted. Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.") From f72b58386b39d1140ffdc489127eb396604afb77 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 13 Dec 2018 11:46:11 +0100 Subject: [PATCH 40/40] Also use CuraConstants for account API root --- cura/API/Account.py | 10 ++++++---- cura/CuraConstants.py | 6 ++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index 397e220478..7b4bc32e99 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -6,6 +6,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty from UM.i18n import i18nCatalog from UM.Message import Message +from cura import CuraConstants from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.Models import OAuth2Settings @@ -37,15 +38,16 @@ class Account(QObject): self._logged_in = False self._callback_port = 32118 - self._oauth_root = "https://account.ultimaker.com" - self._cloud_api_root = "https://api.ultimaker.com" + self._oauth_root = CuraConstants.CuraCloudAccountAPIRoot self._oauth_settings = OAuth2Settings( OAUTH_SERVER_URL= self._oauth_root, CALLBACK_PORT=self._callback_port, CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port), - CLIENT_ID="um---------------ultimaker_cura_drive_plugin", - CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.write", + CLIENT_ID="um----------------------------ultimaker_cura", + CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download " + "packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write " + "cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write", AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data", AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root), AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root) diff --git a/cura/CuraConstants.py b/cura/CuraConstants.py index c573d550c5..7ca8ea865b 100644 --- a/cura/CuraConstants.py +++ b/cura/CuraConstants.py @@ -42,6 +42,7 @@ except ImportError: # --------- DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str DEFAULT_CLOUD_API_VERSION = "1" # type: str +DEFAULT_CLOUD_ACCOUNT_API_ROOT = "https://account.ultimaker.com" # type: str try: from cura.CuraVersion import CuraCloudAPIRoot # type: ignore @@ -52,3 +53,8 @@ try: from cura.CuraVersion import CuraCloudAPIVersion # type: ignore except ImportError: CuraCloudAPIVersion = DEFAULT_CLOUD_API_VERSION + +try: + from cura.CuraVersion import CuraCloudAccountAPIRoot # type: ignore +except ImportError: + CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT