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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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 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 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/76] 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/76] 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 d495ec18bb7ab42de07bfb3617c00e7598a20e9e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 10 Dec 2018 15:28:20 +0100 Subject: [PATCH 31/76] Filter a bit more intelligently on when to check for errors CURA-6016 --- cura/Machines/MachineErrorChecker.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/cura/Machines/MachineErrorChecker.py b/cura/Machines/MachineErrorChecker.py index 06f064315b..fb11123af6 100644 --- a/cura/Machines/MachineErrorChecker.py +++ b/cura/Machines/MachineErrorChecker.py @@ -64,21 +64,21 @@ class MachineErrorChecker(QObject): def _onMachineChanged(self) -> None: if self._global_stack: - self._global_stack.propertyChanged.disconnect(self.startErrorCheck) + self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged) self._global_stack.containersChanged.disconnect(self.startErrorCheck) for extruder in self._global_stack.extruders.values(): - extruder.propertyChanged.disconnect(self.startErrorCheck) + extruder.propertyChanged.disconnect(self.startErrorCheckPropertyChanged) extruder.containersChanged.disconnect(self.startErrorCheck) self._global_stack = self._machine_manager.activeMachine if self._global_stack: - self._global_stack.propertyChanged.connect(self.startErrorCheck) + self._global_stack.propertyChanged.connect(self.startErrorCheckPropertyChanged) self._global_stack.containersChanged.connect(self.startErrorCheck) for extruder in self._global_stack.extruders.values(): - extruder.propertyChanged.connect(self.startErrorCheck) + extruder.propertyChanged.connect(self.startErrorCheckPropertyChanged) extruder.containersChanged.connect(self.startErrorCheck) hasErrorUpdated = pyqtSignal() @@ -93,6 +93,13 @@ class MachineErrorChecker(QObject): def needToWaitForResult(self) -> bool: return self._need_to_check or self._check_in_progress + # Start the error check for property changed + # this is seperate from the startErrorCheck because it ignores a number property types + def startErrorCheckPropertyChanged(self, key, property_name): + if property_name != "value": + return + self.startErrorCheck() + # Starts the error check timer to schedule a new error check. def startErrorCheck(self, *args) -> None: if not self._check_in_progress: From 1436301d780572e976ed0c62edbebee42a404729 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 10 Dec 2018 16:20:00 +0100 Subject: [PATCH 32/76] Ensure setActiveExtruderIndex only gets called once when switching machines CURA-6016 --- cura/Settings/ExtruderManager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index b0bcf3b100..8fa0172305 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -83,8 +83,9 @@ class ExtruderManager(QObject): # \param index The index of the new active extruder. @pyqtSlot(int) def setActiveExtruderIndex(self, index: int) -> None: - self._active_extruder_index = index - self.activeExtruderChanged.emit() + if self._active_extruder_index != index: + self._active_extruder_index = index + self.activeExtruderChanged.emit() @pyqtProperty(int, notify = activeExtruderChanged) def activeExtruderIndex(self) -> int: @@ -344,6 +345,7 @@ class ExtruderManager(QObject): if extruders_changed: self.extrudersChanged.emit(global_stack_id) self.setActiveExtruderIndex(0) + self.activeExtruderChanged.emit() # After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing # "fdmextruder". We need to check a machine here so its extruder definition is correct according to this. From 3132b1f689ad6f83479db5bd19a071e6d47bf74e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 10 Dec 2018 16:56:44 +0100 Subject: [PATCH 33/76] Update the extruder Model a whole lot less CURA-6016 --- cura/Settings/ExtrudersModel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cura/Settings/ExtrudersModel.py b/cura/Settings/ExtrudersModel.py index 5f10ac99d4..e19617c8ef 100644 --- a/cura/Settings/ExtrudersModel.py +++ b/cura/Settings/ExtrudersModel.py @@ -224,6 +224,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel): "definition": "" } items.append(item) - - self.setItems(items) - self.modelChanged.emit() + if self._items != items: + self.setItems(items) + self.modelChanged.emit() From 909f36d28ede29c0c9fc5f69db7f8941fc5a8429 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 11 Dec 2018 09:24:22 +0100 Subject: [PATCH 34/76] Let the settingsMenu use the extruders of the active machine instead of the extruderModel The extruder model gets updated way to much (for all material changes) but we only need the number and names of the extruders, since the other menu's do this by themselves --- cura/Settings/GlobalStack.py | 16 +++++++++++++--- resources/qml/Menus/SettingsMenu.qml | 5 +++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/cura/Settings/GlobalStack.py b/cura/Settings/GlobalStack.py index da1ec61254..44ceee9511 100755 --- a/cura/Settings/GlobalStack.py +++ b/cura/Settings/GlobalStack.py @@ -3,8 +3,8 @@ from collections import defaultdict import threading -from typing import Any, Dict, Optional, Set, TYPE_CHECKING -from PyQt5.QtCore import pyqtProperty, pyqtSlot +from typing import Any, Dict, Optional, Set, TYPE_CHECKING, List +from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal from UM.Decorators import override from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase @@ -42,13 +42,23 @@ class GlobalStack(CuraContainerStack): # Per thread we have our own resolving_settings, or strange things sometimes occur. self._resolving_settings = defaultdict(set) #type: Dict[str, Set[str]] # keys are thread names + extrudersChanged = pyqtSignal() + ## Get the list of extruders of this stack. # # \return The extruders registered with this stack. - @pyqtProperty("QVariantMap") + @pyqtProperty("QVariantMap", notify = extrudersChanged) def extruders(self) -> Dict[str, "ExtruderStack"]: return self._extruders + @pyqtProperty("QVariantList", notify = extrudersChanged) + def extruderList(self) -> List["ExtruderStack"]: + result_tuple_list = sorted(list(self.extruders.items()), key=lambda x: int(x[0])) + result_list = [item[1] for item in result_tuple_list] + + machine_extruder_count = self.getProperty("machine_extruder_count", "value") + return result_list[:machine_extruder_count] + @classmethod def getLoadingPriority(cls) -> int: return 2 diff --git a/resources/qml/Menus/SettingsMenu.qml b/resources/qml/Menus/SettingsMenu.qml index 79f8c5b7bf..4ea3a4d71a 100644 --- a/resources/qml/Menus/SettingsMenu.qml +++ b/resources/qml/Menus/SettingsMenu.qml @@ -16,10 +16,11 @@ Menu Instantiator { - model: Cura.ExtrudersModel { simpleNames: true } + model: Cura.MachineManager.activeMachine.extruderList + Menu { - title: model.name + title: modelData.name NozzleMenu { title: Cura.MachineManager.activeDefinitionVariantsName; visible: Cura.MachineManager.hasVariants; extruderIndex: index } MaterialMenu { title: catalog.i18nc("@title:menu", "&Material"); visible: Cura.MachineManager.hasMaterials; extruderIndex: index } From bb1950525a5e67ae9ee0e7163d720e0d14b72f16 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 11 Dec 2018 09:42:34 +0100 Subject: [PATCH 35/76] Limit the amount of times the buildplate rebuild is done CURA-6016 --- cura/BuildVolume.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index f837f5cef7..aa1f170707 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -83,7 +83,14 @@ class BuildVolume(SceneNode): " with printed models."), title = catalog.i18nc("@info:title", "Build Volume")) self._global_container_stack = None + + self._stack_change_timer = QTimer() + self._stack_change_timer.setInterval(100) + self._stack_change_timer.setSingleShot(True) + self._stack_change_timer.timeout.connect(self._onStackChangeTimerFinished) + self._application.globalContainerStackChanged.connect(self._onStackChanged) + self._onStackChanged() self._engine_ready = False @@ -105,6 +112,8 @@ class BuildVolume(SceneNode): self._setting_change_timer.setSingleShot(True) self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished) + + # Must be after setting _build_volume_message, apparently that is used in getMachineManager. # activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality. # Therefore this works. @@ -526,8 +535,11 @@ class BuildVolume(SceneNode): if extra_z != self._extra_z_clearance: self._extra_z_clearance = extra_z - ## Update the build volume visualization def _onStackChanged(self): + self._stack_change_timer.start() + + ## Update the build volume visualization + def _onStackChangeTimerFinished(self): if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged) extruders = ExtruderManager.getInstance().getActiveExtruderStacks() From badf2962bc90c910fc10b09ecb309b10604d5090 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 11 Dec 2018 10:34:00 +0100 Subject: [PATCH 36/76] 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 37/76] 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 f67ac8d7c4f937a323f8b1a520abfaecad5b438a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 11 Dec 2018 10:59:17 +0100 Subject: [PATCH 38/76] Update far less agressively for the material models CURA-6016 --- cura/Machines/Models/BaseMaterialsModel.py | 14 ++++++++++---- cura/Machines/Models/FavoriteMaterialsModel.py | 5 +---- cura/Machines/Models/GenericMaterialsModel.py | 3 --- cura/Machines/Models/MaterialBrandsModel.py | 4 ---- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/cura/Machines/Models/BaseMaterialsModel.py b/cura/Machines/Models/BaseMaterialsModel.py index ef2e760330..3cd92bb6b4 100644 --- a/cura/Machines/Models/BaseMaterialsModel.py +++ b/cura/Machines/Models/BaseMaterialsModel.py @@ -1,5 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import Optional, Dict, Set from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty from UM.Qt.ListModel import ListModel @@ -9,6 +10,9 @@ from UM.Qt.ListModel import ListModel # Those 2 models are used by the material drop down menu to show generic materials and branded materials separately. # The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top # bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu +from cura.Machines.MaterialNode import MaterialNode + + class BaseMaterialsModel(ListModel): extruderPositionChanged = pyqtSignal() @@ -54,8 +58,8 @@ class BaseMaterialsModel(ListModel): self._extruder_position = 0 self._extruder_stack = None - self._available_materials = None - self._favorite_ids = None + self._available_materials = None # type: Optional[Dict[str, MaterialNode]] + self._favorite_ids = set() # type: Set(str) def _updateExtruderStack(self): global_stack = self._machine_manager.activeMachine @@ -102,8 +106,10 @@ class BaseMaterialsModel(ListModel): return False extruder_stack = global_stack.extruders[extruder_position] - - self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack) + available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack) + if available_materials == self._available_materials: + return False + self._available_materials = available_materials if self._available_materials is None: return False diff --git a/cura/Machines/Models/FavoriteMaterialsModel.py b/cura/Machines/Models/FavoriteMaterialsModel.py index 18fe310c44..cc273e55ce 100644 --- a/cura/Machines/Models/FavoriteMaterialsModel.py +++ b/cura/Machines/Models/FavoriteMaterialsModel.py @@ -4,17 +4,14 @@ from UM.Logger import Logger from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel -class FavoriteMaterialsModel(BaseMaterialsModel): +class FavoriteMaterialsModel(BaseMaterialsModel): def __init__(self, parent = None): super().__init__(parent) self._update() def _update(self): - - # Perform standard check and reset if the check fails if not self._canUpdate(): - self.setItems([]) return # Get updated list of favorites diff --git a/cura/Machines/Models/GenericMaterialsModel.py b/cura/Machines/Models/GenericMaterialsModel.py index c276b865bf..8f41dd6a70 100644 --- a/cura/Machines/Models/GenericMaterialsModel.py +++ b/cura/Machines/Models/GenericMaterialsModel.py @@ -11,10 +11,7 @@ class GenericMaterialsModel(BaseMaterialsModel): self._update() def _update(self): - - # Perform standard check and reset if the check fails if not self._canUpdate(): - self.setItems([]) return # Get updated list of favorites diff --git a/cura/Machines/Models/MaterialBrandsModel.py b/cura/Machines/Models/MaterialBrandsModel.py index 458e4d9b47..ac82cf6670 100644 --- a/cura/Machines/Models/MaterialBrandsModel.py +++ b/cura/Machines/Models/MaterialBrandsModel.py @@ -28,12 +28,8 @@ class MaterialBrandsModel(BaseMaterialsModel): self._update() def _update(self): - - # Perform standard check and reset if the check fails if not self._canUpdate(): - self.setItems([]) return - # Get updated list of favorites self._favorite_ids = self._material_manager.getFavorites() From 76b9fa79986d2622eca7a7316ba65a05aaaa95bd Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 11 Dec 2018 11:53:12 +0100 Subject: [PATCH 39/76] 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 40/76] 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 41/76] 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 6c70543d11bdc37ab658e53686882bcd5a6e4452 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 11 Dec 2018 12:46:11 +0100 Subject: [PATCH 42/76] Only set the containerID when the dialog is visible This prevents unneeded updates CURA-6016 --- resources/qml/Dialogs/WorkspaceSummaryDialog.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/qml/Dialogs/WorkspaceSummaryDialog.qml b/resources/qml/Dialogs/WorkspaceSummaryDialog.qml index 1b3a7aac55..35630bd19b 100644 --- a/resources/qml/Dialogs/WorkspaceSummaryDialog.qml +++ b/resources/qml/Dialogs/WorkspaceSummaryDialog.qml @@ -11,6 +11,7 @@ import Cura 1.0 as Cura UM.Dialog { + id: base title: catalog.i18nc("@title:window", "Save Project") minimumWidth: 500 * screenScaleFactor @@ -49,7 +50,7 @@ UM.Dialog UM.SettingDefinitionsModel { id: definitionsModel - containerId: Cura.MachineManager.activeDefinitionId + containerId: base.visible ? Cura.MachineManager.activeDefinitionId: "" showAll: true exclude: ["command_line_settings"] showAncestors: true From 75ff03f3c8e55c2d2a651da0f36b3d0ee6c0fb8f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 11 Dec 2018 13:13:20 +0100 Subject: [PATCH 43/76] Use setState instead of emitting the backend state CURA-6016 --- .../CuraEngineBackend/CuraEngineBackend.py | 38 +++++++++---------- plugins/CuraEngineBackend/StartSliceJob.py | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 58bc74f3f1..7ede6b6736 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -203,7 +203,7 @@ class CuraEngineBackend(QObject, Backend): @pyqtSlot() def stopSlicing(self) -> None: - self.backendStateChange.emit(BackendState.NotStarted) + self.setState(BackendState.NotStarted) if self._slicing: # We were already slicing. Stop the old job. self._terminate() self._createSocket() @@ -322,7 +322,7 @@ class CuraEngineBackend(QObject, Backend): self._start_slice_job = None if job.isCancelled() or job.getError() or job.getResult() == StartJobResult.Error: - self.backendStateChange.emit(BackendState.Error) + self.setState(BackendState.Error) self.backendError.emit(job) return @@ -331,10 +331,10 @@ class CuraEngineBackend(QObject, Backend): self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() - self.backendStateChange.emit(BackendState.Error) + self.setState(BackendState.Error) self.backendError.emit(job) else: - self.backendStateChange.emit(BackendState.NotStarted) + self.setState(BackendState.NotStarted) return if job.getResult() == StartJobResult.SettingError: @@ -362,10 +362,10 @@ class CuraEngineBackend(QObject, Backend): self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(", ".join(error_labels)), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() - self.backendStateChange.emit(BackendState.Error) + self.setState(BackendState.Error) self.backendError.emit(job) else: - self.backendStateChange.emit(BackendState.NotStarted) + self.setState(BackendState.NotStarted) return elif job.getResult() == StartJobResult.ObjectSettingError: @@ -386,7 +386,7 @@ class CuraEngineBackend(QObject, Backend): self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = ", ".join(errors.values())), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() - self.backendStateChange.emit(BackendState.Error) + self.setState(BackendState.Error) self.backendError.emit(job) return @@ -395,16 +395,16 @@ class CuraEngineBackend(QObject, Backend): self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() - self.backendStateChange.emit(BackendState.Error) + self.setState(BackendState.Error) self.backendError.emit(job) else: - self.backendStateChange.emit(BackendState.NotStarted) + self.setState(BackendState.NotStarted) if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because there are objects associated with disabled Extruder %s." % job.getMessage()), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() - self.backendStateChange.emit(BackendState.Error) + self.setState(BackendState.Error) self.backendError.emit(job) return @@ -413,10 +413,10 @@ class CuraEngineBackend(QObject, Backend): self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() - self.backendStateChange.emit(BackendState.Error) + self.setState(BackendState.Error) self.backendError.emit(job) else: - self.backendStateChange.emit(BackendState.NotStarted) + self.setState(BackendState.NotStarted) self._invokeSlice() return @@ -424,7 +424,7 @@ class CuraEngineBackend(QObject, Backend): self._socket.sendMessage(job.getSliceMessage()) # Notify the user that it's now up to the backend to do it's job - self.backendStateChange.emit(BackendState.Processing) + self.setState(BackendState.Processing) if self._slice_start_time: Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time ) @@ -442,7 +442,7 @@ class CuraEngineBackend(QObject, Backend): for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if node.callDecoration("isBlockSlicing"): enable_timer = False - self.backendStateChange.emit(BackendState.Disabled) + self.setState(BackendState.Disabled) self._is_disabled = True gcode_list = node.callDecoration("getGCodeList") if gcode_list is not None: @@ -451,7 +451,7 @@ class CuraEngineBackend(QObject, Backend): if self._use_timer == enable_timer: return self._use_timer if enable_timer: - self.backendStateChange.emit(BackendState.NotStarted) + self.setState(BackendState.NotStarted) self.enableTimer() return True else: @@ -518,7 +518,7 @@ class CuraEngineBackend(QObject, Backend): self._build_plates_to_be_sliced.append(build_plate_number) self.printDurationMessage.emit(source_build_plate_number, {}, []) self.processingProgress.emit(0.0) - self.backendStateChange.emit(BackendState.NotStarted) + self.setState(BackendState.NotStarted) # if not self._use_timer: # With manually having to slice, we want to clear the old invalid layer data. self._clearLayerData(build_plate_changed) @@ -567,7 +567,7 @@ class CuraEngineBackend(QObject, Backend): self.stopSlicing() self.markSliceAll() self.processingProgress.emit(0.0) - self.backendStateChange.emit(BackendState.NotStarted) + self.setState(BackendState.NotStarted) if not self._use_timer: # With manually having to slice, we want to clear the old invalid layer data. self._clearLayerData() @@ -613,7 +613,7 @@ class CuraEngineBackend(QObject, Backend): # \param message The protobuf message containing the slicing progress. def _onProgressMessage(self, message: Arcus.PythonMessage) -> None: self.processingProgress.emit(message.amount) - self.backendStateChange.emit(BackendState.Processing) + self.setState(BackendState.Processing) def _invokeSlice(self) -> None: if self._use_timer: @@ -632,7 +632,7 @@ class CuraEngineBackend(QObject, Backend): # # \param message The protobuf message signalling that slicing is finished. def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None: - self.backendStateChange.emit(BackendState.Done) + self.setState(BackendState.Done) self.processingProgress.emit(1.0) gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically. diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 9679360ad5..d3882a1209 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -323,7 +323,7 @@ class StartSliceJob(Job): value = stack.getProperty(key, "value") result[key] = value Job.yieldThread() - + result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings. result["print_temperature"] = result["material_print_temperature"] result["time"] = time.strftime("%H:%M:%S") #Some extra settings. From faa3f42acc6f88a63b26443b6edd500061689e93 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 11 Dec 2018 13:53:05 +0100 Subject: [PATCH 44/76] Modify the header in the simulation view menu component This is needed to align with the designs. --- .../SimulationViewMenuComponent.qml | 40 ++++++++++++++----- resources/qml/ViewsSelector.qml | 2 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/plugins/SimulationView/SimulationViewMenuComponent.qml b/plugins/SimulationView/SimulationViewMenuComponent.qml index 58b2bfe520..ed615bf4f6 100644 --- a/plugins/SimulationView/SimulationViewMenuComponent.qml +++ b/plugins/SimulationView/SimulationViewMenuComponent.qml @@ -35,14 +35,36 @@ Cura.ExpandableComponent } } - headerItem: Label + headerItem: Item { - id: layerViewTypesLabel - text: catalog.i18nc("@label", "Color scheme") - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("setting_control_text") - height: base.height - verticalAlignment: Text.AlignVCenter + Label + { + id: colorSchemeLabel + text: catalog.i18nc("@label", "Color scheme") + verticalAlignment: Text.AlignVCenter + height: parent.height + elide: Text.ElideRight + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text_medium") + renderType: Text.NativeRendering + } + + Label + { + text: layerTypeCombobox.currentText + verticalAlignment: Text.AlignVCenter + anchors + { + left: colorSchemeLabel.right + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + } + height: parent.height + elide: Text.ElideRight + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + renderType: Text.NativeRendering + } } contentItem: Column @@ -125,7 +147,7 @@ Cura.ExpandableComponent Label { id: compatibilityModeLabel - text: catalog.i18nc("@label","Compatibility Mode") + text: catalog.i18nc("@label", "Compatibility Mode") font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") visible: UM.SimulationView.compatibilityMode @@ -136,7 +158,7 @@ Cura.ExpandableComponent Item // Spacer { - height: Math.round(UM.Theme.getSize("default_margin").width / 2) + height: UM.Theme.getSize("narrow_margin").width width: width } diff --git a/resources/qml/ViewsSelector.qml b/resources/qml/ViewsSelector.qml index f2906f9d4c..0e2598a0d8 100644 --- a/resources/qml/ViewsSelector.qml +++ b/resources/qml/ViewsSelector.qml @@ -43,7 +43,7 @@ Cura.ExpandablePopup Label { id: title - text: catalog.i18nc("@button", "View types") + text: catalog.i18nc("@label", "View types") verticalAlignment: Text.AlignVCenter height: parent.height elide: Text.ElideRight From db56aa028ca24e92ce8366063066f4fef466b6a6 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 11 Dec 2018 13:59:36 +0100 Subject: [PATCH 45/76] Change the swatch from a circle to the extruder icon --- plugins/SimulationView/SimulationViewMenuComponent.qml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/SimulationView/SimulationViewMenuComponent.qml b/plugins/SimulationView/SimulationViewMenuComponent.qml index ed615bf4f6..c2e9b0d6fa 100644 --- a/plugins/SimulationView/SimulationViewMenuComponent.qml +++ b/plugins/SimulationView/SimulationViewMenuComponent.qml @@ -183,17 +183,16 @@ Cura.ExpandableComponent style: UM.Theme.styles.checkbox - Rectangle + + UM.RecolorImage { + id: swatch anchors.verticalCenter: parent.verticalCenter anchors.right: extrudersModelCheckBox.right width: UM.Theme.getSize("layerview_legend_size").width height: UM.Theme.getSize("layerview_legend_size").height + source: UM.Theme.getIcon("extruder_button") color: model.color - radius: Math.round(width / 2) - border.width: UM.Theme.getSize("default_lining").width - border.color: UM.Theme.getColor("lining") - visible: !viewSettings.show_legend && !viewSettings.show_gradient } Label From 742fc34e75305cbefdb339ef6fa08f239c538f61 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 11 Dec 2018 14:31:12 +0100 Subject: [PATCH 46/76] Change labels in the layer view legend --- plugins/SimulationView/SimulationViewMenuComponent.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/SimulationView/SimulationViewMenuComponent.qml b/plugins/SimulationView/SimulationViewMenuComponent.qml index c2e9b0d6fa..95d7d20006 100644 --- a/plugins/SimulationView/SimulationViewMenuComponent.qml +++ b/plugins/SimulationView/SimulationViewMenuComponent.qml @@ -222,25 +222,25 @@ Cura.ExpandableComponent Component.onCompleted: { typesLegendModel.append({ - label: catalog.i18nc("@label", "Show Travels"), + label: catalog.i18nc("@label", "Travels"), initialValue: viewSettings.show_travel_moves, preference: "layerview/show_travel_moves", colorId: "layerview_move_combing" }); typesLegendModel.append({ - label: catalog.i18nc("@label", "Show Helpers"), + label: catalog.i18nc("@label", "Helpers"), initialValue: viewSettings.show_helpers, preference: "layerview/show_helpers", colorId: "layerview_support" }); typesLegendModel.append({ - label: catalog.i18nc("@label", "Show Shell"), + label: catalog.i18nc("@label", "Shell"), initialValue: viewSettings.show_skin, preference: "layerview/show_skin", colorId: "layerview_inset_0" }); typesLegendModel.append({ - label: catalog.i18nc("@label", "Show Infill"), + label: catalog.i18nc("@label", "Infill"), initialValue: viewSettings.show_infill, preference: "layerview/show_infill", colorId: "layerview_infill" From c804610fa6ab8cad1b52b34666200c2026ef3732 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 11 Dec 2018 15:19:40 +0100 Subject: [PATCH 47/76] Make the stage menu in the preview using 85 percent of the total width in order to prevent the print setup panel to be in the middle --- plugins/PreviewStage/PreviewMenu.qml | 92 +++++++++++-------- .../SimulationViewMenuComponent.qml | 1 - resources/themes/cura-light/theme.json | 4 +- 3 files changed, 54 insertions(+), 43 deletions(-) diff --git a/plugins/PreviewStage/PreviewMenu.qml b/plugins/PreviewStage/PreviewMenu.qml index 1543536160..4e1fbac837 100644 --- a/plugins/PreviewStage/PreviewMenu.qml +++ b/plugins/PreviewStage/PreviewMenu.qml @@ -2,6 +2,7 @@ // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.7 +import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 import UM 1.3 as UM @@ -19,56 +20,67 @@ Item name: "cura" } - - Row + // Item to ensure that all of the buttons are nicely centered. + Item { - id: stageMenuRow - anchors.centerIn: parent + anchors.horizontalCenter: parent.horizontalCenter + width: stageMenuRow.width height: parent.height - Cura.ViewsSelector + RowLayout { - id: viewsSelector + id: stageMenuRow + width: Math.round(0.85 * previewMenu.width) height: parent.height - width: UM.Theme.getSize("views_selector").width - headerCornerSide: Cura.RoundedRectangle.Direction.Left - } + spacing: 0 - // Separator line - Rectangle - { - height: parent.height - // If there is no viewPanel, we only need a single spacer, so hide this one. - visible: viewPanel.source != "" - width: visible ? UM.Theme.getSize("default_lining").width : 0 + Cura.ViewsSelector + { + id: viewsSelector + headerCornerSide: Cura.RoundedRectangle.Direction.Left + Layout.minimumWidth: UM.Theme.getSize("views_selector").width + Layout.maximumWidth: UM.Theme.getSize("views_selector").width + Layout.fillWidth: true + Layout.fillHeight: true + } - color: UM.Theme.getColor("lining") - } + // Separator line + Rectangle + { + height: parent.height + // If there is no viewPanel, we only need a single spacer, so hide this one. + visible: viewPanel.source != "" + width: UM.Theme.getSize("default_lining").width - Loader - { - id: viewPanel - height: parent.height - width: childrenRect.width - source: UM.Controller.activeView != null && UM.Controller.activeView.stageMenuComponent != null ? UM.Controller.activeView.stageMenuComponent : "" - } + color: UM.Theme.getColor("lining") + } - // Separator line - Rectangle - { - height: parent.height - width: UM.Theme.getSize("default_lining").width - color: UM.Theme.getColor("lining") - } + Loader + { + id: viewPanel + Layout.fillHeight: true + Layout.fillWidth: true + Layout.preferredWidth: stageMenuRow.width - viewsSelector.width - printSetupSelectorItem.width - 2 * UM.Theme.getSize("default_lining").width + source: UM.Controller.activeView != null && UM.Controller.activeView.stageMenuComponent != null ? UM.Controller.activeView.stageMenuComponent : "" + } - Item - { - id: printSetupSelectorItem - // This is a work around to prevent the printSetupSelector from having to be re-loaded every time - // a stage switch is done. - children: [printSetupSelector] - height: childrenRect.height - width: childrenRect.width + // Separator line + Rectangle + { + height: parent.height + width: UM.Theme.getSize("default_lining").width + color: UM.Theme.getColor("lining") + } + + Item + { + id: printSetupSelectorItem + // This is a work around to prevent the printSetupSelector from having to be re-loaded every time + // a stage switch is done. + children: [printSetupSelector] + height: childrenRect.height + width: childrenRect.width + } } } } diff --git a/plugins/SimulationView/SimulationViewMenuComponent.qml b/plugins/SimulationView/SimulationViewMenuComponent.qml index 95d7d20006..9f43252126 100644 --- a/plugins/SimulationView/SimulationViewMenuComponent.qml +++ b/plugins/SimulationView/SimulationViewMenuComponent.qml @@ -15,7 +15,6 @@ Cura.ExpandableComponent { id: base - width: UM.Theme.getSize("layerview_menu_size").width contentHeaderTitle: catalog.i18nc("@label", "Color scheme") Connections diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 3dc216ad70..0c17a34e9a 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -380,7 +380,7 @@ "machine_selector_widget_content": [25.0, 32.0], "machine_selector_icon": [2.66, 2.66], - "views_selector": [16.0, 4.5], + "views_selector": [23.0, 4.0], "printer_type_label": [3.5, 1.5], @@ -450,7 +450,7 @@ "slider_handle": [1.5, 1.5], "slider_layerview_size": [1.0, 26.0], - "layerview_menu_size": [15, 20], + "layerview_menu_size": [16.0, 4.0], "layerview_legend_size": [1.0, 1.0], "layerview_row": [11.0, 1.5], "layerview_row_spacing": [0.0, 0.5], From daeb86bdb1ec55400b86edc14ec76c66cba4dfbd Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Tue, 11 Dec 2018 15:55:03 +0100 Subject: [PATCH 48/76] Use short printer family tags Contributes to CL-1173 --- .../resources/qml/MonitorPrintJobCard.qml | 1 + .../resources/qml/MonitorPrinterPill.qml | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml index 5eaeff2e84..d8c5d1ec28 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml @@ -97,6 +97,7 @@ Item return "" } visible: printJob + width: 120 * screenScaleFactor // TODO: Theme! // FIXED-LINE-HEIGHT: height: 18 * screenScaleFactor // TODO: Theme! diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml index cd78f1b11f..94d9c7e7d0 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml @@ -12,7 +12,19 @@ import UM 1.2 as UM Item { // The printer name - property alias text: printerNameLabel.text; + property var text: "" + property var tagText: { + switch(text) { + case "Ultimaker 3": + return "UM 3" + case "Ultimaker 3 Extended": + return "UM 3 EXT" + case "Ultimaker S5": + return "UM S5" + default: + return "" + } + } implicitHeight: 18 * screenScaleFactor // TODO: Theme! implicitWidth: printerNameLabel.contentWidth + 12 // TODO: Theme! @@ -28,7 +40,7 @@ Item id: printerNameLabel anchors.centerIn: parent color: "#535369" // TODO: Theme! - text: "" + text: tagText font.pointSize: 10 } } \ No newline at end of file From 1ababf38ad1cd50946ebca15a72e971e949223a7 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Tue, 11 Dec 2018 16:42:44 +0100 Subject: [PATCH 49/76] Use full printer name if no tag is known Contributes to CL-1173 --- plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml index 94d9c7e7d0..80a089cc2a 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml @@ -22,7 +22,7 @@ Item case "Ultimaker S5": return "UM S5" default: - return "" + return text } } From b413b4cdb69fd3f6c46f93c3797c92bd9abc53b8 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 11 Dec 2018 17:21:14 +0100 Subject: [PATCH 50/76] Correct a typo in typing. [CURA-6016] --- cura/Machines/Models/BaseMaterialsModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Machines/Models/BaseMaterialsModel.py b/cura/Machines/Models/BaseMaterialsModel.py index 3cd92bb6b4..629e5c2b48 100644 --- a/cura/Machines/Models/BaseMaterialsModel.py +++ b/cura/Machines/Models/BaseMaterialsModel.py @@ -59,7 +59,7 @@ class BaseMaterialsModel(ListModel): self._extruder_stack = None self._available_materials = None # type: Optional[Dict[str, MaterialNode]] - self._favorite_ids = set() # type: Set(str) + self._favorite_ids = set() # type: Set[str] def _updateExtruderStack(self): global_stack = self._machine_manager.activeMachine From 0b6841e5d9ba0af192866c1dbe0cec21c14debe1 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 12 Dec 2018 09:19:43 +0100 Subject: [PATCH 51/76] 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 8c07a6e89b97c6a545720e481a606f4f2a7e29f7 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 12 Dec 2018 10:35:36 +0100 Subject: [PATCH 52/76] 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 53/76] 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 54/76] 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 55/76] 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 a1d7fa893da138f241fd1cc74895ec8e5d928eb6 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 12 Dec 2018 17:26:13 +0100 Subject: [PATCH 56/76] Change the style for the toolbuttons that appear in the popup panel of some tools Contributes to CURA-6024 --- resources/themes/cura-light/styles.qml | 94 ++++++-------------------- resources/themes/cura-light/theme.json | 2 - 2 files changed, 20 insertions(+), 76 deletions(-) diff --git a/resources/themes/cura-light/styles.qml b/resources/themes/cura-light/styles.qml index bcc754f4ca..c940052668 100755 --- a/resources/themes/cura-light/styles.qml +++ b/resources/themes/cura-light/styles.qml @@ -177,8 +177,8 @@ QtObject { background: Item { - implicitWidth: Theme.getSize("button").width; - implicitHeight: Theme.getSize("button").height; + implicitWidth: Theme.getSize("button").width + implicitHeight: Theme.getSize("button").height UM.PointingRectangle { @@ -205,20 +205,20 @@ QtObject id: button_tip anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter; + anchors.verticalCenter: parent.verticalCenter text: control.text; - font: Theme.getFont("button_tooltip"); - color: Theme.getColor("tooltip_text"); + font: Theme.getFont("button_tooltip") + color: Theme.getColor("tooltip_text") } } Rectangle { - id: buttonFace; + id: buttonFace - anchors.fill: parent; - property bool down: control.pressed || (control.checkable && control.checked); + anchors.fill: parent + property bool down: control.pressed || (control.checkable && control.checked) color: { @@ -228,58 +228,22 @@ QtObject } else if(control.checkable && control.checked && control.hovered) { - return Theme.getColor("button_active_hover"); + return Theme.getColor("toolbar_button_active_hover") } else if(control.pressed || (control.checkable && control.checked)) { - return Theme.getColor("button_active"); + return Theme.getColor("toolbar_button_active") } else if(control.hovered) { - return Theme.getColor("button_hover"); - } - else - { - return Theme.getColor("button"); + return Theme.getColor("toolbar_button_hover") } + return Theme.getColor("toolbar_background") } Behavior on color { ColorAnimation { duration: 50; } } - border.width: (control.hasOwnProperty("needBorder") && control.needBorder) ? 2 * screenScaleFactor : 0 - border.color: Theme.getColor("tool_button_border") - - UM.RecolorImage - { - id: tool_button_arrow - anchors.right: parent.right; - anchors.rightMargin: Theme.getSize("button").width - Math.round(Theme.getSize("button_icon").width / 4) - anchors.bottom: parent.bottom; - anchors.bottomMargin: Theme.getSize("button").height - Math.round(Theme.getSize("button_icon").height / 4) - width: Theme.getSize("standard_arrow").width - height: Theme.getSize("standard_arrow").height - sourceSize.height: width - visible: control.menu != null; - color: - { - if(control.checkable && control.checked && control.hovered) - { - return Theme.getColor("button_text_active_hover"); - } - else if(control.pressed || (control.checkable && control.checked)) - { - return Theme.getColor("button_text_active"); - } - else if(control.hovered) - { - return Theme.getColor("button_text_hover"); - } - else - { - return Theme.getColor("button_text"); - } - } - source: Theme.getIcon("arrow_bottom") - } + border.width: (control.hasOwnProperty("needBorder") && control.needBorder) ? Theme.getSize("default_lining").width : 0 + border.color: Theme.getColor("lining") } } @@ -287,30 +251,12 @@ QtObject { UM.RecolorImage { - anchors.centerIn: parent; - opacity: !control.enabled ? 0.2 : 1.0 - source: control.iconSource; - width: Theme.getSize("button_icon").width; - height: Theme.getSize("button_icon").height; - color: - { - if(control.checkable && control.checked && control.hovered) - { - return Theme.getColor("button_text_active_hover"); - } - else if(control.pressed || (control.checkable && control.checked)) - { - return Theme.getColor("button_text_active"); - } - else if(control.hovered) - { - return Theme.getColor("button_text_hover"); - } - else - { - return Theme.getColor("button_text"); - } - } + anchors.centerIn: parent + opacity: control.enabled ? 1.0 : 0.2 + source: control.iconSource + width: Theme.getSize("button_icon").width + height: Theme.getSize("button_icon").height + color: Theme.getColor("toolbar_button_text") sourceSize: Theme.getSize("button_icon") } diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 3dc216ad70..59596e9a3f 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -243,8 +243,6 @@ "tooltip": [68, 192, 255, 255], "tooltip_text": [255, 255, 255, 255], - "tool_button_border": [255, 255, 255, 0], - "message_background": [255, 255, 255, 255], "message_shadow": [0, 0, 0, 120], "message_border": [192, 193, 194, 255], From e7fe7571aafdb89f24af2a687ca13e067b07d425 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 13 Dec 2018 09:02:14 +0100 Subject: [PATCH 57/76] Change the behaviour of the output device selector Now the behavior is the following: - The active output device is the one that shows up in the button (same as before) - The list of the output devices don't show the active device - When clicking in one item of the list, it becomes the active output and automatically it performs the action. Contributes to CURA-6026. --- .../qml/ActionPanel/OutputDevicesActionButton.qml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/resources/qml/ActionPanel/OutputDevicesActionButton.qml b/resources/qml/ActionPanel/OutputDevicesActionButton.qml index 9a6c97bcff..b56f50b9a9 100644 --- a/resources/qml/ActionPanel/OutputDevicesActionButton.qml +++ b/resources/qml/ActionPanel/OutputDevicesActionButton.qml @@ -12,6 +12,12 @@ Item { id: widget + function requestWriteToDevice() + { + UM.OutputDeviceManager.requestWriteToDevice(UM.OutputDeviceManager.activeDevice, PrintInformation.jobName, + { "filter_by_machine": true, "preferred_mimetypes": Cura.MachineManager.activeMachine.preferred_output_file_formats }); + } + Cura.PrimaryButton { id: saveToButton @@ -32,9 +38,8 @@ Item onClicked: { - forceActiveFocus(); - UM.OutputDeviceManager.requestWriteToDevice(UM.OutputDeviceManager.activeDevice, PrintInformation.jobName, - { "filter_by_machine": true, "preferred_mimetypes": Cura.MachineManager.activeMachine.preferred_output_file_formats }); + forceActiveFocus() + widget.requestWriteToDevice() } } @@ -81,6 +86,7 @@ Item delegate: Cura.ActionButton { text: model.description + visible: model.id != UM.OutputDeviceManager.activeDevice // Don't show the active device in the list color: "transparent" cornerRadius: 0 hoverColor: UM.Theme.getColor("primary") @@ -88,6 +94,7 @@ Item onClicked: { UM.OutputDeviceManager.setActiveDevice(model.id) + widget.requestWriteToDevice() popup.close() } } From 121af7ad6085096bb9c8bbae4643516de342eb93 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 13 Dec 2018 09:41:07 +0100 Subject: [PATCH 58/76] Reuse the primary button in the toolbox for the "Quit Cura" button --- .../Toolbox/resources/qml/ToolboxFooter.qml | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxFooter.qml b/plugins/Toolbox/resources/qml/ToolboxFooter.qml index 2d42ca7269..6f46e939ff 100644 --- a/plugins/Toolbox/resources/qml/ToolboxFooter.qml +++ b/plugins/Toolbox/resources/qml/ToolboxFooter.qml @@ -2,21 +2,23 @@ // Toolbox is released under the terms of the LGPLv3 or higher. import QtQuick 2.10 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 +import QtQuick.Controls 2.3 + import UM 1.1 as UM +import Cura 1.0 as Cura Item { id: footer width: parent.width anchors.bottom: parent.bottom - height: visible ? Math.floor(UM.Theme.getSize("toolbox_footer").height) : 0 + height: visible ? UM.Theme.getSize("toolbox_footer").height : 0 + Label { text: catalog.i18nc("@info", "You will need to restart Cura before changes in packages have effect.") color: UM.Theme.getColor("text") - height: Math.floor(UM.Theme.getSize("toolbox_footer_button").height) + height: UM.Theme.getSize("toolbox_footer_button").height verticalAlignment: Text.AlignVCenter anchors { @@ -28,10 +30,10 @@ Item } renderType: Text.NativeRendering } - Button + + Cura.PrimaryButton { id: restartButton - text: catalog.i18nc("@info:button", "Quit Cura") anchors { top: parent.top @@ -39,27 +41,11 @@ Item right: parent.right rightMargin: UM.Theme.getSize("wide_margin").width } - iconName: "dialog-restart" + height: UM.Theme.getSize("toolbox_footer_button").height + text: catalog.i18nc("@info:button", "Quit Cura") onClicked: toolbox.restart() - style: ButtonStyle - { - background: Rectangle - { - implicitWidth: UM.Theme.getSize("toolbox_footer_button").width - implicitHeight: Math.floor(UM.Theme.getSize("toolbox_footer_button").height) - color: control.hovered ? UM.Theme.getColor("primary_hover") : UM.Theme.getColor("primary") - } - label: Label - { - color: UM.Theme.getColor("button_text") - font: UM.Theme.getFont("default_bold") - text: control.text - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - renderType: Text.NativeRendering - } - } } + ToolboxShadow { visible: footer.visible From f72b58386b39d1140ffdc489127eb396604afb77 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 13 Dec 2018 11:46:11 +0100 Subject: [PATCH 59/76] 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 From 819f8531a21f68ebf47f18c347043f30117dbf35 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 13 Dec 2018 11:54:10 +0100 Subject: [PATCH 60/76] Use CuraConstants for Cloud printing API root --- plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index b08bac6670..2dd0c84442 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -7,6 +7,7 @@ from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from UM.Logger import Logger +from cura import CuraConstants from cura.API import Account from cura.NetworkClient import NetworkClient from ..Models import BaseModel @@ -23,8 +24,7 @@ from .Models.CloudPrintJobResponse import CloudPrintJobResponse class CloudApiClient(NetworkClient): # The cloud URL to use for this remote cluster. - # TODO: Make sure that this URL goes to the live api before release - ROOT_PATH = "https://api-staging.ultimaker.com" + ROOT_PATH = CuraConstants.CuraCloudAPIRoot CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH) CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH) From 0a6c1a7c14e2d67ec8879ec2dc24e2acbade0c1d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 13 Dec 2018 13:12:57 +0100 Subject: [PATCH 61/76] Fix CLIENT_ID (again) It got messed up in a merge again. Ugh. --- cura/API/Account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index 397e220478..be77a6307b 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -44,7 +44,7 @@ class Account(QObject): 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_ID="um----------------------------ultimaker_cura", CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.write", AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data", AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root), From a1a9b058f5e874f0ab41871afc94a7ac712badd0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 13 Dec 2018 14:28:32 +0100 Subject: [PATCH 62/76] Let header listen to activeStageId instead of the model CURA-6028 --- resources/qml/MainWindow/MainWindowHeader.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/qml/MainWindow/MainWindowHeader.qml b/resources/qml/MainWindow/MainWindowHeader.qml index ae1c13d9c3..6cb7370ec2 100644 --- a/resources/qml/MainWindow/MainWindowHeader.qml +++ b/resources/qml/MainWindow/MainWindowHeader.qml @@ -54,7 +54,8 @@ Item { text: model.name.toUpperCase() checkable: true - checked: model.active + checked: model.id == UM.Controller.activeStage.stageId + anchors.verticalCenter: parent.verticalCenter exclusiveGroup: mainWindowHeaderMenuGroup style: UM.Theme.styles.main_window_header_tab From c4700b27522dafa6d2164ea08b2215e1f738e013 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 13 Dec 2018 14:30:00 +0100 Subject: [PATCH 63/76] Also update the model if the data changed CURA-6028 --- .../Recommended/RecommendedQualityProfileSelector.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml index 15d40f545a..349c6dbb57 100644 --- a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml +++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml @@ -39,6 +39,7 @@ Item { target: Cura.QualityProfilesDropDownMenuModel onItemsChanged: qualityModel.update() + onDataChanged: qualityModel.update() } Connections { From 17f0c12858ae2a3cf6466e8e5bc900338d22f444 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 13 Dec 2018 14:31:34 +0100 Subject: [PATCH 64/76] Make the middle component to have zero width if there is no component to fill the gap. Contributes to CURA-6020. --- plugins/PreviewStage/PreviewMenu.qml | 95 +++++++++++++--------------- 1 file changed, 44 insertions(+), 51 deletions(-) diff --git a/plugins/PreviewStage/PreviewMenu.qml b/plugins/PreviewStage/PreviewMenu.qml index 4e1fbac837..62f814aac9 100644 --- a/plugins/PreviewStage/PreviewMenu.qml +++ b/plugins/PreviewStage/PreviewMenu.qml @@ -20,67 +20,60 @@ Item name: "cura" } - // Item to ensure that all of the buttons are nicely centered. - Item + Row { - anchors.horizontalCenter: parent.horizontalCenter - width: stageMenuRow.width + id: stageMenuRow + anchors.centerIn: parent height: parent.height + width: childrenRect.width - RowLayout + // We want this row to have a preferred with equals to the 85% of the parent + property int preferredWidth: Math.round(0.85 * previewMenu.width) + + Cura.ViewsSelector { - id: stageMenuRow - width: Math.round(0.85 * previewMenu.width) + id: viewsSelector height: parent.height - spacing: 0 + width: UM.Theme.getSize("views_selector").width + headerCornerSide: Cura.RoundedRectangle.Direction.Left + } - Cura.ViewsSelector - { - id: viewsSelector - headerCornerSide: Cura.RoundedRectangle.Direction.Left - Layout.minimumWidth: UM.Theme.getSize("views_selector").width - Layout.maximumWidth: UM.Theme.getSize("views_selector").width - Layout.fillWidth: true - Layout.fillHeight: true - } + // Separator line + Rectangle + { + height: parent.height + // If there is no viewPanel, we only need a single spacer, so hide this one. + visible: viewPanel.source != "" + width: visible ? UM.Theme.getSize("default_lining").width : 0 - // Separator line - Rectangle - { - height: parent.height - // If there is no viewPanel, we only need a single spacer, so hide this one. - visible: viewPanel.source != "" - width: UM.Theme.getSize("default_lining").width + color: UM.Theme.getColor("lining") + } - color: UM.Theme.getColor("lining") - } + // This component will grow freely up to complete the preferredWidth of the row. + Loader + { + id: viewPanel + height: parent.height + width: source != "" ? (stageMenuRow.preferredWidth - viewsSelector.width - printSetupSelectorItem.width - 2 * UM.Theme.getSize("default_lining").width) : 0 + source: UM.Controller.activeView != null && UM.Controller.activeView.stageMenuComponent != null ? UM.Controller.activeView.stageMenuComponent : "" + } - Loader - { - id: viewPanel - Layout.fillHeight: true - Layout.fillWidth: true - Layout.preferredWidth: stageMenuRow.width - viewsSelector.width - printSetupSelectorItem.width - 2 * UM.Theme.getSize("default_lining").width - source: UM.Controller.activeView != null && UM.Controller.activeView.stageMenuComponent != null ? UM.Controller.activeView.stageMenuComponent : "" - } + // Separator line + Rectangle + { + height: parent.height + width: UM.Theme.getSize("default_lining").width + color: UM.Theme.getColor("lining") + } - // Separator line - Rectangle - { - height: parent.height - width: UM.Theme.getSize("default_lining").width - color: UM.Theme.getColor("lining") - } - - Item - { - id: printSetupSelectorItem - // This is a work around to prevent the printSetupSelector from having to be re-loaded every time - // a stage switch is done. - children: [printSetupSelector] - height: childrenRect.height - width: childrenRect.width - } + Item + { + id: printSetupSelectorItem + // This is a work around to prevent the printSetupSelector from having to be re-loaded every time + // a stage switch is done. + children: [printSetupSelector] + height: childrenRect.height + width: childrenRect.width } } } From b48eedfb805fd8ed6aa3accb4658622dcd57ef43 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 13 Dec 2018 15:15:22 +0100 Subject: [PATCH 65/76] Modify color for the progress bar in the messages. --- resources/themes/cura-light/theme.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 1a7d377fa7..6d76004739 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -255,8 +255,8 @@ "message_button_text": [255, 255, 255, 255], "message_button_text_hover": [255, 255, 255, 255], "message_button_text_active": [255, 255, 255, 255], - "message_progressbar_background": [200, 200, 200, 255], - "message_progressbar_control": [77, 182, 226, 255], + "message_progressbar_background": [245, 245, 245, 255], + "message_progressbar_control": [50, 130, 255, 255], "tool_panel_background": [255, 255, 255, 255], From 1b3cb323344cd6c75ec160ca936a1aff379ad64f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 13 Dec 2018 15:59:15 +0100 Subject: [PATCH 66/76] Fix the simulation view not being selected as default CURA-6028 --- resources/qml/ViewsSelector.qml | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/resources/qml/ViewsSelector.qml b/resources/qml/ViewsSelector.qml index 0e2598a0d8..d91412cad0 100644 --- a/resources/qml/ViewsSelector.qml +++ b/resources/qml/ViewsSelector.qml @@ -14,24 +14,31 @@ Cura.ExpandablePopup contentPadding: UM.Theme.getSize("default_lining").width contentAlignment: Cura.ExpandablePopup.ContentAlignment.AlignLeft - property var viewModel: UM.ViewModel { } - - property var activeView: + property var viewModel: UM.ViewModel { - for (var i = 0; i < viewModel.count; i++) + onDataChanged: updateActiveView() + } + + + property var activeView: null + + function updateActiveView() + { + for (var index in viewModel.items) { - if (viewModel.items[i].active) + + if (viewModel.items[index].active) { - return viewModel.items[i] + activeView = viewModel.items[index] + return } } - return null + activeView = null } Component.onCompleted: { - // Nothing was active, so just return the first one (the list is sorted by priority, so the most - // important one should be returned) + updateActiveView() if (activeView == null) { UM.Controller.setActiveView(viewModel.getItem(0).id) From d48e89e22432ae9146c404a745d785af9491da37 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 13 Dec 2018 16:32:48 +0100 Subject: [PATCH 67/76] Prevent the parent of printSetupSelector from being set to null If it's parent gets set to null it will break the focus, which we need for the binding updates. CURA-5941 --- resources/qml/Cura.qml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index a4faa27b67..cf6f36a492 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -278,6 +278,14 @@ UM.MainWindow height: UM.Theme.getSize("stage_menu").height source: UM.Controller.activeStage != null ? UM.Controller.activeStage.stageMenuComponent : "" + + // HACK: This is to ensure that the parent never gets set to null, as this wreaks havoc on the focus. + function onParentDestroyed() + { + printSetupSelector.parent = stageMenu + printSetupSelector.visible = false + } + // The printSetupSelector is defined here so that the setting list doesn't need to get re-instantiated // Every time the stage is changed. property var printSetupSelector: Cura.PrintSetupSelector @@ -285,6 +293,11 @@ UM.MainWindow width: UM.Theme.getSize("print_setup_widget").width height: UM.Theme.getSize("stage_menu").height headerCornerSide: RoundedRectangle.Direction.Right + onParentChanged: + { + visible = parent != stageMenu + parent.Component.destruction.connect(stageMenu.onParentDestroyed) + } } } From 3f3cd5e33443ebe1f01671f5c08e3397c85fd962 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 13 Dec 2018 16:32:27 +0100 Subject: [PATCH 68/76] Remove broken load of PrepareSidebar.qml This file no longer exists. Loading it gives a warning. --- cura/Stages/CuraStage.py | 4 ---- plugins/PrepareStage/PrepareStage.py | 6 +----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/cura/Stages/CuraStage.py b/cura/Stages/CuraStage.py index e8537fb6b9..844b0d0768 100644 --- a/cura/Stages/CuraStage.py +++ b/cura/Stages/CuraStage.py @@ -24,10 +24,6 @@ class CuraStage(Stage): def mainComponent(self) -> QUrl: return self.getDisplayComponent("main") - @pyqtProperty(QUrl, constant = True) - def sidebarComponent(self) -> QUrl: - return self.getDisplayComponent("sidebar") - @pyqtProperty(QUrl, constant = True) def stageMenuComponent(self) -> QUrl: return self.getDisplayComponent("menu") \ No newline at end of file diff --git a/plugins/PrepareStage/PrepareStage.py b/plugins/PrepareStage/PrepareStage.py index b22f3385b8..b0f862dc48 100644 --- a/plugins/PrepareStage/PrepareStage.py +++ b/plugins/PrepareStage/PrepareStage.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os.path from UM.Application import Application @@ -15,9 +15,5 @@ class PrepareStage(CuraStage): Application.getInstance().engineCreatedSignal.connect(self._engineCreated) def _engineCreated(self): - sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), - "PrepareSidebar.qml") - menu_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMenu.qml") self.addDisplayComponent("menu", menu_component_path) - self.addDisplayComponent("sidebar", sidebar_component_path) From 012ee0c02a66ef4e1bbbc5dde6aee8d8fec9988a Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 13 Dec 2018 16:44:49 +0100 Subject: [PATCH 69/76] Use the MouseArea trick to assure that the binding still works for the checked property of the button. Contributes to CURA-6028. --- resources/qml/MainWindow/MainWindowHeader.qml | 8 +++++++- resources/qml/ViewsSelector.qml | 3 --- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/resources/qml/MainWindow/MainWindowHeader.qml b/resources/qml/MainWindow/MainWindowHeader.qml index 6cb7370ec2..793df42da0 100644 --- a/resources/qml/MainWindow/MainWindowHeader.qml +++ b/resources/qml/MainWindow/MainWindowHeader.qml @@ -60,11 +60,17 @@ Item exclusiveGroup: mainWindowHeaderMenuGroup style: UM.Theme.styles.main_window_header_tab height: UM.Theme.getSize("main_window_header_button").height - onClicked: UM.Controller.setActiveStage(model.id) iconSource: model.stage.iconSource property color overlayColor: "transparent" property string overlayIconSource: "" + + // This is a trick to assure the activeStage is correctly changed. It doesn't work propertly if done in the onClicked (see CURA-6028) + MouseArea + { + anchors.fill: parent + onClicked: UM.Controller.setActiveStage(model.id) + } } } diff --git a/resources/qml/ViewsSelector.qml b/resources/qml/ViewsSelector.qml index d91412cad0..06d2e662b5 100644 --- a/resources/qml/ViewsSelector.qml +++ b/resources/qml/ViewsSelector.qml @@ -19,14 +19,12 @@ Cura.ExpandablePopup onDataChanged: updateActiveView() } - property var activeView: null function updateActiveView() { for (var index in viewModel.items) { - if (viewModel.items[index].active) { activeView = viewModel.items[index] @@ -38,7 +36,6 @@ Cura.ExpandablePopup Component.onCompleted: { - updateActiveView() if (activeView == null) { UM.Controller.setActiveView(viewModel.getItem(0).id) From 3d4da94b36593505a4e86f41e46999f9660337f1 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 13 Dec 2018 16:49:27 +0100 Subject: [PATCH 70/76] Remove obsolete description detail about old Cura versions This setting had a detail in the description about how the setting used to behave in old Cura versions. Let's remove it since it is not relevant any more. Fixes #4536. --- resources/definitions/fdmprinter.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index c015ab8ccb..f39e267354 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3385,7 +3385,7 @@ "retraction_combing": { "label": "Combing Mode", - "description": "Combing keeps the nozzle within already printed areas when traveling. This results in slightly longer travel moves but reduces the need for retractions. If combing is off, the material will retract and the nozzle moves in a straight line to the next point. It is also possible to avoid combing over top/bottom skin areas and also to only comb within the infill. Note that the 'Within Infill' option behaves exactly like the 'Not in Skin' option in earlier Cura releases.", + "description": "Combing keeps the nozzle within already printed areas when traveling. This results in slightly longer travel moves but reduces the need for retractions. If combing is off, the material will retract and the nozzle moves in a straight line to the next point. It is also possible to avoid combing over top/bottom skin areas or to only comb within the infill.", "type": "enum", "options": { From 473160a102c902bd4161d3b7d4027414a71c144a Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 13 Dec 2018 17:02:13 +0100 Subject: [PATCH 71/76] Add a check for null to avoid warnings in the logs. --- resources/qml/MainWindow/MainWindowHeader.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/MainWindow/MainWindowHeader.qml b/resources/qml/MainWindow/MainWindowHeader.qml index 793df42da0..8f6957d9cd 100644 --- a/resources/qml/MainWindow/MainWindowHeader.qml +++ b/resources/qml/MainWindow/MainWindowHeader.qml @@ -54,7 +54,7 @@ Item { text: model.name.toUpperCase() checkable: true - checked: model.id == UM.Controller.activeStage.stageId + checked: UM.Controller.activeStage != null ? model.id == UM.Controller.activeStage.stageId : false anchors.verticalCenter: parent.verticalCenter exclusiveGroup: mainWindowHeaderMenuGroup From 856c1dcb20d42a624bfc367ca135042d7404953f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 13 Dec 2018 17:21:07 +0100 Subject: [PATCH 72/76] Also disconnect the signal for the old parent of printSetupSelector --- resources/qml/Cura.qml | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index cf6f36a492..ff5a603fda 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -278,29 +278,33 @@ UM.MainWindow height: UM.Theme.getSize("stage_menu").height source: UM.Controller.activeStage != null ? UM.Controller.activeStage.stageMenuComponent : "" - // HACK: This is to ensure that the parent never gets set to null, as this wreaks havoc on the focus. function onParentDestroyed() { printSetupSelector.parent = stageMenu printSetupSelector.visible = false } + property item oldParent: null // The printSetupSelector is defined here so that the setting list doesn't need to get re-instantiated // Every time the stage is changed. property var printSetupSelector: Cura.PrintSetupSelector { - width: UM.Theme.getSize("print_setup_widget").width - height: UM.Theme.getSize("stage_menu").height - headerCornerSide: RoundedRectangle.Direction.Right - onParentChanged: - { - visible = parent != stageMenu - parent.Component.destruction.connect(stageMenu.onParentDestroyed) - } + width: UM.Theme.getSize("print_setup_widget").width + height: UM.Theme.getSize("stage_menu").height + headerCornerSide: RoundedRectangle.Direction.Right + onParentChanged: + { + if(stageMenu.oldParent !=null) + { + stageMenu.oldParent.Component.destruction.disconnect(stageMenu.onParentDestroyed) + } + stageMenu.oldParent = parent + visible = parent != stageMenu + parent.Component.destruction.connect(stageMenu.onParentDestroyed) + } } } - UM.MessageStack { anchors From 8dc2a41fb7af876a2f4c041fd68482f131b4385c Mon Sep 17 00:00:00 2001 From: Aleksei S Date: Thu, 13 Dec 2018 17:27:16 +0100 Subject: [PATCH 73/76] Typo, wrong item type, "item" CURA-5941 --- resources/qml/Cura.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index ff5a603fda..2b6f989e0b 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -284,7 +284,7 @@ UM.MainWindow printSetupSelector.parent = stageMenu printSetupSelector.visible = false } - property item oldParent: null + property Item oldParent: null // The printSetupSelector is defined here so that the setting list doesn't need to get re-instantiated // Every time the stage is changed. From 15cd2a926975403ad0e225ffa0b2e63beadcab1b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 14 Dec 2018 11:36:40 +0100 Subject: [PATCH 74/76] Changed textColor of recommended infill slider text to be themed. This is required to make the dark theme work correctly --- .../Recommended/RecommendedInfillDensitySelector.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedInfillDensitySelector.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedInfillDensitySelector.qml index 2971415948..0da53cc1c1 100644 --- a/resources/qml/PrintSetupSelector/Recommended/RecommendedInfillDensitySelector.qml +++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedInfillDensitySelector.qml @@ -144,6 +144,7 @@ Item anchors.horizontalCenter: parent.horizontalCenter y: UM.Theme.getSize("thin_margin").height renderType: Text.NativeRendering + color: UM.Theme.getColor("quality_slider_available") } } } From 6a3ac9955162885c81859354d366d82cad40e42f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 14 Dec 2018 11:59:22 +0100 Subject: [PATCH 75/76] Ensure that all icons use the same color from theme. Also added some fixes for the dark theme --- plugins/PrepareStage/PrepareMenu.qml | 2 +- resources/qml/ActionPanel/PrintInformationWidget.qml | 2 +- resources/qml/IconWithText.qml | 4 ++-- resources/qml/Toolbar.qml | 2 +- resources/themes/cura-dark/theme.json | 6 ++++++ resources/themes/cura-light/theme.json | 2 ++ 6 files changed, 13 insertions(+), 5 deletions(-) diff --git a/plugins/PrepareStage/PrepareMenu.qml b/plugins/PrepareStage/PrepareMenu.qml index b7980bc30b..b62d65254d 100644 --- a/plugins/PrepareStage/PrepareMenu.qml +++ b/plugins/PrepareStage/PrepareMenu.qml @@ -100,7 +100,7 @@ Item source: UM.Theme.getIcon("load") width: UM.Theme.getSize("button_icon").width height: UM.Theme.getSize("button_icon").height - color: UM.Theme.getColor("toolbar_button_text") + color: UM.Theme.getColor("icon") sourceSize.height: height } diff --git a/resources/qml/ActionPanel/PrintInformationWidget.qml b/resources/qml/ActionPanel/PrintInformationWidget.qml index 554273a818..0826e2c715 100644 --- a/resources/qml/ActionPanel/PrintInformationWidget.qml +++ b/resources/qml/ActionPanel/PrintInformationWidget.qml @@ -15,7 +15,7 @@ UM.RecolorImage width: UM.Theme.getSize("section_icon").width height: UM.Theme.getSize("section_icon").height - color: popup.opened ? UM.Theme.getColor("primary") : UM.Theme.getColor("text_medium") + color: UM.Theme.getColor("icon") MouseArea { diff --git a/resources/qml/IconWithText.qml b/resources/qml/IconWithText.qml index 5530740040..f9220380f2 100644 --- a/resources/qml/IconWithText.qml +++ b/resources/qml/IconWithText.qml @@ -18,7 +18,7 @@ Item property alias color: label.color property alias text: label.text property alias font: label.font - + property alias iconColor: icon.color property real margin: UM.Theme.getSize("narrow_margin").width // These properties can be used in combination with layouts. @@ -39,7 +39,7 @@ Item width: UM.Theme.getSize("section_icon").width height: UM.Theme.getSize("section_icon").height - color: label.color + color: UM.Theme.getColor("icon") anchors { diff --git a/resources/qml/Toolbar.qml b/resources/qml/Toolbar.qml index 1e335472d4..c3c4c1cd6d 100644 --- a/resources/qml/Toolbar.qml +++ b/resources/qml/Toolbar.qml @@ -67,7 +67,7 @@ Item toolItem: UM.RecolorImage { source: UM.Theme.getIcon(model.icon) != "" ? UM.Theme.getIcon(model.icon) : "file:///" + model.location + "/" + model.icon - color: UM.Theme.getColor("toolbar_button_text") + color: UM.Theme.getColor("icon") sourceSize: UM.Theme.getSize("button_icon") } diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index 8540abf61a..2fa96c8897 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -17,6 +17,12 @@ "border": [127, 127, 127, 255], "secondary": [95, 95, 95, 255], + "icon": [204, 204, 204, 255], + "toolbar_background": [39, 44, 48, 255], + "toolbar_button_active": [95, 95, 95, 255], + "toolbar_button_hover": [95, 95, 95, 255], + "toolbar_button_active_hover": [95, 95, 95, 255], + "main_window_header_button_text_inactive": [128, 128, 128, 255], "main_window_header_button_text_hovered": [255, 255, 255, 255], diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 6d76004739..b64190dd23 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -83,6 +83,8 @@ "secondary": [240, 240, 240, 255], "secondary_shadow": [216, 216, 216, 255], + "icon": [8, 7, 63, 255], + "primary_button": [38, 113, 231, 255], "primary_button_shadow": [27, 95, 202, 255], "primary_button_hover": [81, 145, 247, 255], From c3aca8907cdfe83439b863912dcf3291142980dc Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 14 Dec 2018 12:02:52 +0100 Subject: [PATCH 76/76] Fix extruder number not being visible in dark theme --- resources/qml/ExtruderIcon.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/qml/ExtruderIcon.qml b/resources/qml/ExtruderIcon.qml index bb0b347b7e..c44b6e0fa3 100644 --- a/resources/qml/ExtruderIcon.qml +++ b/resources/qml/ExtruderIcon.qml @@ -49,6 +49,7 @@ Item anchors.centerIn: parent text: index + 1 font: UM.Theme.getFont("very_small") + color: UM.Theme.getColor("text") width: contentWidth height: contentHeight visible: extruderEnabled