Merge remote-tracking branch 'origin/marketplace_redesign' into CURA-8587_disable_update_install_and_uninstall

# Conflicts:
#	plugins/Marketplace/LocalPackageList.py
#	plugins/Marketplace/PackageModel.py
#	plugins/Marketplace/RemotePackageList.py
#	plugins/Marketplace/resources/qml/Marketplace.qml
#	plugins/Marketplace/resources/qml/PackageCard.qml
#	plugins/Marketplace/resources/qml/Packages.qml
This commit is contained in:
Jelle Spijker 2021-12-03 13:36:56 +01:00
commit 4fef2de71c
No known key found for this signature in database
GPG key ID: 6662DC033BE6B99A
4 changed files with 739 additions and 277 deletions

View file

@ -1,11 +1,14 @@
# Copyright (c) 2021 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
from typing import Any, Dict, Optional import re
from typing import Any, Dict, List, Optional
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with.
from UM.Logger import Logger from UM.Logger import Logger
from UM.i18n import i18nCatalog # To translate placeholder names if data is not present. from UM.i18n import i18nCatalog # To translate placeholder names if data is not present.
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -39,10 +42,20 @@ class PackageModel(QObject):
self._package_info_url = package_data.get("website", "") # Not to be confused with 'download_url'. 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._download_count = package_data.get("download_count", 0)
self._description = package_data.get("description", "") self._description = package_data.get("description", "")
self._formatted_description = self._format(self._description)
self._download_url = package_data.get("download_url", "") # Not used yet, will be. self._download_url = package_data.get("download_url", "")
self._release_notes = package_data.get("release_notes", "") # Not used yet, propose to add to description? self._release_notes = package_data.get("release_notes", "") # Not used yet, propose to add to description?
subdata = package_data.get("data", {})
self._technical_data_sheet = self._findLink(subdata, "technical_data_sheet")
self._safety_data_sheet = self._findLink(subdata, "safety_data_sheet")
self._where_to_buy = self._findLink(subdata, "where_to_buy")
self._compatible_printers = self._getCompatiblePrinters(subdata)
self._compatible_support_materials = self._getCompatibleSupportMaterials(subdata)
self._is_compatible_material_station = self._isCompatibleMaterialStation(subdata)
self._is_compatible_air_manager = self._isCompatibleAirManager(subdata)
author_data = package_data.get("author", {}) author_data = package_data.get("author", {})
self._author_name = author_data.get("display_name", catalog.i18nc("@label:property", "Unknown Author")) self._author_name = author_data.get("display_name", catalog.i18nc("@label:property", "Unknown Author"))
self._author_info_url = author_data.get("website", "") self._author_info_url = author_data.get("website", "")
@ -53,6 +66,111 @@ class PackageModel(QObject):
self._section_title = section_title self._section_title = section_title
# Note that there's a lot more info in the package_data than just these specified here. # Note that there's a lot more info in the package_data than just these specified here.
def _findLink(self, subdata: Dict[str, Any], link_type: str) -> str:
"""
Searches the package data for a link of a certain type.
The links are not in a fixed path in the package data. We need to iterate over the available links to find them.
:param subdata: The "data" element in the package data, which should contain links.
:param link_type: The type of link to find.
:return: A URL of where the link leads, or an empty string if there is no link of that type in the package data.
"""
links = subdata.get("links", [])
for link in links:
if link.get("type", "") == link_type:
return link.get("url", "")
else:
return "" # No link with the correct type was found.
def _format(self, text: str) -> str:
"""
Formats a user-readable block of text for display.
:return: A block of rich text with formatting embedded.
"""
# Turn all in-line hyperlinks into actual links.
url_regex = re.compile(r"(((http|https)://)[a-zA-Z0-9@:%.\-_+~#?&/=]{2,256}\.[a-z]{2,12}(/[a-zA-Z0-9@:%.\-_+~#?&/=]*)?)")
text = re.sub(url_regex, r'<a href="\1">\1</a>', text)
# Turn newlines into <br> so that they get displayed as newlines when rendering as rich text.
text = text.replace("\n", "<br>")
return text
def _getCompatiblePrinters(self, subdata: Dict[str, Any]) -> List[str]:
"""
Gets the list of printers that this package provides material compatibility with.
Any printer is listed, even if it's only for a single nozzle on a single material in the package.
:param subdata: The "data" element in the package data, which should contain this compatibility information.
:return: A list of printer names that this package provides material compatibility with.
"""
result = set()
for material in subdata.get("materials", []):
for compatibility in material.get("compatibility", []):
printer_name = compatibility.get("machine_name")
if printer_name is None:
continue # Missing printer name information. Skip this one.
for subcompatibility in compatibility.get("compatibilities", []):
if subcompatibility.get("hardware_compatible", False):
result.add(printer_name)
break
return list(sorted(result))
def _getCompatibleSupportMaterials(self, subdata: Dict[str, Any]) -> List[str]:
"""
Gets the list of support materials that the materials in this package are compatible with.
Since the materials are individually encoded as keys in the API response, only PVA and Breakaway are currently
supported.
:param subdata: The "data" element in the package data, which should contain this compatibility information.
:return: A list of support materials that the materials in this package are compatible with.
"""
result = set()
container_registry = CuraContainerRegistry.getInstance()
try:
pva_name = container_registry.findContainersMetadata(id = "ultimaker_pva")[0].get("name", "Ultimaker PVA")
except IndexError:
pva_name = "Ultimaker PVA"
try:
breakaway_name = container_registry.findContainersMetadata(id = "ultimaker_bam")[0].get("name", "Ultimaker Breakaway")
except IndexError:
breakaway_name = "Ultimaker Breakaway"
for material in subdata.get("materials", []):
if material.get("pva_compatible", False):
result.add(pva_name)
if material.get("breakaway_compatible", False):
result.add(breakaway_name)
return list(sorted(result))
def _isCompatibleMaterialStation(self, subdata: Dict[str, Any]) -> bool:
"""
Finds out if this package provides any material that is compatible with the material station.
:param subdata: The "data" element in the package data, which should contain this compatibility information.
:return: Whether this package provides any material that is compatible with the material station.
"""
for material in subdata.get("materials", []):
for compatibility in material.get("compatibility", []):
if compatibility.get("material_station_optimized", False):
return True
return False
def _isCompatibleAirManager(self, subdata: Dict[str, Any]) -> bool:
"""
Finds out if this package provides any material that is compatible with the air manager.
:param subdata: The "data" element in the package data, which should contain this compatibility information.
:return: Whether this package provides any material that is compatible with the air manager.
"""
for material in subdata.get("materials", []):
for compatibility in material.get("compatibility", []):
if compatibility.get("air_manager_optimized", False):
return True
return False
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def packageId(self) -> str: def packageId(self) -> str:
return self._package_id return self._package_id
@ -89,6 +207,10 @@ class PackageModel(QObject):
def description(self): def description(self):
return self._description return self._description
@pyqtProperty(str, constant = True)
def formattedDescription(self) -> str:
return self._formatted_description
@pyqtProperty(str, constant=True) @pyqtProperty(str, constant=True)
def authorName(self): def authorName(self):
return self._author_name return self._author_name
@ -105,6 +227,34 @@ class PackageModel(QObject):
def sectionTitle(self) -> Optional[str]: def sectionTitle(self) -> Optional[str]:
return self._section_title return self._section_title
@pyqtProperty(str, constant = True)
def technicalDataSheet(self) -> str:
return self._technical_data_sheet
@pyqtProperty(str, constant = True)
def safetyDataSheet(self) -> str:
return self._safety_data_sheet
@pyqtProperty(str, constant = True)
def whereToBuy(self) -> str:
return self._where_to_buy
@pyqtProperty("QStringList", constant = True)
def compatiblePrinters(self) -> List[str]:
return self._compatible_printers
@pyqtProperty("QStringList", constant = True)
def compatibleSupportMaterials(self) -> List[str]:
return self._compatible_support_materials
@pyqtProperty(bool, constant = True)
def isCompatibleMaterialStation(self) -> bool:
return self._is_compatible_material_station
@pyqtProperty(bool, constant = True)
def isCompatibleAirManager(self) -> bool:
return self._is_compatible_air_manager
isInstalledChanged = pyqtSignal() isInstalledChanged = pyqtSignal()
@pyqtProperty(bool, notify = isInstalledChanged) @pyqtProperty(bool, notify = isInstalledChanged)

