feat/tx-comps: Add RecipientSelector component

Based on the spec in https://www.notion.so/emizzle/Wallet-transaction-components-2003b78a8d0d41c4ab3d21eb2496fb20, this component handles user input for a recipient address, which can be sourced from manual address input, ENS name, contact selection, or another of the user's wallet accounts.
This commit is contained in:
emizzle 2020-08-06 17:25:53 +10:00 committed by Pascal Precht
parent 9466714d90
commit d07daac377
No known key found for this signature in database
GPG Key ID: 0EE28D8D6FD85D7D
14 changed files with 585 additions and 42 deletions

View File

@ -11,6 +11,8 @@ type
Identicon = UserRole + 4
IsContact = UserRole + 5
IsBlocked = UserRole + 6
Alias = UserRole + 7
EnsVerified = UserRole + 8
QtObject:
type ContactList* = ref object of QAbstractListModel
@ -48,6 +50,8 @@ QtObject:
of "pubKey": result = contact.id
of "isContact": result = $contact.isContact()
of "isBlocked": result = $contact.isBlocked()
of "alias": result = contact.alias
of "ensVerified": result = $contact.ensVerified
method data(self: ContactList, index: QModelIndex, role: int): QVariant =
if not index.isValid:
@ -62,6 +66,8 @@ QtObject:
of ContactRoles.PubKey: result = newQVariant(contact.id)
of ContactRoles.IsContact: result = newQVariant(contact.isContact())
of ContactRoles.IsBlocked: result = newQVariant(contact.isBlocked())
of ContactRoles.Alias: result = newQVariant(contact.alias)
of ContactRoles.EnsVerified: result = newQVariant(contact.ensVerified)
method roleNames(self: ContactList): Table[int, string] =
{
@ -70,7 +76,9 @@ QtObject:
ContactRoles.Identicon.int:"identicon",
ContactRoles.PubKey.int:"pubKey",
ContactRoles.IsContact.int:"isContact",
ContactRoles.IsBlocked.int:"isBlocked"
ContactRoles.IsBlocked.int:"isBlocked",
ContactRoles.Alias.int:"alias",
ContactRoles.EnsVerified.int:"ensVerified"
}.toTable
proc addContactToList*(self: ContactList, contact: Profile) =

View File

