diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailList.qml b/plugins/Toolbox/resources/qml/ToolboxDetailList.qml index fe65eed7c4..ba772b343a 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailList.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailList.qml @@ -12,10 +12,8 @@ Item id: base anchors { - topMargin: UM.Theme.getSize("default_margin").height - bottomMargin: UM.Theme.getSize("default_margin").height - leftMargin: UM.Theme.getSize("double_margin").width - rightMargin: UM.Theme.getSize("double_margin").width + topMargin: UM.Theme.getSize("double_margin").height + bottomMargin: UM.Theme.getSize("double_margin").height } ScrollView { @@ -24,6 +22,8 @@ Item style: UM.Theme.styles.scrollview Column { + anchors.right: base.right + anchors.rightMargin: UM.Theme.getSize("double_margin").width height: childrenRect.height spacing: UM.Theme.getSize("default_margin").height Repeater diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml b/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml index 6054414405..156c829767 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml @@ -11,6 +11,7 @@ import UM 1.1 as UM Item { + property var details: manager.packagesModel.items[0] id: base anchors.fill: parent Item @@ -82,7 +83,7 @@ Item width: UM.Theme.getSize("toolbox_thumbnail_medium").width height: UM.Theme.getSize("toolbox_thumbnail_medium").height fillMode: Image.PreserveAspectFit - source: manager.detailData["icon_url"] || "../images/logobot.svg" + source: details.icon_url || "../images/logobot.svg" anchors { top: parent.top @@ -104,21 +105,21 @@ Item spacing: Math.floor(UM.Theme.getSize("default_margin").height/2) Label { - text: manager.detailData["name"] + text: details.name font: UM.Theme.getFont("large") wrapMode: Text.WordWrap width: parent.width } Label { - text: manager.detailData["description"] + text: details.description font: UM.Theme.getFont("default") wrapMode: Text.WordWrap width: parent.width } Label { - text: "Author: " + manager.detailData["author"]["name"] + text: "Author: " + details.author_name font: UM.Theme.getFont("small") wrapMode: Text.WordWrap width: parent.width @@ -131,8 +132,10 @@ Item { right: header.right top: header.bottom + left: header.left bottom: base.bottom + } } } diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml index 5671e95495..cc4e61c55b 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml @@ -9,9 +9,9 @@ import UM 1.1 as UM Rectangle { - width: base.width - height: UM.Theme.getSize("base_unit").height * 12 - color: "steelblue" + width: base.width - UM.Theme.getSize("double_margin").width + height: UM.Theme.getSize("base_unit").height * 8 + color: "transparent" Column { anchors @@ -20,7 +20,6 @@ Rectangle right: controls.left rightMargin: UM.Theme.getSize("default_margin").width top: parent.top - leftMargin: UM.Theme.getSize("default_margin").width topMargin: UM.Theme.getSize("default_margin").height } Label diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml index b05963ecd1..45eedbedbd 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml @@ -34,7 +34,7 @@ Column Repeater { - model: manager.packagesModel + model: manager.viewCategory == "material" ? manager.authorsModel : manager.packagesModel delegate: ToolboxDownloadsGridTile { Layout.preferredWidth: (grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml index e8b4f2453f..193857f756 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml @@ -70,16 +70,16 @@ Item onClicked: { if ( manager.viewCategory == "material" ) { - console.log("filtering by " + model.author) - manager.viewSelection = model.author.name + manager.viewSelection = model.name manager.viewPage = "author" - manager.filterPackages("author", model.author) + manager.filterPackages("author_name", model.name) } else { manager.viewSelection = model.id manager.viewPage = "detail" manager.filterPackages("id", model.id) + } } } diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml index c9a0babc5f..78a884c95d 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml @@ -18,7 +18,7 @@ Column Label { id: heading - text: "Top Downloads" + text: "Showcase" width: parent.width color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("medium") diff --git a/plugins/Toolbox/resources/qml/ToolboxHeader.qml b/plugins/Toolbox/resources/qml/ToolboxHeader.qml index cedf95fda7..16c82866dc 100644 --- a/plugins/Toolbox/resources/qml/ToolboxHeader.qml +++ b/plugins/Toolbox/resources/qml/ToolboxHeader.qml @@ -55,7 +55,8 @@ Rectangle { } onClicked: { - manager.filterPackagesByType("plugin") + manager.filterPackages("type", "plugin") + manager.filterAuthors("type", "plugin") manager.viewCategory = "plugin" manager.viewPage = "overview" } @@ -91,7 +92,8 @@ Rectangle { } onClicked: { - manager.filterPackagesByType("material") + manager.filterPackages("type", "material") + manager.filterAuthors("type", "material") manager.viewCategory = "material" manager.viewPage = "overview" } diff --git a/plugins/Toolbox/src/AuthorsModel.py b/plugins/Toolbox/src/AuthorsModel.py new file mode 100644 index 0000000000..2156534b31 --- /dev/null +++ b/plugins/Toolbox/src/AuthorsModel.py @@ -0,0 +1,88 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import re +from typing import Dict + +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): + NameRole = Qt.UserRole + 1 + EmailRole = Qt.UserRole + 2 + WebsiteRole = Qt.UserRole + 3 + TypeRole = Qt.UserRole + 4 + + def __init__(self, parent = None): + super().__init__(parent) + + self._authors_metadata = None + + self.addRoleName(AuthorsModel.NameRole, "name") + self.addRoleName(AuthorsModel.EmailRole, "email") + self.addRoleName(AuthorsModel.WebsiteRole, "website") + self.addRoleName(AuthorsModel.TypeRole, "type") + + # List of filters for queries. The result is the union of the each list of results. + self._filter = {} # type: Dict[str,str] + + def setMetaData(self, data): + self._authors_metadata = data + self._update() + + def _update(self): + items = [] + + for author in self._authors_metadata: + items.append({ + "name": author["name"], + "email": author["email"], + "website": author["website"], + "type": author["type"] + }) + + # Filter on all the key-word arguments. + for key, value in self._filter.items(): + if "*" in value: + key_filter = lambda candidate, key = key, value = value: self._matchRegExp(candidate, key, value) + else: + key_filter = lambda candidate, key = key, value = value: self._matchString(candidate, key, value) + items = filter(key_filter, items) + + # Execute all filters. + filtered_items = list(items) + + filtered_items.sort(key = lambda k: k["name"]) + self.setItems(filtered_items) + + ## Set the filter of this model based on a string. + # \param filter_dict \type{Dict} Dictionary to do the filtering by. + def setFilter(self, filter_dict: Dict[str, str]) -> None: + if filter_dict != self._filter: + self._filter = filter_dict + self._update() + + @pyqtProperty("QVariantMap", fset = setFilter, constant = True) + def filter(self) -> Dict[str, str]: + return self._filter + + # Check to see if a container matches with a regular expression + def _matchRegExp(self, metadata, property_name, value): + if property_name not in metadata: + return False + value = re.escape(value) #Escape for regex patterns. + value = "^" + value.replace("\\*", ".*") + "$" #Instead of (now escaped) asterisks, match on any string. Also add anchors for a complete match. + if self._ignore_case: + value_pattern = re.compile(value, re.IGNORECASE) + else: + value_pattern = re.compile(value) + + return value_pattern.match(str(metadata[property_name])) + + # Check to see if a container matches with a string + def _matchString(self, metadata, property_name, value): + if property_name not in metadata: + return False + return value.lower() == str(metadata[property_name]).lower() diff --git a/plugins/Toolbox/src/CuraPackageModel.py b/plugins/Toolbox/src/CuraPackageModel.py index facf21cc6f..de105cf6a1 100644 --- a/plugins/Toolbox/src/CuraPackageModel.py +++ b/plugins/Toolbox/src/CuraPackageModel.py @@ -14,10 +14,11 @@ class CuraPackageModel(ListModel): TypeRole = Qt.UserRole + 2 NameRole = Qt.UserRole + 3 VersionRole = Qt.UserRole + 4 - AuthorRole = Qt.UserRole + 5 - DescriptionRole = Qt.UserRole + 6 - IconURLRole = Qt.UserRole + 7 - ImageURLsRole = Qt.UserRole + 8 + AuthorNameRole = Qt.UserRole + 5 + AuthorEmailRole = Qt.UserRole + 6 + DescriptionRole = Qt.UserRole + 7 + IconURLRole = Qt.UserRole + 8 + ImageURLsRole = Qt.UserRole + 9 def __init__(self, parent = None): super().__init__(parent) @@ -28,7 +29,8 @@ class CuraPackageModel(ListModel): self.addRoleName(CuraPackageModel.TypeRole, "type") self.addRoleName(CuraPackageModel.NameRole, "name") self.addRoleName(CuraPackageModel.VersionRole, "version") - self.addRoleName(CuraPackageModel.AuthorRole, "author") + self.addRoleName(CuraPackageModel.AuthorNameRole, "author_name") + self.addRoleName(CuraPackageModel.AuthorEmailRole, "author_email") self.addRoleName(CuraPackageModel.DescriptionRole, "description") self.addRoleName(CuraPackageModel.IconURLRole, "icon_url") self.addRoleName(CuraPackageModel.ImageURLsRole, "image_urls") @@ -49,7 +51,8 @@ class CuraPackageModel(ListModel): "type": package["package_type"], "name": package["display_name"], "version": package["package_version"], - "author": package["author"], + "author_name": package["author"]["name"], + "author_email": package["author"]["email"], "description": package["description"], "icon_url": package["icon_url"] if "icon_url" in package else None, "image_urls": package["image_urls"] diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 60e43b357e..abac3c560d 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -22,6 +22,7 @@ import platform import zipfile from cura.CuraApplication import CuraApplication +from .AuthorsModel import AuthorsModel from .CuraPackageModel import CuraPackageModel i18n_catalog = i18nCatalog("cura") @@ -32,7 +33,7 @@ class Toolbox(QObject, Extension): super().__init__(parent) self._api_version = 1 - self._api_url = "https://api-staging.ultimaker.com/cura-packages/v%s/" % self._api_version + self._api_url = "https://api-staging.ultimaker.com/cura-packages/v%s" % self._api_version self._package_list_request = None self._download_plugin_request = None @@ -45,6 +46,8 @@ class Toolbox(QObject, Extension): self._packages_metadata = [] # Stores the remote information of the packages self._packages_model = None # Model that list the remote available packages + self._showcase_model = None + self._authors_model = None # These properties are for keeping track of the UI state: @@ -112,10 +115,14 @@ class Toolbox(QObject, Extension): showLicenseDialog = pyqtSignal() showRestartDialog = pyqtSignal() + packagesMetadataChanged = pyqtSignal() + authorsMetadataChanged = pyqtSignal() + onDownloadProgressChanged = pyqtSignal() onIsDownloadingChanged = pyqtSignal() restartRequiredChanged = pyqtSignal() + viewChanged = pyqtSignal() detailViewChanged = pyqtSignal() filterChanged = pyqtSignal() @@ -161,7 +168,7 @@ class Toolbox(QObject, Extension): def requestPackageList(self): Logger.log("i", "Requesting package list") - url = QUrl("{base_url}packages?cura_version={version}".format(base_url = self._api_url, version = self._packages_version_number)) + url = QUrl("{base_url}/cura/v{version}/packages".format(base_url = self._api_url, version = self._packages_version_number)) self._package_list_request = QNetworkRequest(url) self._package_list_request.setRawHeader(*self._request_header) self._network_manager.get(self._package_list_request) @@ -355,17 +362,16 @@ class Toolbox(QObject, Extension): for item in self._packages_metadata: if item["id"] == plugin["id"]: plugin["update_url"] = item["file_location"] - - # if self._current_view == "plugins": - # self.filterPackagesByType("plugin") - # elif self._current_view == "materials": - # self.filterPackagesByType("material") return self._plugins_model @pyqtProperty(QObject, notify = packagesMetadataChanged) def packagesModel(self): return self._packages_model + @pyqtProperty(QObject, notify = authorsMetadataChanged) + def authorsModel(self): + return self._authors_model + @pyqtProperty(bool, notify = packagesMetadataChanged) def dataReady(self): return self._packages_model is not None @@ -425,16 +431,35 @@ class Toolbox(QObject, Extension): return if reply.operation() == QNetworkAccessManager.GetOperation: - if reply_url == "{base_url}packages?cura_version={version}".format(base_url = self._api_url, version = self._packages_version_number): + if reply_url == "{base_url}/cura/v{version}/packages".format(base_url = self._api_url, version = self._packages_version_number): try: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) - + print(json_data) # Add metadata to the manager: - self._packages_metadata = json_data["data"] + + + # Create packages model with all packages: if not self._packages_model: self._packages_model = CuraPackageModel() + self._packages_metadata = json_data["data"] self._packages_model.setPackagesMetaData(self._packages_metadata) self.packagesMetadataChanged.emit() + + # Create authors model with all authors: + if not self._authors_model: + self._authors_model = AuthorsModel() + # In the future, this will be its own API call. + self._authors_metadata = [] + for package in self._packages_metadata: + package["author"]["type"] = package["package_type"] + print(package["author"]) + if package["author"] not in self._authors_metadata: + self._authors_metadata.append(package["author"]) + self._authors_model.setMetaData(self._authors_metadata) + self.authorsMetadataChanged.emit() + + + except json.decoder.JSONDecodeError: Logger.log("w", "Received an invalid print job state message: Not valid JSON.") return @@ -469,6 +494,7 @@ class Toolbox(QObject, Extension): CuraApplication.getInstance().windowClosed() + # Getter & Setter for self._view_category def setViewCategory(self, category = "plugins"): self._view_category = category @@ -494,19 +520,13 @@ class Toolbox(QObject, Extension): return self._view_selection - # Filtering - @pyqtSlot(str) - def filterPackagesByType(self, type): - if not self._packages_model: - return - self._packages_model.setFilter({"type": type}) - self.filterChanged.emit() + # Filtering @pyqtSlot(str, str) def filterPackages(self, filterType, parameter): if not self._packages_model: return - self._packages_model.setFilter({filterType: parameter}) + self._packages_model.setFilter({ filterType: parameter }) self.filterChanged.emit() @pyqtSlot() @@ -515,3 +535,17 @@ class Toolbox(QObject, Extension): return self._packages_model.setFilter({}) self.filterChanged.emit() + + @pyqtSlot(str, str) + def filterAuthors(self, filterType, parameter): + if not self._authors_model: + return + self._authors_model.setFilter({ filterType: parameter }) + self.filterChanged.emit() + + @pyqtSlot() + def unfilterAuthors(self): + if not self._authors_model: + return + self._authors_model.setFilter({}) + self.filterChanged.emit()