View file

@ -86,6 +86,15 @@ Window
} }
} }
OnboardBanner
{
visible: content.item && content.item.bannerVisible
text: content.item && content.item.bannerText
icon: content.item && content.item.bannerIcon
onRemove: content.item && content.item.onRemoveBanner
readMoreUrl: content.item && content.item.bannerReadMoreUrl
}
// Search & Top-Level Tabs // Search & Top-Level Tabs
Item Item
{ {
@ -167,6 +176,25 @@ Window
} }
} }
FontMetrics
{
id: fontMetrics
font: UM.Theme.getFont("default")
}
Cura.TertiaryButton
{
text: catalog.i18nc("@info", "Search in the browser")
iconSource: UM.Theme.getIcon("LinkExternal")
visible: pageSelectionTabBar.currentItem.hasSearch
isIconOnRightSide: true
height: fontMetrics.height
textFont: fontMetrics.font
textColor: UM.Theme.getColor("text")
onClicked: content.item && Qt.openUrlExternally(content.item.searchInBrowserUrl)
}
// Page contents. // Page contents.
Rectangle Rectangle
{ {

View file

@ -13,7 +13,7 @@ Rectangle
property var packageData property var packageData
property bool expanded: false property bool expanded: false
height: UM.Theme.getSize("card").height height: childrenRect.height
color: UM.Theme.getColor("main_background") color: UM.Theme.getColor("main_background")
radius: UM.Theme.getSize("default_radius").width radius: UM.Theme.getSize("default_radius").width
@ -25,9 +25,19 @@ Rectangle
when: !expanded when: !expanded
PropertyChanges PropertyChanges
{ {
target: descriptionArea target: shortDescription
visible: true visible: true
} }
PropertyChanges
{
target: downloadCount
visible: false
}
PropertyChanges
{
target: extendedDescription
visible: false
}
}, },
State State
{ {
@ -35,13 +45,33 @@ Rectangle
when: expanded when: expanded
PropertyChanges PropertyChanges
{ {
target: descriptionArea target: shortDescription
visible: false visible: false
} }
PropertyChanges
{
target: downloadCount
visible: true
}
PropertyChanges
{
target: extendedDescription
visible: true
}
} }
] ]
// Separate column for icon on the left. Column
{
width: parent.width
spacing: 0
Item
{
width: parent.width
height: UM.Theme.getSize("card").height
Image Image
{ {
id: packageItem id: packageItem
@ -57,19 +87,25 @@ Rectangle
source: packageData.iconUrl != "" ? packageData.iconUrl : "../images/placeholder.svg" source: packageData.iconUrl != "" ? packageData.iconUrl : "../images/placeholder.svg"
} }
ColumnLayout
{
anchors
{
left: packageItem.right
leftMargin: UM.Theme.getSize("default_margin").width
right: parent.right
rightMargin: UM.Theme.getSize("thick_margin").width
top: parent.top
topMargin: UM.Theme.getSize("narrow_margin").height
}
height: packageItem.height + packageItem.anchors.margins * 2
// Title row. // Title row.
RowLayout RowLayout
{ {
id: titleBar id: titleBar
anchors Layout.preferredWidth: parent.width
{ Layout.preferredHeight: childrenRect.height
left: packageItem.right
right: parent.right
top: parent.top
topMargin: UM.Theme.getSize("narrow_margin").height
leftMargin: UM.Theme.getSize("default_margin").width
rightMargin:UM.Theme.getSize("thick_margin").width
}
Label Label
{ {
@ -84,7 +120,6 @@ Rectangle
Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width
Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").height Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").height
enabled: packageData.isCheckedByUltimaker enabled: packageData.isCheckedByUltimaker
visible: packageData.isCheckedByUltimaker visible: packageData.isCheckedByUltimaker
@ -100,7 +135,7 @@ Rectangle
} }
} }
visible: parent.hovered visible: parent.hovered
targetPoint: Qt.point(0, Math.round(parent.y + parent.height / 2)) targetPoint: Qt.point(0, Math.round(parent.y + parent.height / 4))
} }
Rectangle Rectangle
@ -182,22 +217,14 @@ Rectangle
} }
onClicked: Qt.openUrlExternally(packageData.authorInfoUrl) onClicked: Qt.openUrlExternally(packageData.authorInfoUrl)
} }
} }
// Description area
Item Item
{ {
id: descriptionArea id: shortDescription
height: childrenRect.height > descriptionLabel.height ? childrenRect.height : descriptionLabel.height Layout.preferredWidth: parent.width
anchors Layout.fillHeight: true
{
top: titleBar.bottom
left: packageItem.right
right: parent.right
rightMargin: UM.Theme.getSize("default_margin").width
leftMargin: UM.Theme.getSize("default_margin").width
}
Label Label
{ {
id: descriptionLabel id: descriptionLabel
@ -205,11 +232,13 @@ Rectangle
property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision. property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision.
text: packageData.description text: packageData.description
textFormat: Text.PlainText //Must be plain text, or we won't get onLineLaidOut signals. Don't auto-detect!
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
maximumLineCount: 2 maximumLineCount: 2
wrapMode: Text.Wrap wrapMode: Text.Wrap
elide: Text.ElideRight elide: Text.ElideRight
visible: text !== ""
onLineLaidOut: onLineLaidOut:
{ {
@ -233,24 +262,24 @@ Rectangle
id: tripleDotLabel id: tripleDotLabel
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: descriptionLabel.lastLineWidth anchors.leftMargin: descriptionLabel.lastLineWidth
anchors.bottom: readMoreButton.bottom anchors.bottom: descriptionLabel.bottom
text: "… " text: "… "
font: descriptionLabel.font font: descriptionLabel.font
color: descriptionLabel.color color: descriptionLabel.color
visible: descriptionLabel.truncated visible: descriptionLabel.truncated && descriptionLabel.text !== ""
} }
Cura.TertiaryButton Cura.TertiaryButton
{ {
id: readMoreButton id: readMoreButton
anchors.left: tripleDotLabel.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
height: fontMetrics.height //Height of a single line. height: fontMetrics.height //Height of a single line.
text: catalog.i18nc("@info", "Read more") text: catalog.i18nc("@info", "Read more")
iconSource: UM.Theme.getIcon("LinkExternal") iconSource: UM.Theme.getIcon("LinkExternal")
visible: descriptionLabel.truncated visible: descriptionLabel.truncated && descriptionLabel.text !== ""
enabled: visible enabled: visible
leftPadding: UM.Theme.getSize("default_margin").width leftPadding: UM.Theme.getSize("default_margin").width
rightPadding: UM.Theme.getSize("wide_margin").width rightPadding: UM.Theme.getSize("wide_margin").width
@ -261,24 +290,47 @@ Rectangle
} }
} }
Row
{
id: downloadCount
Layout.preferredWidth: parent.width
Layout.fillHeight: true
UM.RecolorImage
{
id: downloadsIcon
width: UM.Theme.getSize("card_tiny_icon").width
height: UM.Theme.getSize("card_tiny_icon").height
visible: packageData.installationStatus !== "bundled" //Don't show download count for packages that are bundled. It'll usually be 0.
source: UM.Theme.getIcon("Download")
color: UM.Theme.getColor("text")
}
Label
{
anchors.verticalCenter: downloadsIcon.verticalCenter
visible: packageData.installationStatus !== "bundled" //Don't show download count for packages that are bundled. It'll usually be 0.
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
text: packageData.downloadCount
}
}
// Author and action buttons. // Author and action buttons.
RowLayout RowLayout
{ {
id: authorAndActionButton id: authorAndActionButton
width: parent.width Layout.preferredWidth: parent.width
anchors Layout.preferredHeight: childrenRect.height
{
bottom: parent.bottom
left: packageItem.right
right: parent.right
margins: UM.Theme.getSize("default_margin").height
}
spacing: UM.Theme.getSize("narrow_margin").width spacing: UM.Theme.getSize("narrow_margin").width
Label Label
{ {
id: authorBy id: authorBy
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignTop
text: catalog.i18nc("@label", "By") text: catalog.i18nc("@label", "By")
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
@ -289,7 +341,7 @@ Rectangle
{ {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: authorBy.height Layout.preferredHeight: authorBy.height
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignTop
text: packageData.authorName text: packageData.authorName
textFont: UM.Theme.getFont("default_bold") textFont: UM.Theme.getFont("default_bold")
@ -304,26 +356,252 @@ Rectangle
Cura.SecondaryButton Cura.SecondaryButton
{ {
id: enableManageButton id: disableButton
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
text: packageData.enableManageButtonText text: catalog.i18nc("@button", "Disable")
visible: packageData.enableManageButtonVisible visible: false // not functional right now, also only when unfolding and required
} }
Cura.SecondaryButton Cura.SecondaryButton
{ {
id: installManageButton id: uninstallButton
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
text: packageData.installManageButtonText text: catalog.i18nc("@button", "Uninstall")
visible: packageData.installManageButtonVisible visible: false // not functional right now, also only when unfolding and required
} }
Cura.PrimaryButton Cura.PrimaryButton
{ {
id: updateManageButton id: installButton
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
text: catalog.i18nc("@button", "Update") // OR Download, if new! text: catalog.i18nc("@button", "Update") // OR Download, if new!
visible: packageData.updateManageButtonVisible visible: false // not functional right now, also only when unfolding and required
}
}
}
}
Column
{
id: extendedDescription
width: parent.width
padding: UM.Theme.getSize("default_margin").width
topPadding: 0
spacing: UM.Theme.getSize("default_margin").height
Label
{
width: parent.width - parent.padding * 2
text: catalog.i18nc("@header", "Description")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
{
width: parent.width - parent.padding * 2
text: packageData.formattedDescription
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
wrapMode: Text.Wrap
textFormat: Text.RichText
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
}
Column //Separate column to have no spacing between compatible printers.
{
id: compatiblePrinterColumn
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible printers")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Repeater
{
model: packageData.compatiblePrinters
Label
{
width: compatiblePrinterColumn.width
text: modelData
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Label
{
width: parent.width
visible: packageData.compatiblePrinters.length == 0
text: "(" + catalog.i18nc("@info", "No compatibility information") + ")"
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Column
{
id: compatibleSupportMaterialColumn
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible support materials")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Repeater
{
model: packageData.compatibleSupportMaterials
Label
{
width: compatibleSupportMaterialColumn.width
text: modelData
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Label
{
width: parent.width
visible: packageData.compatibleSupportMaterials.length == 0
text: "(" + catalog.i18nc("@info No materials", "None") + ")"
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Column
{
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible with Material Station")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
{
width: parent.width
text: packageData.isCompatibleMaterialStation ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Column
{
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Optimized for Air Manager")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
{
width: parent.width
text: packageData.isCompatibleAirManager ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Row
{
id: externalButtonRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: UM.Theme.getSize("narrow_margin").width
Cura.SecondaryButton
{
text: packageData.packageType === "plugin" ? catalog.i18nc("@button", "Visit plug-in website") : catalog.i18nc("@button", "Website")
iconSource: UM.Theme.getIcon("Globe")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.packageInfoUrl)
}
Cura.SecondaryButton
{
visible: packageData.packageType === "material"
text: catalog.i18nc("@button", "Buy spool")
iconSource: UM.Theme.getIcon("ShoppingCart")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.whereToBuy)
}
Cura.SecondaryButton
{
visible: packageData.packageType === "material"
text: catalog.i18nc("@button", "Safety datasheet")
iconSource: UM.Theme.getIcon("Warning")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.safetyDataSheet)
}
Cura.SecondaryButton
{
visible: packageData.packageType === "material"
text: catalog.i18nc("@button", "Technical datasheet")
iconSource: UM.Theme.getIcon("DocumentFilled")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.technicalDataSheet)
}
}
} }
} }

View file

@ -13,6 +13,12 @@ ListView
property string pageTitle property string pageTitle
property var selectedPackage property var selectedPackage
property string searchInBrowserUrl
property bool bannerVisible
property var bannerIcon
property string bannerText
property string bannerReadMoreUrl
property var onRemoveBanner
clip: true clip: true