diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py index e57a402fd6..0e73cac3b7 100644 --- a/plugins/Marketplace/PackageModel.py +++ b/plugins/Marketplace/PackageModel.py @@ -4,10 +4,12 @@ from PyQt5.QtCore import pyqtProperty, QObject from typing import Any, Dict, Optional -from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. +from UM.Util import parseBool +from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. catalog = i18nCatalog("cura") + class PackageModel(QObject): """ Represents a package, containing all the relevant information to be displayed about a package. @@ -25,17 +27,65 @@ class PackageModel(QObject): """ super().__init__(parent) self._package_id = package_data.get("package_id", "UnknownPackageId") + + self._icon_url = package_data.get("icon_url", "") self._display_name = package_data.get("display_name", catalog.i18nc("@label:property", "Unknown Package")) + self._is_verified = "verified" in package_data.get("tags", []) + self._package_version = package_data.get("package_version", "") # Display purpose, no need for 'UM.Version'. + self._package_info_url = package_data.get("website", "") # Not to be confused with 'download_url'. + self._download_count = package_data.get("download_count", 0) + self._description = package_data.get("description", "") + + self._download_url = package_data.get("download_url", "") # Not used yet, will be. + self._release_notes = package_data.get("release_notes", "") # Not used yet, propose to add to description? + + author_data = package_data.get("author", {}) + self._author_name = author_data.get("display_name", catalog.i18nc("@label:property", "Unknown Author")) + self._author_info_url = author_data.get("website", "") + self._section_title = section_title + # Note that there's a lot more info in the package_data than just these specified here. @pyqtProperty(str, constant = True) def packageId(self) -> str: return self._package_id + @pyqtProperty(str, constant=True) + def iconUrl(self): + return self._icon_url + @pyqtProperty(str, constant = True) def displayName(self) -> str: return self._display_name + @pyqtProperty(bool, constant=True) + def isVerified(self): + return self._is_verified + + @pyqtProperty(str, constant=True) + def packageVersion(self): + return self._package_version + + @pyqtProperty(str, constant=True) + def packageInfoUrl(self): + return self._package_info_url + + @pyqtProperty(int, constant=True) + def downloadCount(self): + return self._download_count + + @pyqtProperty(str, constant=True) + def description(self): + return self._description + + @pyqtProperty(str, constant=True) + def authorName(self): + return self._author_name + + @pyqtProperty(str, constant=True) + def authorInfoUrl(self): + return self._author_info_url + @pyqtProperty(str, constant = True) def sectionTitle(self) -> Optional[str]: return self._section_title diff --git a/plugins/Marketplace/resources/images/placeholder.svg b/plugins/Marketplace/resources/images/placeholder.svg new file mode 100644 index 0000000000..cc674a4b38 --- /dev/null +++ b/plugins/Marketplace/resources/images/placeholder.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml new file mode 100644 index 0000000000..3a185a3e02 --- /dev/null +++ b/plugins/Marketplace/resources/qml/PackageCard.qml @@ -0,0 +1,351 @@ +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.1 + +import UM 1.6 as UM +import Cura 1.6 as Cura + +Rectangle +{ + property var packageData + + width: parent ? parent.width - UM.Theme.getSize("default_margin").width : 0 + height: childrenRect.height + + color: UM.Theme.getColor("main_background") + radius: UM.Theme.getSize("default_radius").width + + states: + [ + State + { + name: "Folded" + when: true // TODO + PropertyChanges + { + target: downloadCountRow + visible: false + } + PropertyChanges + { + target: descriptionArea + visible: true + } + }, + State + { + name: "Header" + when: false // TODO + PropertyChanges + { + target: downloadCountRow + visible: true + } + PropertyChanges + { + target: descriptionArea + visible: false + } + } + ] + + RowLayout + { + width: parent.width - UM.Theme.getSize("thin_margin").width * 2 + height: childrenRect.height + UM.Theme.getSize("thin_margin").height * 2 + x: UM.Theme.getSize("thin_margin").width + y: UM.Theme.getSize("thin_margin").height + + spacing: UM.Theme.getSize("default_margin").width + + Image //Separate column for icon on the left. + { + Layout.preferredWidth: UM.Theme.getSize("card_icon").width + Layout.preferredHeight: UM.Theme.getSize("card_icon").height + Layout.alignment: Qt.AlignTop + + source: packageData.iconUrl != "" ? packageData.iconUrl : "../images/placeholder.svg" + } + + Column + { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + + RowLayout //Title row. + { + Layout.alignment: Qt.AlignTop + width: parent.width + + Label + { + Layout.alignment: Qt.AlignTop + + text: packageData.displayName + font: UM.Theme.getFont("medium_bold") + color: UM.Theme.getColor("text") + } + + Row //Row inside row, but the non-layout version skips invisible elements. + { + spacing: parent.spacing + Layout.alignment: Qt.AlignTop + + Control + { + width: UM.Theme.getSize("card_tiny_icon").width + height: UM.Theme.getSize("card_tiny_icon").height + Layout.alignment: Qt.AlignTop + + enabled: packageData.isVerified + visible: packageData.isVerified + + Cura.ToolTip + { + tooltipText: catalog.i18nc("@info", "Verified") + visible: parent.hovered + } + + UM.RecolorImage + { + anchors.fill: parent + + color: UM.Theme.getColor("primary") + source: UM.Theme.getIcon("CheckCircle") + } + + //NOTE: Can we link to something here? (Probably a static link explaining what verified is): + // onClicked: Qt.openUrlExternally( XXXXXX ) + } + + Control + { + width: UM.Theme.getSize("card_tiny_icon").width + height: UM.Theme.getSize("card_tiny_icon").height + Layout.alignment: Qt.AlignTop + + enabled: false // remove! + visible: false // replace packageInfo.XXXXXX + // TODO: waiting for materials card implementation + + Cura.ToolTip + { + tooltipText: "" // TODO + visible: parent.hovered + } + + UM.RecolorImage + { + anchors.fill: parent + + color: UM.Theme.getColor("primary") + source: UM.Theme.getIcon("CheckCircle") // TODO + } + + // onClicked: Qt.openUrlExternally( XXXXXX ) // TODO + } + } + + Label + { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + + text: packageData.packageVersion + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + } + + Button + { + id: externalLinkButton + + Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").height + Layout.alignment: Qt.AlignTop + + Rectangle + { + anchors.fill: parent + color: externalLinkButton.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("detail_background") + + UM.RecolorImage + { + anchors.fill: parent + color: externalLinkButton.hovered ? UM.Theme.getColor("text_link") : UM.Theme.getColor("text") + source: UM.Theme.getIcon("LinkExternal") + } + } + + onClicked: Qt.openUrlExternally(packageData.packageInfoUrl) + } + } + + RowLayout + { + id: downloadCountRow + + width: childrenRect.width + height: childrenRect.height + x: UM.Theme.getSize("thin_margin").width + y: UM.Theme.getSize("thin_margin").height + + spacing: UM.Theme.getSize("thin_margin").width + + UM.RecolorImage + { + id: downloadCountIcon + width: UM.Theme.getSize("card_tiny_icon").width + height: UM.Theme.getSize("card_tiny_icon").height + color: UM.Theme.getColor("icon") + + source: UM.Theme.getIcon("Download") + } + + Label + { + id: downloadCountLabel + text: packageData.downloadCount + } + } + + Item + { + id: descriptionArea + width: parent.width + height: descriptionLabel.height + + Label + { + id: descriptionLabel + width: parent.width + property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision. + + text: packageData.description + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Text.ElideRight + + onLineLaidOut: + { + if(truncated && line.isLast) + { + let max_line_width = parent.width - readMoreButton.width - fontMetrics.advanceWidth("… ") - UM.Theme.getSize("default_margin").width; + if(line.implicitWidth > max_line_width) + { + line.width = max_line_width; + } + else + { + line.width = line.implicitWidth - fontMetrics.advanceWidth("…"); //Truncate the ellipsis. We're adding this ourselves. + } + descriptionLabel.lastLineWidth = line.implicitWidth; + } + } + } + + Cura.TertiaryButton + { + id: readMoreButton + anchors.right: parent.right + anchors.bottom: parent.bottom + height: fontMetrics.height //Height of a single line. + + text: catalog.i18nc("@info", "Read more") + iconSource: UM.Theme.getIcon("LinkExternal") + + visible: descriptionLabel.truncated + enabled: visible + leftPadding: UM.Theme.getSize("default_margin").width + rightPadding: UM.Theme.getSize("wide_margin").width + textFont: descriptionLabel.font + isIconOnRightSide: true + + // NOTE: Is this the right URL for this action? + onClicked: Qt.openUrlExternally(packageData.packageInfoUrl) + } + + Label + { + anchors.left: parent.left + anchors.leftMargin: descriptionLabel.lastLineWidth + anchors.bottom: readMoreButton.bottom + + text: "… " + font: descriptionLabel.font + color: descriptionLabel.color + visible: descriptionLabel.truncated + } + } + + RowLayout //Author and action buttons. + { + width: parent.width + Layout.alignment: Qt.AlignTop + spacing: UM.Theme.getSize("thin_margin").width + + Label + { + id: authorBy + Layout.alignment: Qt.AlignTop + + text: catalog.i18nc("@label", "By") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + } + + Cura.TertiaryButton + { + Layout.fillWidth: true + Layout.preferredHeight: authorBy.height + Layout.alignment: Qt.AlignTop + + text: packageData.authorName + textFont: UM.Theme.getFont("default_bold") + textColor: UM.Theme.getColor("text") // override normal link color + leftPadding: 0 + rightPadding: 0 + iconSource: UM.Theme.getIcon("LinkExternal") + isIconOnRightSide: true + + onClicked: Qt.openUrlExternally(packageData.authorInfoUrl) + } + + Cura.SecondaryButton + { + id: disableButton + Layout.alignment: Qt.AlignTop + text: catalog.i18nc("@button", "Disable") + visible: false // not functional right now, also only when unfolding and required + } + + Cura.SecondaryButton + { + id: uninstallButton + Layout.alignment: Qt.AlignTop + text: catalog.i18nc("@button", "Uninstall") + visible: false // not functional right now, also only when unfolding and required + } + + Cura.PrimaryButton + { + id: installButton + Layout.alignment: Qt.AlignTop + text: catalog.i18nc("@button", "Update") // OR Download, if new! + visible: false // not functional right now, also only when unfolding and required + } + } + } + } + + FontMetrics + { + id: fontMetrics + font: UM.Theme.getFont("medium") + } +} diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml index c9ddf88d16..95064c4469 100644 --- a/plugins/Marketplace/resources/qml/Packages.qml +++ b/plugins/Marketplace/resources/qml/Packages.qml @@ -46,24 +46,9 @@ ScrollView } } - delegate: Rectangle + delegate: PackageCard { - width: packagesListview.width - height: UM.Theme.getSize("card").height - - color: UM.Theme.getColor("main_background") - radius: UM.Theme.getSize("default_radius").width - - Label - { - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: Math.round((parent.height - height) / 2) - - text: model.package.displayName - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("text") - } + packageData: model.package } //Wrapper item to add spacing between content and footer. diff --git a/resources/qml/ToolTip.qml b/resources/qml/ToolTip.qml index 3157f81d89..c4edc5a361 100644 --- a/resources/qml/ToolTip.qml +++ b/resources/qml/ToolTip.qml @@ -38,7 +38,7 @@ ToolTip onAboutToHide: hide() // If the text is not set, just set the height to 0 to prevent it from showing - height: text != "" ? label.contentHeight + 2 * UM.Theme.getSize("thin_margin").width: 0 + height: label.contentHeight + 2 * UM.Theme.getSize("thin_margin").width x: { @@ -74,7 +74,7 @@ ToolTip } function show() { - opacity = 1 + opacity = text != "" ? 1 : 0 } function hide() { diff --git a/resources/themes/cura-light/icons/default/Download.svg b/resources/themes/cura-light/icons/default/Download.svg new file mode 100644 index 0000000000..cbe0da2a99 --- /dev/null +++ b/resources/themes/cura-light/icons/default/Download.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index abd4844f47..a4f3172036 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -553,7 +553,10 @@ "standard_list_lineheight": [1.5, 1.5], "standard_arrow": [1.0, 1.0], + "card": [25.0, 8.75], + "card_icon": [6.0, 6.0], + "card_tiny_icon": [1.2, 1.2], "button": [4, 4], "button_icon": [2.5, 2.5],