@ -19,7 +19,7 @@ Item {
return;
}
let result = walletModel.onSendTransaction(selectFromAccount.selectedAccount.address,
txtTo.text,
selectRecipient.selectedRecipient,
selectAsset.selectedAsset.address,
txtAmount.text,
txtPassword.text)
@ -35,6 +35,7 @@ Item {
}
function validate() {
selectRecipient.validate()
if (txtPassword.text === "") {
//% "You need to enter a password"
passwordValidationError = qsTrId("you-need-to-enter-a-password")
@ -45,16 +46,6 @@ Item {
passwordValidationError = ""
}
if (txtTo.text === "") {
//% "You need to enter a destination address"
toValidationError = qsTrId("you-need-to-enter-a-destination-address")
} else if (!Utils.isAddress(txtTo.text)) {
//% "This needs to be a valid address (starting with 0x)"
toValidationError = qsTrId("this-needs-to-be-a-valid-address-(starting-with-0x)")
} else {
toValidationError = ""
}
if (txtAmount.text === "") {
//% "You need to enter an amount"
amountValidationError = qsTrId("you-need-to-enter-an-amount")
@ -114,15 +105,15 @@ Item {
}
}
Input {
id: txtTo
//% "Recipient"
label: qsTrId("recipient")
//% "Send to"
placeholderText: qsTrId("send-to")
RecipientSelector {
id: selectRecipient
accounts: walletModel.accounts
contacts: profileModel.addedContacts
label: qsTr("Recipient")
anchors.top: selectFromAccount.bottom
anchors.topMargin: Style.current.padding
validationError: toValidationError
anchors.left: parent.left
anchors.right: parent.right
}
Input {
@ -131,7 +122,7 @@ Item {
label: qsTrId("password")
//% "Enter Password"
placeholderText: qsTrId("biometric-auth-login-ios-fallback-label")
anchors.top: txtTo.bottom
anchors.top: selectRecipient.bottom
anchors.topMargin: Style.current.padding
textField.echoMode: TextInput.Password
validationError: passwordValidationError

View File

@ -5,6 +5,7 @@ Theme {
property color white: "#FFFFFF"
property color white2: "#FCFCFC"
property color black: "#000000"
property color almostBlack: "#141414"
property color grey: "#EEF2F5"
property color lightBlue: "#ECEFFC"
property color cyan: "#00FFFF"
@ -18,10 +19,15 @@ Theme {
property color lightRed: "#FFEAEE"
property color green: "#4EBC60"
property color turquoise: "#007b7d"
property color tenPercentWhite: Qt.rgba(255, 255, 255, 0.1)
property color tenPercentBlue: Qt.rgba(67, 96, 223, 0.1)
property color background: "#141414"
property color background: almostBlack
property color border: "#252528"
property color borderSecondary: tenPercentWhite
property color borderTertiary: blue
property color textColor: white
property color textColorTertiary: blue
property color currentUserTextColor: white
property color secondaryBackground: "#23252F"
property color inputBackground: secondaryBackground
@ -29,6 +35,9 @@ Theme {
property color modalBackground: background
property color backgroundHover: "#252528"
property color secondaryText: darkGrey
property color secondaryHover: Qt.rgba(255, 255, 255, 0.1)
property color secondaryHover: tenPercentWhite
property color danger: red
property color primaryMenuItemHover: blue
property color primaryMenuItemTextHover: almostBlack
property color backgroundTertiary: tenPercentBlue
}

View File

@ -18,10 +18,15 @@ Theme {
property color lightRed: "#FFEAEE"
property color green: "#4EBC60"
property color turquoise: "#007b7d"
property color tenPercentBlack: Qt.rgba(0, 0, 0, 0.1)
property color tenPercentBlue: Qt.rgba(67, 96, 223, 0.1)
property color background: white
property color border: grey
property color borderSecondary: tenPercentBlack
property color borderTertiary: blue
property color textColor: black
property color textColorTertiary: blue
property color currentUserTextColor: white
property color secondaryBackground: lightBlue
property color inputBackground: grey
@ -29,6 +34,9 @@ Theme {
property color modalBackground: white2
property color backgroundHover: grey
property color secondaryText: darkGrey
property color secondaryHover: Qt.rgba(0, 0, 0, 0.1)
property color secondaryHover: tenPercentBlack
property color danger: red
property color primaryMenuItemHover: blue
property color primaryMenuItemTextHover: white
property color backgroundTertiary: tenPercentBlue
}

View File

@ -17,6 +17,7 @@ Item {
// NOTE: if this asset is not selected as a wallet token in the UI, then
// nothing will be displayed
property string showAssetBalance: ""
property int dropdownWidth: width
Repeater {
visible: showAssetBalance !== ""
@ -36,7 +37,7 @@ Item {
id: select
label: root.label
model: root.accounts
menuAlignment: Select.MenuAlignment.Left
menu.delegate: menuItem
menu.onOpened: {
selectedAccountDetails.visible = false
@ -44,6 +45,7 @@ Item {
menu.onClosed: {
selectedAccountDetails.visible = true
}
menu.width: dropdownWidth
selectedItemView: Item {
anchors.fill: parent
@ -85,18 +87,11 @@ Item {
StyledText {
id: textSelectedAddress
text: selectedAccount.address
text: selectedAccount.address + " • "
font.pixelSize: 12
elide: Text.ElideMiddle
height: 16
width: 85
color: Style.current.secondaryText
}
StyledText {
id: separator
text: "• "
font.pixelSize: 12
height: 16
width: 90
color: Style.current.secondaryText
}
StyledText {

View File

@ -0,0 +1,89 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtGraphicalEffects 1.13
import "../imports"
Item {
id: root
property string validationError: "Error"
property alias label: inpAddress.label
property string selectedAddress
height: inpAddress.height
function isValidAddress(inputValue) {
return /0x[a-fA-F0-9]{40}/.test(inputValue)
}
function isValidEns(inputValue) {
// TODO: Check if the entered value resolves to an address. Long operation.
// Issue tracked: https://github.com/status-im/nim-status-client/issues/718
const isEmail = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(inputValue)
const isDomain = /(?:(?:(?<thld>[\w\-]*)(?:\.))?(?<sld>[\w\-]*))\.(?<tld>[\w\-]*)/.test(inputValue)
return isEmail || isDomain
}
function validate(inputValue) {
if (!inputValue) inputValue = selectedAddress
let isValid =
(inputValue && inputValue.startsWith("0x") && isValidAddress(inputValue)) ||
isValidEns(inputValue)
inpAddress.validationError = isValid ? "" : validationError
return isValid
}
Input {
id: inpAddress
placeholderText: qsTr("eg. 0x1234 or ENS")
customHeight: 56
validationErrorAlignment: TextEdit.AlignRight
validationErrorTopMargin: 8
textField.onFocusChanged: {
let isValid = true
if (text !== "") {
isValid = root.validate(metrics.text)
}
if (!isValid) {
return
}
if (textField.focus) {
text = metrics.text
} else if (root.isValidAddress(metrics.text)) {
text = metrics.elidedText
}
}
textField.rightPadding: 73
onTextEdited: {
metrics.text = text
const isValid = root.validate(inputValue)
if (isValid) {
root.selectedAddress = inputValue
}
}
TextMetrics {
id: metrics
elideWidth: 97
elide: Text.ElideMiddle
}
TertiaryButton {
anchors.right: parent.right
anchors.rightMargin: 8
anchors.top: parent.top
anchors.topMargin: 14
label: qsTr("Paste")
onClicked: {
if (inpAddress.textField.canPaste) {
inpAddress.textField.paste()
}
}
}
}
}
/*##^##
Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -0,0 +1,67 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtGraphicalEffects 1.13
import "../imports"
Item {
id: root
property var sources: []
property string selectedSource: sources[0] || "Address"
property int dropdownWidth: 220
height: select.height
Select {
id: select
anchors.left: parent.left
anchors.right: parent.right
model: root.sources
selectedItemView: Item {
anchors.fill: parent
StyledText {
id: selectedTextField
text: root.selectedSource
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: 15
verticalAlignment: Text.AlignVCenter
height: 24
}
}
menu.width: dropdownWidth
menu.topPadding: 8
menu.bottomPadding: 8
menu.delegate: Component {
MenuItem {
id: menuItem
height: 40
width: parent.width
onTriggered: function () {
root.selectedSource = root.sources[index]
}
StyledText {
id: itemText
text: root.sources[index]
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: 15
height: 22
color: menuItem.highlighted ? Style.current.primaryMenuItemTextHover : Style.current.textColor
}
background: Rectangle {
color: menuItem.highlighted ? Style.current.primaryMenuItemHover : Style.current.transparent
}
}
}
}
}
/*##^##
Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -0,0 +1,186 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtGraphicalEffects 1.13
import "../imports"
Item {
id: root
property var contacts
property var selectedContact
height: select.height + (validationErrorText.visible ? validationErrorText.height : 0)
property int dropdownWidth: width
property string validationError: validationErrorText.text
property alias validationErrorAlignment: validationErrorText.horizontalAlignment
onContactsChanged: {
root.selectedContact = { name: qsTr("Select a contact") }
}
function validate() {
const isValid = root.selectedContact && root.selectedContact.address
if (!isValid) {
select.select.border.color = Style.current.danger
select.select.border.width = 1
validationErrorText.visible = true
} else {
select.select.border.color = Style.current.transparent
select.select.border.width = 0
validationErrorText.visible = false
}
}
Select {
id: select
label: ""
model: root.contacts
width: parent.width
menuAlignment: Select.MenuAlignment.Left
selectedItemView: Item {
anchors.fill: parent
Identicon {
id: iconImg
anchors.left: parent.left
anchors.leftMargin: 14
anchors.verticalCenter: parent.verticalCenter
height: 32
width: !!selectedContact.identicon ? 32 : 0
visible: !!selectedContact.identicon
source: selectedContact.identicon ? selectedContact.identicon : ""
}
StyledText {
id: selectedTextField
text: selectedContact.name
anchors.left: iconImg.right
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: 15
height: 22
verticalAlignment: Text.AlignVCenter
}
}
zeroItemsView: Item {
height: 186
StyledText {
anchors.fill: parent
text: qsTr("You don't have any contacts yet")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 13
height: 18
color: Style.current.secondaryText
}
}
menu.delegate: menuItem
menu.width: dropdownWidth
}
TextEdit {
id: validationErrorText
visible: false
text: qsTr("Please select a contact")
anchors.top: select.bottom
anchors.topMargin: 8
selectByMouse: true
readOnly: true
font.pixelSize: 12
height: 16
color: Style.current.danger
width: parent.width
horizontalAlignment: TextEdit.AlignRight
}
Component {
id: menuItem
MenuItem {
id: itemContainer
property bool isFirstItem: index === 0
property bool isLastItem: index === contacts.rowCount() - 1
width: parent.width
height: visible ? 72 : 0
Identicon {
id: iconImg
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 40
source: identicon
}
Column {
anchors.left: iconImg.right
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
Text {
text: name
font.pixelSize: 15
font.family: Style.current.fontBold.name
font.bold: true
color: Style.current.textColor
height: 22
}
Row {
StyledText {
text: alias + " • "
visible: ensVerified
color: Style.current.secondaryText
font.pixelSize: 12
height: 16
}
StyledText {
text: address
width: 85
elide: Text.ElideMiddle
color: Style.current.secondaryText
font.pixelSize: 12
height: 16
}
}
}
background: Rectangle {
color: itemContainer.highlighted ? Style.current.backgroundHover : Style.current.background
radius: Style.current.radius
// cover bottom left/right corners with square corners
Rectangle {
visible: !isLastItem
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: parent.radius
color: parent.color
}
// cover top left/right corners with square corners
Rectangle {
visible: !isFirstItem
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: parent.radius
color: parent.color
}
}
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: itemContainer
onClicked: {
root.selectedContact = { address, name, alias, isContact, identicon, ensVerified }
select.menu.close()
validate()
}
}
}
}
}
/*##^##
Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -9,7 +9,7 @@ Rectangle {
color: Style.current.background
radius: 50
border.width: 1
border.color: Style.current.border
border.color: Style.current.borderSecondary
Image {
width: parent.width

View File

@ -5,8 +5,11 @@ import "../imports"
Item {
property alias textField: inputValue
property string placeholderText: "My placeholder"
property string placeholderTextColor: Style.current.secondaryText
property alias text: inputValue.text
property string validationError: ""
property alias validationErrorAlignment: validationErrorText.horizontalAlignment
property int validationErrorTopMargin: 1
property string label: ""
readonly property bool hasLabel: label !== ""
property color bgColor: Style.current.inputBackground
@ -22,9 +25,10 @@ Item {
property int customHeight: 44
property int fontPixelSize: 15
signal editingFinished(string inputValue)
signal textEdited(string inputValue)
id: inputBox
height: inputRectangle.height + (hasLabel ? inputLabel.height + labelMargin : 0) + (!!validationError ? validationErrorText.height : 0)
height: inputRectangle.height + (hasLabel ? inputLabel.height + labelMargin : 0) + (!!validationError ? (validationErrorText.height + validationErrorTopMargin) : 0)
anchors.right: parent.right
anchors.left: parent.left
@ -56,6 +60,7 @@ Item {
id: inputValue
visible: !inputBox.isTextArea && !inputBox.isSelect
placeholderText: inputBox.placeholderText
placeholderTextColor: inputBox.placeholderTextColor
text: inputBox.text
anchors.top: parent.top
anchors.topMargin: 0
@ -72,6 +77,7 @@ Item {
color: Style.current.transparent
}
onEditingFinished: inputBox.editingFinished(inputBox.text)
onTextEdited: inputBox.textEdited(inputBox.text)
}
SVGImage {
@ -91,12 +97,13 @@ Item {
id: validationErrorText
text: validationError
anchors.top: inputRectangle.bottom
anchors.topMargin: 1
anchors.topMargin: validationErrorTopMargin
selectByMouse: true
readOnly: true
font.pixelSize: 12
color: Style.current.red
height: 16
color: Style.current.danger
width: parent.width
}
}

View File

@ -82,8 +82,8 @@ Menu {
anchors.fill: parent
source: parent
color: popupMenuItem.highlighted ?
Style.current.white :
(popupMenuItem.action.icon.color != "#00000000" ? popupMenuItem.action.icon.color : Style.current.blue)
Style.current.primaryMenuItemTextHover :
(popupMenuItem.action.icon.color != "#00000000" ? popupMenuItem.action.icon.color : Style.current.primaryMenuItemHover)
}
}
@ -92,7 +92,7 @@ Menu {
anchors.leftMargin: popupMenu.paddingSize
text: popupMenuItem.text
font: popupMenuItem.font
color: popupMenuItem.highlighted ? Style.current.white : popupMenuItem.textColor
color: popupMenuItem.highlighted ? Style.current.primaryMenuItemTextHover : popupMenuItem.textColor
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
opacity: enabled ? 1.0 : 0.3

View File

@ -0,0 +1,121 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtGraphicalEffects 1.13
import "../imports"
Item {
id: root
property var accounts
property var contacts
property int inputWidth: 272
property int sourceSelectWidth: 136
property string label: qsTr("Recipient")
property string selectedRecipient: ""
height: inpAddress.height + txtLabel.height
function validate() {
if (selAddressSource.selectedSource === "Address") {
inpAddress.validate()
} else if (selAddressSource.selectedSource === "Contact") {
selContact.validate()
}
}
Text {
id: txtLabel
visible: label !== ""
text: root.label
font.pixelSize: 13
font.family: Style.current.fontBold.name
color: Style.current.textColor
height: 18
}
RowLayout {
anchors.top: txtLabel.bottom
anchors.topMargin: 7
anchors.left: parent.left
anchors.right: parent.right
spacing: 8
AddressInput {
id: inpAddress
width: root.inputWidth
label: ""
Layout.preferredWidth: root.inputWidth
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
validationError: qsTr("Invalid ethereum address")
onSelectedAddressChanged: {
root.selectedRecipient = selectedAddress
}
}
ContactSelector {
id: selContact
contacts: root.contacts
visible: false
width: root.inputWidth
dropdownWidth: parent.width
Layout.preferredWidth: root.inputWidth
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
onSelectedContactChanged: {
if(selectedContact && selectedContact.address) {
root.selectedRecipient = selectedContact.address
}
}
}
AccountSelector {
id: selAccount
accounts: root.accounts
visible: false
width: root.inputWidth
dropdownWidth: parent.width
label: ""
Layout.preferredWidth: root.inputWidth
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
onSelectedAccountChanged: {
root.selectedRecipient = selectedAccount.address
}
}
AddressSourceSelector {
id: selAddressSource
sources: ["Address", "Contact", "My account"]
width: sourceSelectWidth
Layout.preferredWidth: root.sourceSelectWidth
Layout.alignment: Qt.AlignTop
onSelectedSourceChanged: {
switch (selectedSource) {
case "Address":
inpAddress.visible = true
selContact.visible = selAccount.visible = false
root.height = Qt.binding(function() { return inpAddress.height + txtLabel.height })
break;
case "Contact":
selContact.visible = true
inpAddress.visible = selAccount.visible = false
root.height = Qt.binding(function() { return selContact.height + txtLabel.height })
break;
case "My account":
selAccount.visible = true
inpAddress.visible = selContact.visible = false
root.height = Qt.binding(function() { return selAccount.height + txtLabel.height })
break;
}
}
}
}
}
/*##^##
Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -5,6 +5,10 @@ import QtGraphicalEffects 1.13
import "../imports"
Item {
enum MenuAlignment {
Left,
Right
}
property string label: ""
readonly property bool hasLabel: label !== ""
property color bgColor: Style.current.inputBackground
@ -15,6 +19,8 @@ Item {
property alias selectedItemView: selectedItemContainer.children
property int caretRightMargin: Style.current.padding
property alias select: inputRectangle
property int menuAlignment: Select.MenuAlignment.Right
property Item zeroItemsView: Item {}
anchors.left: parent.left
anchors.right: parent.right
@ -105,7 +111,24 @@ Item {
Repeater {
id: menuItems
model: root.model
property int zeroItemsViewHeight
delegate: selectMenu.delegate
onItemAdded: {
root.zeroItemsView.visible = false
root.zeroItemsView.height = 0
}
onItemRemoved: {
if (count === 0) {
root.zeroItemsView.visible = true
root.zeroItemsView.height = zeroItemsViewHeight
}
}
Component.onCompleted: {
zeroItemsViewHeight = root.zeroItemsView.height
root.zeroItemsView.visible = count === 0
root.zeroItemsView.height = count !== 0 ? 0 : root.zeroItemsView.height
selectMenu.insertItem(0, root.zeroItemsView)
}
}
}
MouseArea {
@ -123,7 +146,8 @@ Item {
if (selectMenu.opened) {
selectMenu.close()
} else {
const offset = inputRectangle.width - selectMenu.width
const rightOffset = inputRectangle.width - selectMenu.width
const offset = root.menuAlignment === Select.MenuAlignment.Left ? 0 : rightOffset
selectMenu.popup(inputRectangle.x + offset, inputRectangle.y + inputRectangle.height + 8)
}
}

View File

@ -0,0 +1,38 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import "../imports"
Button {
id: root
property alias label: txtBtnLabel.text
width: txtBtnLabel.width + 2 * 12
height: txtBtnLabel.height + 2 * 6
background: Rectangle {
color: Style.current.backgroundTertiary
radius: 6
anchors.fill: parent
border.color: Style.current.borderTertiary
border.width: 1
}
StyledText {
id: txtBtnLabel
color: Style.current.textColorTertiary
font.pixelSize: 12
height: 16
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Paste")
}
MouseArea {
id: mouse
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: {
parent.clicked()
}
}
}