diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 5f998b4c18..5d59cd53ba 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -142,7 +142,7 @@ class CuraPackageManager(QObject): if package_id in Application.getInstance().getRequiredPlugins(): continue - plugin_package_info["is_bundled"] = True if plugin_package_info["author"]["name"] == "Ultimaker B.V." else False + plugin_package_info["is_bundled"] = True if plugin_package_info["author"]["display_name"] == "Ultimaker B.V." else False plugin_package_info["is_active"] = self._plugin_registry.isActivePlugin(package_id) package_type = "plugin" if package_type not in installed_packages_dict: @@ -160,7 +160,8 @@ class CuraPackageManager(QObject): "cura_version": int(plugin_metadata["plugin"]["api"]), "website": "", "author": { - "name": plugin_metadata["plugin"].get("author", ""), + "author_id": plugin_metadata["plugin"].get("author", ""), + "display_name": plugin_metadata["plugin"].get("author", ""), "email": "", "website": "", }, diff --git a/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml b/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml index 989e065ed4..5c60e368a9 100644 --- a/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml +++ b/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml @@ -48,7 +48,6 @@ Item { toolbox.viewPage = "overview" toolbox.filterModelByProp("packages", "type", toolbox.viewCategory) - toolbox.filterModelByProp("authors", "type", toolbox.viewCategory) } style: ButtonStyle { diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailList.qml b/plugins/Toolbox/resources/qml/ToolboxDetailList.qml index 7e319974a4..8ac8cee4d0 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailList.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailList.qml @@ -6,10 +6,9 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM -Rectangle +Item { id: detailList - // color: "green" ScrollView { frameVisible: false @@ -24,6 +23,8 @@ Rectangle bottomMargin: UM.Theme.getSize("wide_margin").height top: parent.top } + // TODO: Sometimes the height is not the childrenRect.height. Lord + // knows why. Probably because QT is garbage. height: childrenRect.height spacing: UM.Theme.getSize("default_margin").height Repeater diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml index 1080a9213b..5071f8ed81 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml @@ -11,9 +11,14 @@ Item id: tile property bool installed: toolbox.isInstalled(model.id) width: detailList.width - UM.Theme.getSize("wide_margin").width - height: UM.Theme.getSize("toolbox_detail_tile").height - Column + // TODO: Without this line, every instance of this object has 0 height. With + // it, QML spits out tons of bugs claiming a binding loop (not true). Why? + // Because QT is garbage. + height: Math.max( UM.Theme.getSize("toolbox_detail_tile").height, childrenRect.height + UM.Theme.getSize("default_margin").height) + Item { + id: normalData + height: childrenRect.height anchors { left: parent.left @@ -23,15 +28,17 @@ Item } Label { + id: packageName width: parent.width height: UM.Theme.getSize("toolbox_property_label").height text: model.name wrapMode: Text.WordWrap color: UM.Theme.getColor("text") - font: UM.Theme.getFont("default_bold") + font: UM.Theme.getFont("medium_bold") } Label { + anchors.top: packageName.bottom width: parent.width text: { @@ -53,13 +60,13 @@ Item font: UM.Theme.getFont("default") } } - Rectangle + Item { id: controls anchors.right: tile.right anchors.top: tile.top width: childrenRect.width - color: "blue" + height: childrenRect.height Button { id: installButton @@ -179,6 +186,78 @@ Item } } } + + Item + { + anchors.top: normalData.bottom + anchors.topMargin: UM.Theme.getSize("default_margin").height + height: model.type == "material" ? childrenRect.height : 0 + width: normalData.width + visible: model.type == "material" + Label + { + id: compatibilityHeading + anchors.topMargin: UM.Theme.getSize("default_margin").height + width: parent.width + text: catalog.i18nc("@label", "Compatibility") + wrapMode: Text.WordWrap + color: UM.Theme.getColor("text_medium") + font: UM.Theme.getFont("default") + } + Column + { + id: compatibilityLabels + anchors + { + top: compatibilityHeading.bottom + topMargin: UM.Theme.getSize("default_margin").height + bottomMargin: UM.Theme.getSize("default_margin").height + } + width: childrenRect.width + Label + { + text: catalog.i18nc("@label", "Machines") + ":" + font: UM.Theme.getFont("small") + } + Label + { + text: catalog.i18nc("@label", "Print Cores") + ":" + font: UM.Theme.getFont("small") + } + Label + { + text: catalog.i18nc("@label", "Quality Profiles") + ":" + font: UM.Theme.getFont("small") + } + } + Column + { + id: compatibilityValues + anchors + { + left: compatibilityLabels.right + leftMargin: UM.Theme.getSize("default_margin").height + top: compatibilityLabels.top + bottom: compatibilityLabels.bottom + } + Label + { + text: "Thingy" + font: UM.Theme.getFont("very_small") + } + Label + { + text: "Thingy" + font: UM.Theme.getFont("very_small") + } + Label + { + text: "Thingy" + font: UM.Theme.getFont("very_small") + } + } + } + Rectangle { color: UM.Theme.getColor("lining") diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml index 4d479278b2..d7b1364a65 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml @@ -92,7 +92,7 @@ Item { case "material": toolbox.viewPage = "author" - toolbox.filterModelByProp("packages", "author_name", model.name) + toolbox.filterModelByProp("packages", "author_id", model.id) break default: toolbox.viewPage = "detail" diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml index 1621340184..0a2924c600 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml @@ -10,17 +10,6 @@ Item { width: UM.Theme.getSize("toolbox_thumbnail_large").width height: childrenRect.height - visible: - { - if (toolbox.viewCategory == "material" && model.packages_count) - { - return model.packages_count > 0 - } - else - { - return true - } - } Rectangle { id: highlight diff --git a/plugins/Toolbox/resources/qml/ToolboxHeader.qml b/plugins/Toolbox/resources/qml/ToolboxHeader.qml index 75740982d9..59dbe23ea4 100644 --- a/plugins/Toolbox/resources/qml/ToolboxHeader.qml +++ b/plugins/Toolbox/resources/qml/ToolboxHeader.qml @@ -38,7 +38,7 @@ Item active: toolbox.viewCategory == "material" onClicked: { - toolbox.filterModelByProp("authors", "type", "material") + toolbox.filterModelByProp("authors", "package_types", "material") toolbox.viewCategory = "material" toolbox.viewPage = "overview" } diff --git a/plugins/Toolbox/src/AuthorsModel.py b/plugins/Toolbox/src/AuthorsModel.py index f61fdbe223..2dfba8fb23 100644 --- a/plugins/Toolbox/src/AuthorsModel.py +++ b/plugins/Toolbox/src/AuthorsModel.py @@ -15,13 +15,14 @@ class AuthorsModel(ListModel): self._metadata = None - self.addRoleName(Qt.UserRole + 1, "name") - self.addRoleName(Qt.UserRole + 2, "email") - self.addRoleName(Qt.UserRole + 3, "website") - self.addRoleName(Qt.UserRole + 4, "type") - self.addRoleName(Qt.UserRole + 5, "icon_url") - self.addRoleName(Qt.UserRole + 6, "packages_count") - self.addRoleName(Qt.UserRole + 7, "description") + self.addRoleName(Qt.UserRole + 1, "id") + self.addRoleName(Qt.UserRole + 2, "name") + self.addRoleName(Qt.UserRole + 3, "email") + self.addRoleName(Qt.UserRole + 4, "website") + self.addRoleName(Qt.UserRole + 5, "package_count") + self.addRoleName(Qt.UserRole + 6, "package_types") + self.addRoleName(Qt.UserRole + 7, "icon_url") + 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] @@ -35,21 +36,24 @@ class AuthorsModel(ListModel): for author in self._metadata: items.append({ - "name": author["name"], - "email": author["email"] if "email" in author else None, - "website": author["website"], - "type": author["type"] if "type" in author else None, - "icon_url": author["icon_url"] if "icon_url" in author else None, - "packages_count": author["packages_count"] if "packages_count" in author else 0, - "description": "Material and quality profiles from {author_name}".format( author_name = author["name"]) + "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"], + "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"]) }) # 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) + if key is "package_types": + key_filter = lambda item, value = value: value in item["package_types"] + elif "*" in value: + key_filter = lambda item, key = key, value = value: self._matchRegExp(item, key, value) else: - key_filter = lambda candidate, key = key, value = value: self._matchString(candidate, key, value) + key_filter = lambda item, key = key, value = value: self._matchString(item, key, value) items = filter(key_filter, items) # Execute all filters. diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py index b030ad895e..f7ee1dd324 100644 --- a/plugins/Toolbox/src/PackagesModel.py +++ b/plugins/Toolbox/src/PackagesModel.py @@ -21,14 +21,15 @@ class PackagesModel(ListModel): self.addRoleName(Qt.UserRole + 2, "type") self.addRoleName(Qt.UserRole + 3, "name") self.addRoleName(Qt.UserRole + 4, "version") - self.addRoleName(Qt.UserRole + 5, "author_name") - self.addRoleName(Qt.UserRole + 6, "author_email") - self.addRoleName(Qt.UserRole + 7, "description") - self.addRoleName(Qt.UserRole + 8, "icon_url") - self.addRoleName(Qt.UserRole + 9, "image_urls") - self.addRoleName(Qt.UserRole + 10, "download_url") - self.addRoleName(Qt.UserRole + 11, "last_updated") - self.addRoleName(Qt.UserRole + 12, "is_bundled") + self.addRoleName(Qt.UserRole + 5, "author_id") + self.addRoleName(Qt.UserRole + 6, "author_name") + self.addRoleName(Qt.UserRole + 7, "author_email") + self.addRoleName(Qt.UserRole + 8, "description") + self.addRoleName(Qt.UserRole + 9, "icon_url") + self.addRoleName(Qt.UserRole + 10, "image_urls") + self.addRoleName(Qt.UserRole + 11, "download_url") + self.addRoleName(Qt.UserRole + 12, "last_updated") + self.addRoleName(Qt.UserRole + 13, "is_bundled") # List of filters for queries. The result is the union of the each list of results. self._filter = {} # type: Dict[str, str] @@ -46,8 +47,9 @@ class PackagesModel(ListModel): "type": package["package_type"], "name": package["display_name"], "version": package["package_version"], - "author_name": package["author"]["name"], - "author_email": package["author"]["email"] if "email" in package["author"] else None, + "author_id": package["author"]["author_id"], + "author_name": package["author"]["display_name"], + "author_email": package["author"]["email"] if "email" in package["author"] else "None", "description": package["description"], "icon_url": package["icon_url"] if "icon_url" in package else None, "image_urls": package["image_urls"] if "image_urls" in package else None, diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index d3b0032b51..104d3d323a 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -56,10 +56,10 @@ class Toolbox(QObject, Extension): ) ] self._request_urls = { - "authors": None, - "packages": QUrl("{base_url}/packages".format(base_url = self._api_url)), - "plugins_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)), - "materials_showcase": None + "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)), + "materials_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)) } # Data: @@ -68,33 +68,7 @@ class Toolbox(QObject, Extension): "packages": [], "plugins_showcase": [], "plugins_installed": [], - # TODO: Replace this with a proper API call: - "materials_showcase": [ - { - "name": "Ultimaker", - "email": "ian.paschal@gmail.com", - "website": "ultimaker.com", - "type": "material", - "icon": None, - "packages_count": 7 - }, - { - "name": "DSM", - "email": "contact@dsm.nl", - "website": "www.dsm.nl", - "type": "material", - "icon": None, - "packages_count": 0 - }, - { - "name": "BASF", - "email": "contact@basf.de", - "website": "www.basf.de", - "type": "material", - "icon": None, - "packages_count": 0 - } - ], + "materials_showcase": [], "materials_installed": [] } @@ -103,8 +77,10 @@ class Toolbox(QObject, Extension): "authors": AuthorsModel(self), "packages": PackagesModel(self), "plugins_showcase": PackagesModel(self), + "plugins_available": PackagesModel(self), "plugins_installed": PackagesModel(self), "materials_showcase": AuthorsModel(self), + "materials_available": PackagesModel(self), "materials_installed": PackagesModel(self) } @@ -190,7 +166,9 @@ class Toolbox(QObject, Extension): # Make remote requests: self._makeRequestByType("packages") + self._makeRequestByType("authors") self._makeRequestByType("plugins_showcase") + self._makeRequestByType("materials_showcase") # Gather installed packages: self._updateInstalledModels() @@ -298,6 +276,15 @@ class Toolbox(QObject, Extension): return True return False + def loadingComplete(self) -> bool: + populated = 0 + for list in self._metadata.items(): + if len(list) > 0: + populated += 1 + if populated == len(self._metadata.items()): + return True + return False + # Make API Calls @@ -362,111 +349,50 @@ class Toolbox(QObject, Extension): return if reply.operation() == QNetworkAccessManager.GetOperation: - # TODO: In the future use the following to build any model from any - # request. Right now this doesn't work because the packages request - # is also responsible for populating other models. - # for type, url in self._request_urls.items(): - # if reply.url() == url: - # try: - # json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) - # - # # 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[type]: - # Logger.log("e", "Could not find the %s model.", type) - # break - # self._metadata[type] = json_data["data"] - # self._models[type].setMetadata(self._metadata[type]) - # self.metadataChanged.emit() - # self.setViewPage("overview") - # return - # except json.decoder.JSONDecodeError: - # Logger.log("w", "Toolbox: Received invalid JSON for %s.", type) - # break + for type, url in self._request_urls.items(): + if reply.url() == url: + try: + json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) - if reply.url() == self._request_urls["packages"]: - try: - json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) + # 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[type]: + Logger.log("e", "Could not find the %s model.", type) + break + + # HACK: Eventually get rid of the code from here... + if type is "plugins_showcase" or type is "materials_showcase": + self._metadata["plugins_showcase"] = json_data["data"]["plugin"]["packages"] + self._metadata["materials_showcase"] = json_data["data"]["material"]["authors"] + else: + # ...until here. + # This hack arises for multiple reasons but the main + # one is because there are not separate API calls + # for different kinds of showcases. + self._metadata[type] = json_data["data"] + self._models[type].setMetadata(self._metadata[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 type is "authors": + self._models[type].setFilter({"package_types": "material"}) + + self.metadataChanged.emit() + + if self.loadingComplete() is True: + self.setViewPage("overview") - # Check for errors: - if "errors" in json_data: - for error in json_data["errors"]: - Logger.log("e", "%s", error["title"]) return - - # Create packages model with all packages: - if not self._models["packages"]: - self._models["packages"] = PackagesModel(self) - self._metadata["packages"] = json_data["data"] - self._models["packages"].setMetadata(self._metadata["packages"]) - self.metadataChanged.emit() - - # Create authors model with all authors: - if not self._models["authors"]: - self._models["authors"] = AuthorsModel() - # TODO: Replace this with a proper API call: - for package in self._metadata["packages"]: - if package["author"] not in self._metadata["authors"]: - self._metadata["authors"].append(package["author"]) - - for author in self._metadata["authors"]: - if "package_count" not in author: - author["package_count"] = 0 - - for package in self._metadata["packages"]: - if package["author"]["name"] == author["name"]: - author["package_count"] += 1 - author["type"] = package["package_type"] - if "icon_url" in package: - author["icon_url"] = package["icon_url"] - - self._models["authors"].setMetadata(self._metadata["authors"]) - self.metadataChanged.emit() - - if not self._models["materials_showcase"]: - self._models["materials_showcase"] = AuthorsModel(self) - # TODO: Replace this with a proper API call: - self._models["materials_showcase"].setMetadata(self._metadata["materials_showcase"]) - - # This part is also needed for comparing downloaded packages to - # installed packages. - self._models["packages"].setMetadata(self._metadata["packages"]) - self._models["packages"].setFilter({"type": "plugin"}) - - self.metadataChanged.emit() - - self.setViewPage("overview") - return - - except json.decoder.JSONDecodeError: - Logger.log("w", "Toolbox: Received invalid JSON for package list.") - return - - if reply.url() == self._request_urls["plugins_showcase"]: - try: - json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) - - # Check for errors: - if "errors" in json_data: - for error in json_data["errors"]: - Logger.log("e", "%s", error["title"]) - return - - self._metadata["plugins_showcase"] = json_data["data"] - self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"]) - self.metadataChanged.emit() - - self.setViewPage("overview") - return - - except json.decoder.JSONDecodeError: - Logger.log("w", "Toolbox: Received invalid JSON for showcase.") - return + except json.decoder.JSONDecodeError: + Logger.log("w", "Toolbox: Received invalid JSON for %s.", type) + break else: # Ignore any operation that is not a get operation @@ -531,7 +457,7 @@ class Toolbox(QObject, Extension): def activePackage(self) -> dict: return self._active_package - def setViewCategory(self, category: str = "plugins"): + def setViewCategory(self, category: str = "plugin"): self._view_category = category self.viewChanged.emit() @pyqtProperty(str, fset = setViewCategory, notify = viewChanged) @@ -549,8 +475,6 @@ class Toolbox(QObject, Extension): # Expose Models: # -------------------------------------------------------------------------- - # TODO: Maybe replace this with simply exposing self._models to Qt and then - # setting model: toolbox.models.foobar instead of toolbox.foobarModel @pyqtProperty(QObject, notify = metadataChanged) def authorsModel(self) -> AuthorsModel: return self._models["authors"]