From 322fe7d61f6210cd3ca2e5b4abc848c0620b46ba Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 30 Mar 2018 14:15:00 +0200 Subject: [PATCH 1/7] CURA-5137 Rename Plugin Browser into Toolbox --- cura/CuraApplication.py | 2 +- plugins/PluginBrowser/__init__.py | 12 ------------ plugins/PluginBrowser/plugin.json | 7 ------- .../PluginBrowser.py => Toolbox/Toolbox/Toolbox.py} | 7 ++++--- plugins/Toolbox/__init__.py | 12 ++++++++++++ plugins/Toolbox/plugin.json | 7 +++++++ .../resources/qml/PluginBrowser.qml | 0 .../resources/qml/PluginEntry.qml | 0 .../resources/qml/SectionShadow.qml | 0 .../resources/qml/ToolboxDetailBlock.qml | 0 .../resources/qml/ToolboxFooter.qml | 0 .../resources/qml/ToolboxGrid.qml | 0 .../resources/qml/ToolboxGridTile.qml | 0 .../resources/qml/ToolboxHeader.qml | 0 .../resources/qml/ToolboxLicenseDialog.qml | 0 .../resources/qml/ToolboxRestartDialog.qml | 0 .../resources/qml/ToolboxShowcase.qml | 0 .../resources/qml/ToolboxShowcaseTile.qml | 0 .../resources/qml/ToolboxViewDetail.qml | 0 .../resources/qml/ToolboxViewDownloads.qml | 0 .../resources/qml/ToolboxViewInstalled.qml | 0 resources/qml/Actions.qml | 2 +- resources/qml/Cura.qml | 4 ++-- 23 files changed, 27 insertions(+), 26 deletions(-) delete mode 100644 plugins/PluginBrowser/__init__.py delete mode 100644 plugins/PluginBrowser/plugin.json rename plugins/{PluginBrowser/PluginBrowser.py => Toolbox/Toolbox/Toolbox.py} (98%) create mode 100644 plugins/Toolbox/__init__.py create mode 100644 plugins/Toolbox/plugin.json rename plugins/{PluginBrowser => Toolbox}/resources/qml/PluginBrowser.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/PluginEntry.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/SectionShadow.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/ToolboxDetailBlock.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/ToolboxFooter.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/ToolboxGrid.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/ToolboxGridTile.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/ToolboxHeader.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/ToolboxLicenseDialog.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/ToolboxRestartDialog.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/ToolboxShowcase.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/ToolboxShowcaseTile.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/ToolboxViewDetail.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/ToolboxViewDownloads.qml (100%) rename plugins/{PluginBrowser => Toolbox}/resources/qml/ToolboxViewInstalled.qml (100%) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 5ac0f03b2e..d3ea3f134c 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -261,7 +261,7 @@ class CuraApplication(QtApplication): "TranslateTool", "FileLogger", "XmlMaterialProfile", - "PluginBrowser", + "Toolbox", "PrepareStage", "MonitorStage" ]) diff --git a/plugins/PluginBrowser/__init__.py b/plugins/PluginBrowser/__init__.py deleted file mode 100644 index d414c36a99..0000000000 --- a/plugins/PluginBrowser/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2017 Ultimaker B.V. -# PluginBrowser is released under the terms of the LGPLv3 or higher. - -from . import PluginBrowser - - -def getMetaData(): - return {} - - -def register(app): - return {"extension": PluginBrowser.PluginBrowser()} diff --git a/plugins/PluginBrowser/plugin.json b/plugins/PluginBrowser/plugin.json deleted file mode 100644 index 491e21f506..0000000000 --- a/plugins/PluginBrowser/plugin.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "Plugin Browser", - "author": "Ultimaker B.V.", - "version": "1.0.0", - "api": 4, - "description": "Find, manage and install new plugins." -} \ No newline at end of file diff --git a/plugins/PluginBrowser/PluginBrowser.py b/plugins/Toolbox/Toolbox/Toolbox.py similarity index 98% rename from plugins/PluginBrowser/PluginBrowser.py rename to plugins/Toolbox/Toolbox/Toolbox.py index 917502efa0..9c64efc9fa 100644 --- a/plugins/PluginBrowser/PluginBrowser.py +++ b/plugins/Toolbox/Toolbox/Toolbox.py @@ -1,5 +1,5 @@ -# Copyright (c) 2017 Ultimaker B.V. -# PluginBrowser is released under the terms of the LGPLv3 or higher. +# Copyright (c) 2018 Ultimaker B.V. +# Toolbox is released under the terms of the LGPLv3 or higher. from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply @@ -24,7 +24,7 @@ from cura.CuraApplication import CuraApplication i18n_catalog = i18nCatalog("cura") -class PluginBrowser(QObject, Extension): +class Toolbox(QObject, Extension): def __init__(self, parent=None): super().__init__(parent) @@ -138,6 +138,7 @@ class PluginBrowser(QObject, Extension): def _createDialog(self, qml_name): Logger.log("d", "Creating dialog [%s]", qml_name) path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "resources", "qml", qml_name) + Logger.log("d", "Creating dialog [%s]", path) dialog = Application.getInstance().createQmlComponent(path, {"manager": self}) return dialog diff --git a/plugins/Toolbox/__init__.py b/plugins/Toolbox/__init__.py new file mode 100644 index 0000000000..dc2d6c9c23 --- /dev/null +++ b/plugins/Toolbox/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Toolbox is released under the terms of the LGPLv3 or higher. + +from .Toolbox import Toolbox + + +def getMetaData(): + return {} + + +def register(app): + return {"extension": Toolbox.Toolbox()} diff --git a/plugins/Toolbox/plugin.json b/plugins/Toolbox/plugin.json new file mode 100644 index 0000000000..7c85851df7 --- /dev/null +++ b/plugins/Toolbox/plugin.json @@ -0,0 +1,7 @@ +{ + "name": "Toolbox", + "author": "Ultimaker B.V.", + "version": "1.0.0", + "api": 4, + "description": "Find, manage and install new cura packages." +} \ No newline at end of file diff --git a/plugins/PluginBrowser/resources/qml/PluginBrowser.qml b/plugins/Toolbox/resources/qml/PluginBrowser.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/PluginBrowser.qml rename to plugins/Toolbox/resources/qml/PluginBrowser.qml diff --git a/plugins/PluginBrowser/resources/qml/PluginEntry.qml b/plugins/Toolbox/resources/qml/PluginEntry.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/PluginEntry.qml rename to plugins/Toolbox/resources/qml/PluginEntry.qml diff --git a/plugins/PluginBrowser/resources/qml/SectionShadow.qml b/plugins/Toolbox/resources/qml/SectionShadow.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/SectionShadow.qml rename to plugins/Toolbox/resources/qml/SectionShadow.qml diff --git a/plugins/PluginBrowser/resources/qml/ToolboxDetailBlock.qml b/plugins/Toolbox/resources/qml/ToolboxDetailBlock.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/ToolboxDetailBlock.qml rename to plugins/Toolbox/resources/qml/ToolboxDetailBlock.qml diff --git a/plugins/PluginBrowser/resources/qml/ToolboxFooter.qml b/plugins/Toolbox/resources/qml/ToolboxFooter.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/ToolboxFooter.qml rename to plugins/Toolbox/resources/qml/ToolboxFooter.qml diff --git a/plugins/PluginBrowser/resources/qml/ToolboxGrid.qml b/plugins/Toolbox/resources/qml/ToolboxGrid.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/ToolboxGrid.qml rename to plugins/Toolbox/resources/qml/ToolboxGrid.qml diff --git a/plugins/PluginBrowser/resources/qml/ToolboxGridTile.qml b/plugins/Toolbox/resources/qml/ToolboxGridTile.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/ToolboxGridTile.qml rename to plugins/Toolbox/resources/qml/ToolboxGridTile.qml diff --git a/plugins/PluginBrowser/resources/qml/ToolboxHeader.qml b/plugins/Toolbox/resources/qml/ToolboxHeader.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/ToolboxHeader.qml rename to plugins/Toolbox/resources/qml/ToolboxHeader.qml diff --git a/plugins/PluginBrowser/resources/qml/ToolboxLicenseDialog.qml b/plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/ToolboxLicenseDialog.qml rename to plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml diff --git a/plugins/PluginBrowser/resources/qml/ToolboxRestartDialog.qml b/plugins/Toolbox/resources/qml/ToolboxRestartDialog.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/ToolboxRestartDialog.qml rename to plugins/Toolbox/resources/qml/ToolboxRestartDialog.qml diff --git a/plugins/PluginBrowser/resources/qml/ToolboxShowcase.qml b/plugins/Toolbox/resources/qml/ToolboxShowcase.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/ToolboxShowcase.qml rename to plugins/Toolbox/resources/qml/ToolboxShowcase.qml diff --git a/plugins/PluginBrowser/resources/qml/ToolboxShowcaseTile.qml b/plugins/Toolbox/resources/qml/ToolboxShowcaseTile.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/ToolboxShowcaseTile.qml rename to plugins/Toolbox/resources/qml/ToolboxShowcaseTile.qml diff --git a/plugins/PluginBrowser/resources/qml/ToolboxViewDetail.qml b/plugins/Toolbox/resources/qml/ToolboxViewDetail.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/ToolboxViewDetail.qml rename to plugins/Toolbox/resources/qml/ToolboxViewDetail.qml diff --git a/plugins/PluginBrowser/resources/qml/ToolboxViewDownloads.qml b/plugins/Toolbox/resources/qml/ToolboxViewDownloads.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/ToolboxViewDownloads.qml rename to plugins/Toolbox/resources/qml/ToolboxViewDownloads.qml diff --git a/plugins/PluginBrowser/resources/qml/ToolboxViewInstalled.qml b/plugins/Toolbox/resources/qml/ToolboxViewInstalled.qml similarity index 100% rename from plugins/PluginBrowser/resources/qml/ToolboxViewInstalled.qml rename to plugins/Toolbox/resources/qml/ToolboxViewInstalled.qml diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index 7002711614..cb9299e69c 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -430,7 +430,7 @@ Item Action { id: browsePluginsAction - text: catalog.i18nc("@action:menu", "Browse plugins...") + text: catalog.i18nc("@action:menu", "Browse packages...") iconName: "plugins_browse" } diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index c4ebb790e8..db5be5ea25 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -270,7 +270,7 @@ UM.MainWindow Menu { id: plugin_menu - title: catalog.i18nc("@title:menu menubar:toplevel", "P&lugins") + title: catalog.i18nc("@title:menu menubar:toplevel", "&Toolbox") MenuItem { action: Cura.Actions.browsePlugins } } @@ -670,7 +670,7 @@ UM.MainWindow { target: Cura.Actions.browsePlugins onTriggered: { - curaExtensions.callExtensionMethod("Plugin Browser", "browsePlugins") + curaExtensions.callExtensionMethod("Toolbox", "browsePlugins") } } From 0416ba95ae8e70cb336755ef03a76acada8ab6c0 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 30 Mar 2018 14:16:51 +0200 Subject: [PATCH 2/7] CURA-5137 Add init file --- plugins/Toolbox/Toolbox/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 plugins/Toolbox/Toolbox/__init__.py diff --git a/plugins/Toolbox/Toolbox/__init__.py b/plugins/Toolbox/Toolbox/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 89c27ae7f4f6b6ad85f4e15d2a7517996f87a274 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 30 Mar 2018 15:13:10 +0200 Subject: [PATCH 3/7] CURA-5137 Get the list of packages from the server --- plugins/Toolbox/Toolbox/Toolbox.py | 19 +++++++++---------- resources/qml/Actions.qml | 4 ++-- resources/qml/Cura.qml | 6 +++--- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/plugins/Toolbox/Toolbox/Toolbox.py b/plugins/Toolbox/Toolbox/Toolbox.py index 9c64efc9fa..f1efcc6684 100644 --- a/plugins/Toolbox/Toolbox/Toolbox.py +++ b/plugins/Toolbox/Toolbox/Toolbox.py @@ -24,12 +24,13 @@ from cura.CuraApplication import CuraApplication i18n_catalog = i18nCatalog("cura") +## The Toolbox class is responsible of communicating with the server through the API class Toolbox(QObject, Extension): def __init__(self, parent=None): super().__init__(parent) - self._api_version = 4 - self._api_url = "http://software.ultimaker.com/cura/v%s/" % self._api_version + self._api_version = 1 + self._api_url = "https://api-staging.ultimaker.com/cura-packages/v%s/" % self._api_version self._plugin_list_request = None self._download_plugin_request = None @@ -119,18 +120,18 @@ class Toolbox(QObject, Extension): return self._is_downloading @pyqtSlot() - def browsePlugins(self): + def browsePackages(self): self._createNetworkManager() - self.requestPluginList() + self.requestPackageList() if not self._dialog: self._dialog = self._createDialog("PluginBrowser.qml") self._dialog.show() - @pyqtSlot() - def requestPluginList(self): - Logger.log("i", "Requesting plugin list") - url = QUrl(self._api_url + "plugins") + def requestPackageList(self): + cura_version = 4 + Logger.log("i", "Requesting package list") + url = QUrl(self._api_url + "packages?cura_version={version}".format(version = cura_version)) self._plugin_list_request = QNetworkRequest(url) self._plugin_list_request.setRawHeader(*self._request_header) self._network_manager.get(self._plugin_list_request) @@ -138,7 +139,6 @@ class Toolbox(QObject, Extension): def _createDialog(self, qml_name): Logger.log("d", "Creating dialog [%s]", qml_name) path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "resources", "qml", qml_name) - Logger.log("d", "Creating dialog [%s]", path) dialog = Application.getInstance().createQmlComponent(path, {"manager": self}) return dialog @@ -223,7 +223,6 @@ class Toolbox(QObject, Extension): self.openRestartDialog(result["message"]) self._restart_required = True self.restartRequiredChanged.emit() - # Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), result["message"]) @pyqtSlot(str) def removePlugin(self, plugin_id): diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index cb9299e69c..21e6eebf58 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -68,7 +68,7 @@ Item property alias configureSettingVisibility: configureSettingVisibilityAction - property alias browsePlugins: browsePluginsAction + property alias browsePackages: browsePackagesAction UM.I18nCatalog{id: catalog; name:"cura"} @@ -429,7 +429,7 @@ Item Action { - id: browsePluginsAction + id: browsePackagesAction text: catalog.i18nc("@action:menu", "Browse packages...") iconName: "plugins_browse" } diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index db5be5ea25..e20a29fe16 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -272,7 +272,7 @@ UM.MainWindow id: plugin_menu title: catalog.i18nc("@title:menu menubar:toplevel", "&Toolbox") - MenuItem { action: Cura.Actions.browsePlugins } + MenuItem { action: Cura.Actions.browsePackages } } Menu @@ -668,9 +668,9 @@ UM.MainWindow // show the plugin browser dialog Connections { - target: Cura.Actions.browsePlugins + target: Cura.Actions.browsePackages onTriggered: { - curaExtensions.callExtensionMethod("Toolbox", "browsePlugins") + curaExtensions.callExtensionMethod("Toolbox", "browsePackages") } } From 3d452d334b45afd43b23503eeaf7531df7d84c50 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 3 Apr 2018 11:21:15 +0200 Subject: [PATCH 4/7] CURA-5137 Create the Cura Packages Model for the Toolbox. Fetch the data from the new server and create the model for show in the toolbox. --- plugins/Toolbox/Toolbox/CuraPackageModel.py | 71 +++++++++++++++++++++ plugins/Toolbox/Toolbox/Toolbox.py | 53 ++++++++------- 2 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 plugins/Toolbox/Toolbox/CuraPackageModel.py diff --git a/plugins/Toolbox/Toolbox/CuraPackageModel.py b/plugins/Toolbox/Toolbox/CuraPackageModel.py new file mode 100644 index 0000000000..f8d6174d66 --- /dev/null +++ b/plugins/Toolbox/Toolbox/CuraPackageModel.py @@ -0,0 +1,71 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +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 CuraPackageModel(ListModel): + IdRole = Qt.UserRole + 1 + 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 + + def __init__(self, parent = None): + super().__init__(parent) + + self._packages_metadata = None + + self.addRoleName(CuraPackageModel.IdRole, "id") + self.addRoleName(CuraPackageModel.TypeRole, "type") + self.addRoleName(CuraPackageModel.NameRole, "name") + self.addRoleName(CuraPackageModel.VersionRole, "version") + self.addRoleName(CuraPackageModel.AuthorRole, "author") + self.addRoleName(CuraPackageModel.DescriptionRole, "description") + self.addRoleName(CuraPackageModel.IconURLRole, "icon_url") + self.addRoleName(CuraPackageModel.ImageURLsRole, "image_urls") + + # List of filters for queries. The result is the union of the each list of results. + self._filter = None # type: Dict[str,str] + + def setPackagesMetaData(self, data): + self._packages_metadata = data + self._update() + + def _update(self): + items = [] + + for package in self._packages_metadata: + items.append({ + "id": package["package_id"], + "type": package["package_type"], + "name": package["display_name"], + "version": package["package_version"], + "author": package["author"], + "description": package["description"], + "icon_url": package["icon_url"] if "icon_url" in package else None, + "image_urls": package["image_urls"] + }) + + items.sort(key = lambda k: k["name"]) + self.setItems(items) + + filterChanged = pyqtSignal() + + ## 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.filterChanged.emit() + + @pyqtProperty("QVariantMap", fset = setFilter, notify = filterChanged) + def filter(self) -> Dict[str, str]: + return self._filter diff --git a/plugins/Toolbox/Toolbox/Toolbox.py b/plugins/Toolbox/Toolbox/Toolbox.py index f1efcc6684..858a21d18a 100644 --- a/plugins/Toolbox/Toolbox/Toolbox.py +++ b/plugins/Toolbox/Toolbox/Toolbox.py @@ -1,5 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Toolbox is released under the terms of the LGPLv3 or higher. +from typing import Dict from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply @@ -21,6 +22,7 @@ import platform import zipfile from cura.CuraApplication import CuraApplication +from .CuraPackageModel import CuraPackageModel i18n_catalog = i18nCatalog("cura") @@ -32,16 +34,17 @@ class Toolbox(QObject, Extension): self._api_version = 1 self._api_url = "https://api-staging.ultimaker.com/cura-packages/v%s/" % self._api_version - self._plugin_list_request = None + self._package_list_request = None self._download_plugin_request = None self._download_plugin_reply = None self._network_manager = None self._plugin_registry = Application.getInstance().getPluginRegistry() + self._packages_version_number = self._plugin_registry.APIVersion - self._plugins_metadata = [] - self._plugins_model = None + self._packages_metadata = [] + self._packages_model = None # Can be 'installed' or 'available' self._view = "available" @@ -82,7 +85,7 @@ class Toolbox(QObject, Extension): showLicenseDialog = pyqtSignal() showRestartDialog = pyqtSignal() - pluginsMetadataChanged = pyqtSignal() + packagesMetadataChanged = pyqtSignal() onDownloadProgressChanged = pyqtSignal() onIsDownloadingChanged = pyqtSignal() restartRequiredChanged = pyqtSignal() @@ -129,12 +132,11 @@ class Toolbox(QObject, Extension): self._dialog.show() def requestPackageList(self): - cura_version = 4 Logger.log("i", "Requesting package list") - url = QUrl(self._api_url + "packages?cura_version={version}".format(version = cura_version)) - self._plugin_list_request = QNetworkRequest(url) - self._plugin_list_request.setRawHeader(*self._request_header) - self._network_manager.get(self._plugin_list_request) + url = QUrl("{base_url}packages?cura_version={version}".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) def _createDialog(self, qml_name): Logger.log("d", "Creating dialog [%s]", qml_name) @@ -218,7 +220,7 @@ class Toolbox(QObject, Extension): result = PluginRegistry.getInstance().installPlugin("file://" + location) self._newly_installed_plugin_ids.append(result["id"]) - self.pluginsMetadataChanged.emit() + self.packagesMetadataChanged.emit() self.openRestartDialog(result["message"]) self._restart_required = True @@ -229,7 +231,7 @@ class Toolbox(QObject, Extension): result = PluginRegistry.getInstance().uninstallPlugin(plugin_id) self._newly_uninstalled_plugin_ids.append(result["id"]) - self.pluginsMetadataChanged.emit() + self.packagesMetadataChanged.emit() self._restart_required = True self.restartRequiredChanged.emit() @@ -239,13 +241,13 @@ class Toolbox(QObject, Extension): @pyqtSlot(str) def enablePlugin(self, plugin_id): self._plugin_registry.enablePlugin(plugin_id) - self.pluginsMetadataChanged.emit() + self.packagesMetadataChanged.emit() Logger.log("i", "%s was set as 'active'", id) @pyqtSlot(str) def disablePlugin(self, plugin_id): self._plugin_registry.disablePlugin(plugin_id) - self.pluginsMetadataChanged.emit() + self.packagesMetadataChanged.emit() Logger.log("i", "%s was set as 'deactive'", id) @pyqtProperty(int, notify = onDownloadProgressChanged) @@ -283,7 +285,7 @@ class Toolbox(QObject, Extension): def setView(self, view = "available"): self._view = view self.viewChanged.emit() - self.pluginsMetadataChanged.emit() + self.packagesMetadataChanged.emit() @pyqtProperty(str, notify = viewChanged) def viewing(self): @@ -293,13 +295,13 @@ class Toolbox(QObject, Extension): def setDetailView(self, item = ""): self._detail_view = item self.detailViewChanged.emit() - self.pluginsMetadataChanged.emit() + self.packagesMetadataChanged.emit() @pyqtProperty(str, notify = detailViewChanged) def detailView(self): return self._detail_view - @pyqtProperty(QObject, notify = pluginsMetadataChanged) + @pyqtProperty(QObject, notify = packagesMetadataChanged) def pluginsModel(self): self._plugins_model = PluginsModel(None, self._view) # self._plugins_model.update() @@ -311,20 +313,22 @@ class Toolbox(QObject, Extension): if self._checkCanUpgrade(plugin["id"], plugin["version"]): plugin["can_upgrade"] = True - for item in self._plugins_metadata: + for item in self._packages_metadata: if item["id"] == plugin["id"]: plugin["update_url"] = item["file_location"] return self._plugins_model - + @pyqtProperty(QObject, notify = packagesMetadataChanged) + def packagesModel(self): + return self._packages_model def _checkCanUpgrade(self, id, version): # TODO: This could maybe be done more efficiently using a dictionary... # Scan plugin server data for plugin with the given id: - for plugin in self._plugins_metadata: + for plugin in self._packages_metadata: if id == plugin["id"]: reg_version = Version(version) new_version = Version(plugin["version"]) @@ -374,14 +378,17 @@ class Toolbox(QObject, Extension): return if reply.operation() == QNetworkAccessManager.GetOperation: - if reply_url == self._api_url + "plugins": + if reply_url == "{base_url}packages?cura_version={version}".format(base_url = self._api_url, version = self._packages_version_number): try: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) # Add metadata to the manager: - self._plugins_metadata = json_data - self._plugin_registry.addExternalPlugins(self._plugins_metadata) - self.pluginsMetadataChanged.emit() + self._packages_metadata = json_data + if not self._packages_model: + self._packages_model = CuraPackageModel() + self._packages_model.setPackagesMetaData(self._packages_metadata["data"]) + # self._plugin_registry.addExternalPlugins(self._packages_metadata) + self.packagesMetadataChanged.emit() except json.decoder.JSONDecodeError: Logger.log("w", "Received an invalid print job state message: Not valid JSON.") return From b621b5ef6b5fda7c36837df8a4542a4f4c411ec4 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 3 Apr 2018 17:07:33 +0200 Subject: [PATCH 5/7] CURA-5137 Add filtering for materials and plugins and change the behavior of switching tabs. --- plugins/Toolbox/Toolbox/CuraPackageModel.py | 41 ++++++++++++++++--- plugins/Toolbox/Toolbox/Toolbox.py | 40 ++++++++++-------- .../Toolbox/resources/qml/PluginBrowser.qml | 10 ++--- plugins/Toolbox/resources/qml/ToolboxGrid.qml | 13 +++--- .../Toolbox/resources/qml/ToolboxGridTile.qml | 8 ++-- .../Toolbox/resources/qml/ToolboxHeader.qml | 20 ++++++--- .../resources/qml/ToolboxViewDetail.qml | 4 +- 7 files changed, 85 insertions(+), 51 deletions(-) diff --git a/plugins/Toolbox/Toolbox/CuraPackageModel.py b/plugins/Toolbox/Toolbox/CuraPackageModel.py index f8d6174d66..facf21cc6f 100644 --- a/plugins/Toolbox/Toolbox/CuraPackageModel.py +++ b/plugins/Toolbox/Toolbox/CuraPackageModel.py @@ -1,6 +1,7 @@ # 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 @@ -33,7 +34,7 @@ class CuraPackageModel(ListModel): self.addRoleName(CuraPackageModel.ImageURLsRole, "image_urls") # List of filters for queries. The result is the union of the each list of results. - self._filter = None # type: Dict[str,str] + self._filter = {} # type: Dict[str,str] def setPackagesMetaData(self, data): self._packages_metadata = data @@ -54,18 +55,46 @@ class CuraPackageModel(ListModel): "image_urls": package["image_urls"] }) - items.sort(key = lambda k: k["name"]) - self.setItems(items) + # 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) - filterChanged = pyqtSignal() + # 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.filterChanged.emit() + self._update() - @pyqtProperty("QVariantMap", fset = setFilter, notify = filterChanged) + @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/Toolbox/Toolbox.py b/plugins/Toolbox/Toolbox/Toolbox.py index 858a21d18a..c1cdcd5741 100644 --- a/plugins/Toolbox/Toolbox/Toolbox.py +++ b/plugins/Toolbox/Toolbox/Toolbox.py @@ -43,12 +43,12 @@ class Toolbox(QObject, Extension): self._plugin_registry = Application.getInstance().getPluginRegistry() self._packages_version_number = self._plugin_registry.APIVersion - self._packages_metadata = [] - self._packages_model = None + self._packages_metadata = [] # Stores the remote information of the packages + self._packages_model = None # Model that list the remote available packages - # Can be 'installed' or 'available' - self._view = "available" - self._detail_view = "" + # Nowadays can be 'plugins', 'materials' or 'installed' + self._current_view = "plugins" + self._detail_view = False self._restart_required = False @@ -91,6 +91,7 @@ class Toolbox(QObject, Extension): restartRequiredChanged = pyqtSignal() viewChanged = pyqtSignal() detailViewChanged = pyqtSignal() + filterChanged = pyqtSignal() @pyqtSlot(result = str) def getLicenseDialogPluginName(self): @@ -282,28 +283,31 @@ class Toolbox(QObject, Extension): self.setIsDownloading(False) @pyqtSlot(str) - def setView(self, view = "available"): - self._view = view + def filterPackagesByType(self, type): + if not self._packages_model: + return + self._packages_model.setFilter({"type": type}) + self.filterChanged.emit() + + def setCurrentView(self, view = "plugins"): + self._current_view = view self.viewChanged.emit() - self.packagesMetadataChanged.emit() - @pyqtProperty(str, notify = viewChanged) - def viewing(self): - return self._view + @pyqtProperty(str, fset = setCurrentView, notify = viewChanged) + def currentView(self): + return self._current_view - @pyqtSlot(str) - def setDetailView(self, item = ""): + def setDetailView(self, item = False): self._detail_view = item self.detailViewChanged.emit() - self.packagesMetadataChanged.emit() - @pyqtProperty(str, notify = detailViewChanged) + @pyqtProperty(bool, fset = setDetailView, notify = detailViewChanged) def detailView(self): return self._detail_view @pyqtProperty(QObject, notify = packagesMetadataChanged) def pluginsModel(self): - self._plugins_model = PluginsModel(None, self._view) + self._plugins_model = PluginsModel(None, self._current_view) # self._plugins_model.update() # Check each plugin the registry for matching plugin from server @@ -383,10 +387,10 @@ class Toolbox(QObject, Extension): json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) # Add metadata to the manager: - self._packages_metadata = json_data + self._packages_metadata = json_data["data"] if not self._packages_model: self._packages_model = CuraPackageModel() - self._packages_model.setPackagesMetaData(self._packages_metadata["data"]) + self._packages_model.setPackagesMetaData(self._packages_metadata) # self._plugin_registry.addExternalPlugins(self._packages_metadata) self.packagesMetadataChanged.emit() except json.decoder.JSONDecodeError: diff --git a/plugins/Toolbox/resources/qml/PluginBrowser.qml b/plugins/Toolbox/resources/qml/PluginBrowser.qml index 643d26bfbe..a45f717d22 100644 --- a/plugins/Toolbox/resources/qml/PluginBrowser.qml +++ b/plugins/Toolbox/resources/qml/PluginBrowser.qml @@ -14,7 +14,8 @@ import UM 1.1 as UM Window { id: base - title: catalog.i18nc("@title:tab", "Plugins"); + title: catalog.i18nc("@title:tab", "Toolbox"); + modality: Qt.ApplicationModal width: 800 * screenScaleFactor height: 640 * screenScaleFactor minimumWidth: 800 * screenScaleFactor @@ -42,18 +43,17 @@ Window ToolboxViewDownloads { id: viewDownloads - visible: manager.viewing == "available" && manager.detailView == "" ? true : false + visible: manager.currentView != "installed" && !manager.detailView } - ToolboxViewDetail { id: viewDetail - visible: manager.viewing == "available" && manager.detailView != "" ? true : false + visible: manager.currentView != "installed" && manager.detailView } ToolboxViewInstalled { id: installedPluginList - visible: manager.viewing == "installed" ? true : false + visible: manager.currentView == "installed" } } SectionShadow diff --git a/plugins/Toolbox/resources/qml/ToolboxGrid.qml b/plugins/Toolbox/resources/qml/ToolboxGrid.qml index 53c4443493..1ec87e8217 100644 --- a/plugins/Toolbox/resources/qml/ToolboxGrid.qml +++ b/plugins/Toolbox/resources/qml/ToolboxGrid.qml @@ -39,13 +39,10 @@ Rectangle columnSpacing: UM.Theme.getSize("base_unit").width rowSpacing: UM.Theme.getSize("base_unit").height - ToolboxGridTile {} - ToolboxGridTile {} - ToolboxGridTile {} - ToolboxGridTile {} - ToolboxGridTile {} - ToolboxGridTile {} - ToolboxGridTile {} - ToolboxGridTile {} + Repeater + { + model: manager.packagesModel + delegate: ToolboxGridTile {} + } } } diff --git a/plugins/Toolbox/resources/qml/ToolboxGridTile.qml b/plugins/Toolbox/resources/qml/ToolboxGridTile.qml index 160e59bf15..36c7b93223 100644 --- a/plugins/Toolbox/resources/qml/ToolboxGridTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxGridTile.qml @@ -33,7 +33,7 @@ Item Label { id: name - text: "Auto Orientation" + text: model.name width: parent.width wrapMode: Text.WordWrap height: UM.Theme.getSize("base_unit").height * 2 @@ -44,7 +44,7 @@ Item Label { id: info - text: "Automatically orientate your model." + text: model.description width: parent.width wrapMode: Text.WordWrap color: UM.Theme.getColor("text_medium") @@ -55,8 +55,6 @@ Item MouseArea { anchors.fill: parent - onClicked: { - manager.setDetailView("thingy") - } + onClicked: manager.detailView = true } } diff --git a/plugins/Toolbox/resources/qml/ToolboxHeader.qml b/plugins/Toolbox/resources/qml/ToolboxHeader.qml index cce078035d..106b988e9e 100644 --- a/plugins/Toolbox/resources/qml/ToolboxHeader.qml +++ b/plugins/Toolbox/resources/qml/ToolboxHeader.qml @@ -32,7 +32,7 @@ Rectangle { implicitWidth: 96 implicitHeight: 48 Rectangle { - visible: manager.viewing == "available" ? true : false + visible: manager.currentView == "plugins" color: UM.Theme.getColor("primary") anchors.bottom: parent.bottom width: parent.width @@ -49,7 +49,11 @@ Rectangle { horizontalAlignment: Text.AlignHCenter } } - onClicked: manager.setView("available") + onClicked: + { + manager.filterPackagesByType("plugin") + manager.currentView = "plugins" + } } Button { @@ -60,7 +64,7 @@ Rectangle { implicitWidth: 96 implicitHeight: 48 Rectangle { - visible: manager.viewing == "available" ? true : false + visible: manager.currentView == "materials" color: UM.Theme.getColor("primary") anchors.bottom: parent.bottom width: parent.width @@ -77,7 +81,11 @@ Rectangle { horizontalAlignment: Text.AlignHCenter } } - onClicked: manager.setView("available") + onClicked: + { + manager.filterPackagesByType("material") + manager.currentView = "materials" + } } } @@ -91,7 +99,7 @@ Rectangle { implicitWidth: 96 implicitHeight: 48 Rectangle { - visible: manager.viewing == "installed" ? true : false + visible: manager.currentView == "installed" color: UM.Theme.getColor("primary") anchors.bottom: parent.bottom width: parent.width @@ -108,6 +116,6 @@ Rectangle { horizontalAlignment: Text.AlignHCenter } } - onClicked: manager.setView("installed") + onClicked: manager.currentView = "installed" } } diff --git a/plugins/Toolbox/resources/qml/ToolboxViewDetail.qml b/plugins/Toolbox/resources/qml/ToolboxViewDetail.qml index 10ad984bdc..c0fe5790dd 100644 --- a/plugins/Toolbox/resources/qml/ToolboxViewDetail.qml +++ b/plugins/Toolbox/resources/qml/ToolboxViewDetail.qml @@ -29,9 +29,7 @@ Item Button { text: "Back" - onClicked: { - manager.setDetailView("") - } + onClicked: manager.detailView = false } } ScrollView From 0e01e9a6a9e532ce6a1822cd747e841c043b85ae Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 3 Apr 2018 17:24:05 +0200 Subject: [PATCH 6/7] CURA-5137 Reset to the global view when switching tabs --- plugins/Toolbox/Toolbox/Toolbox.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Toolbox/Toolbox/Toolbox.py b/plugins/Toolbox/Toolbox/Toolbox.py index c1cdcd5741..b799754cbf 100644 --- a/plugins/Toolbox/Toolbox/Toolbox.py +++ b/plugins/Toolbox/Toolbox/Toolbox.py @@ -291,6 +291,7 @@ class Toolbox(QObject, Extension): def setCurrentView(self, view = "plugins"): self._current_view = view + self._detail_view = False self.viewChanged.emit() @pyqtProperty(str, fset = setCurrentView, notify = viewChanged) From 2bf6615b53509ab1f997029f485a205940dc8e48 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 4 Apr 2018 10:51:19 +0200 Subject: [PATCH 7/7] CURA-5137 Make some adjustments in the UI. --- .../Toolbox/resources/qml/PluginBrowser.qml | 5 +--- plugins/Toolbox/resources/qml/ToolboxGrid.qml | 20 ++++++-------- .../Toolbox/resources/qml/ToolboxGridTile.qml | 16 ++++++++---- .../Toolbox/resources/qml/ToolboxShowcase.qml | 19 ++------------ .../resources/qml/ToolboxViewDownloads.qml | 26 ++++++++++--------- 5 files changed, 36 insertions(+), 50 deletions(-) diff --git a/plugins/Toolbox/resources/qml/PluginBrowser.qml b/plugins/Toolbox/resources/qml/PluginBrowser.qml index ad69f70c98..c2ce4975f4 100644 --- a/plugins/Toolbox/resources/qml/PluginBrowser.qml +++ b/plugins/Toolbox/resources/qml/PluginBrowser.qml @@ -12,7 +12,7 @@ import QtQuick.Controls.Styles 1.4 import UM 1.1 as UM Window - { +{ id: base title: catalog.i18nc("@title:tab", "Toolbox"); modality: Qt.ApplicationModal @@ -75,9 +75,6 @@ Window } } - - - UM.I18nCatalog { id: catalog; name: "cura" } Connections diff --git a/plugins/Toolbox/resources/qml/ToolboxGrid.qml b/plugins/Toolbox/resources/qml/ToolboxGrid.qml index 1ec87e8217..115638bd71 100644 --- a/plugins/Toolbox/resources/qml/ToolboxGrid.qml +++ b/plugins/Toolbox/resources/qml/ToolboxGrid.qml @@ -11,30 +11,23 @@ import UM 1.1 as UM // TODO: Switch to QtQuick.Controls 2.x and remove QtQuick.Controls.Styles -Rectangle +Column { id: base - width: parent.width - height: childrenRect.height + UM.Theme.getSize("double_margin").height * 8 - color: "transparent" + height: childrenRect.height + spacing: UM.Theme.getSize("base_unit").height Label { id: heading text: "Community Plugins" width: parent.width - height: UM.Theme.getSize("base_unit").width * 4 - verticalAlignment: Text.AlignVCenter color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("medium") } GridLayout { id: grid - width: base.width - anchors - { - top: heading.bottom - } + width: parent.width columns: 3 columnSpacing: UM.Theme.getSize("base_unit").width rowSpacing: UM.Theme.getSize("base_unit").height @@ -42,7 +35,10 @@ Rectangle Repeater { model: manager.packagesModel - delegate: ToolboxGridTile {} + delegate: ToolboxGridTile + { + Layout.preferredWidth: (grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns + } } } } diff --git a/plugins/Toolbox/resources/qml/ToolboxGridTile.qml b/plugins/Toolbox/resources/qml/ToolboxGridTile.qml index 36c7b93223..67c04a4cc7 100644 --- a/plugins/Toolbox/resources/qml/ToolboxGridTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxGridTile.qml @@ -13,7 +13,7 @@ Item { id: base height: childrenRect.height - Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignLeft Row { width: parent.width @@ -29,22 +29,28 @@ Item } Column { - width: UM.Theme.getSize("base_unit").width * 12 + width: parent.width - thumbnail.width - parent.spacing + spacing: Math.floor(UM.Theme.getSize("base_unit").width / 2) Label { id: name text: model.name width: parent.width wrapMode: Text.WordWrap - height: UM.Theme.getSize("base_unit").height * 2 - verticalAlignment: Text.AlignVCenter color: UM.Theme.getColor("text") font: UM.Theme.getFont("default_bold") } Label { id: info - text: model.description + text: + { + if (model.description.length > 50) + { + return model.description.substring(0, 50) + "..." + } + return model.description + } width: parent.width wrapMode: Text.WordWrap color: UM.Theme.getColor("text_medium") diff --git a/plugins/Toolbox/resources/qml/ToolboxShowcase.qml b/plugins/Toolbox/resources/qml/ToolboxShowcase.qml index f5aec67e09..5abf14cecc 100644 --- a/plugins/Toolbox/resources/qml/ToolboxShowcase.qml +++ b/plugins/Toolbox/resources/qml/ToolboxShowcase.qml @@ -10,45 +10,30 @@ import UM 1.1 as UM // TODO: Switch to QtQuick.Controls 2.x and remove QtQuick.Controls.Styles -Rectangle +Column { id: base - width: parent.width height: childrenRect.height - color: "transparent" + spacing: UM.Theme.getSize("base_unit").height Label { id: heading text: "Top Downloads" width: parent.width - height: UM.Theme.getSize("base_unit").width * 4 - verticalAlignment: Text.AlignVCenter color: UM.Theme.getColor("text_medium") font: UM.Theme.getFont("medium") } Row { height: childrenRect.height - width: childrenRect.width spacing: UM.Theme.getSize("base_unit").width * 2 anchors { horizontalCenter: parent.horizontalCenter - top: heading.bottom } ToolboxShowcaseTile {} ToolboxShowcaseTile {} ToolboxShowcaseTile {} } - Rectangle - { - color: UM.Theme.getColor("text_medium") - width: parent.width - height: UM.Theme.getSize("base_unit").height / 6 - anchors - { - bottom: parent.bottom - } - } } diff --git a/plugins/Toolbox/resources/qml/ToolboxViewDownloads.qml b/plugins/Toolbox/resources/qml/ToolboxViewDownloads.qml index 55e3650488..4141685787 100644 --- a/plugins/Toolbox/resources/qml/ToolboxViewDownloads.qml +++ b/plugins/Toolbox/resources/qml/ToolboxViewDownloads.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // PluginBrowser is released under the terms of the LGPLv3 or higher. -import QtQuick 2.2 +import QtQuick 2.7 import QtQuick.Dialogs 1.1 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 @@ -12,29 +12,31 @@ import UM 1.1 as UM ScrollView { id: base - frameVisible: false - anchors.fill: parent + frameVisible: true + width: parent.width + height: parent.height style: UM.Theme.styles.scrollview Column { width: base.width spacing: UM.Theme.getSize("base_unit").height - height: childrenRect.height - anchors - { - fill: parent - topMargin: UM.Theme.getSize("base_unit").height - bottomMargin: UM.Theme.getSize("base_unit").height - leftMargin: UM.Theme.getSize("base_unit").width * 2 - rightMargin: UM.Theme.getSize("base_unit").width * 2 - } + padding: UM.Theme.getSize("base_unit").height * 2 + height: childrenRect.height + 2 * padding ToolboxShowcase { id: showcase + width: parent.width - 2 * parent.padding + } + Rectangle + { + color: UM.Theme.getColor("text_medium") + width: parent.width - 2 * parent.padding + height: UM.Theme.getSize("base_unit").height / 6 } ToolboxGrid { id: allPlugins + width: parent.width - 2 * parent.padding } } }