From 919d4baf531cffb53f23b7878c3ff6721b38da38 Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Wed, 10 Jan 2024 14:31:44 +0100 Subject: [PATCH] fix(savedaddresses): making add/edit saved address popup's content scrollable when there's no enough space for the content --- .../popups/AddEditSavedAddressPopup.qml | 588 +++++++++--------- 1 file changed, 301 insertions(+), 287 deletions(-) diff --git a/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml b/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml index 47d629fa75..d6230ab2cc 100644 --- a/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml +++ b/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml @@ -11,7 +11,7 @@ import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Controls 0.1 import StatusQ.Controls.Validators 0.1 -import StatusQ.Popups.Dialog 0.1 +import StatusQ.Popups 0.1 import StatusQ.Components 0.1 import SortFilterProxyModel 0.2 @@ -22,21 +22,20 @@ import "../stores" import "../controls" import ".." -StatusDialog { +StatusModal { id: root property var allNetworks - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside width: 477 - topPadding: 24 // (16 + 8 for Name, until we add it to the StatusInput component) - bottomPadding: 28 - header: StatusDialogHeader { - headline.title: d.editMode? qsTr("Edit saved address") : qsTr("Add new saved address") - headline.subtitle: d.editMode? d.name : "" - actions.closeButton.onClicked: root.close() + headerSettings.title: d.editMode? qsTr("Edit saved address") : qsTr("Add new saved address") + headerSettings.subTitle: d.editMode? d.name : "" + + onClosed: { + root.close() } function initWithParams(params = {}) { @@ -76,6 +75,8 @@ StatusDialog { QtObject { id: d + readonly property int componentWidth: 445 + property bool editMode: false property bool addAddress: false property alias name: nameInput.text @@ -121,8 +122,8 @@ StatusDialog { function submit(event) { if (!d.valid - || !d.dirty - || event !== undefined && event.key !== Qt.Key_Return && event.key !== Qt.Key_Enter) + || !d.dirty + || event !== undefined && event.key !== Qt.Key_Return && event.key !== Qt.Key_Enter) return RootStore.createOrUpdateSavedAddress(d.name, d.address, d.ens, d.colorId, d.chainShortNames) @@ -137,300 +138,315 @@ StatusDialog { }); } - Column { - width: parent.width - height: childrenRect.height + StatusScrollView { + id: scrollView - spacing: Style.current.xlPadding + anchors.fill: parent + padding: 0 + contentWidth: availableWidth - StatusInput { - id: nameInput - implicitWidth: parent.width - charLimit: 24 - input.edit.objectName: "savedAddressNameInput" - placeholderText: qsTr("Address name") - label: qsTr("Name") - validators: [ - StatusMinLengthValidator { - minLength: 1 - errorMessage: qsTr("Please name your saved address") - }, - StatusValidator { - name: "check-for-no-emojis" - validate: (value) => { - return !Constants.regularExpressions.emoji.test(value) - } - errorMessage: Constants.errorMessages.emojRegExp - }, - StatusRegularExpressionValidator { - regularExpression: Constants.regularExpressions.alphanumericalExpanded1 - errorMessage: Constants.errorMessages.alphanumericalExpanded1RegExp - }, - StatusValidator { - name: "check-saved-address-existence" - validate: (value) => { - return !RootStore.savedAddressNameExists(value) - || d.editMode && d.storedName == value - } - errorMessage: qsTr("Name already in use") + Column { + width: scrollView.availableWidth + height: childrenRect.height + topPadding: 24 // (16 + 8 for Name, until we add it to the StatusInput component) + bottomPadding: 28 + + spacing: Style.current.xlPadding + + StatusInput { + id: nameInput + implicitWidth: d.componentWidth + anchors.horizontalCenter: parent.horizontalCenter + charLimit: 24 + input.edit.objectName: "savedAddressNameInput" + placeholderText: qsTr("Address name") + label: qsTr("Name") + validators: [ + StatusMinLengthValidator { + minLength: 1 + errorMessage: qsTr("Please name your saved address") + }, + StatusValidator { + name: "check-for-no-emojis" + validate: (value) => { + return !Constants.regularExpressions.emoji.test(value) + } + errorMessage: Constants.errorMessages.emojRegExp + }, + StatusRegularExpressionValidator { + regularExpression: Constants.regularExpressions.alphanumericalExpanded1 + errorMessage: Constants.errorMessages.alphanumericalExpanded1RegExp + }, + StatusValidator { + name: "check-saved-address-existence" + validate: (value) => { + return !RootStore.savedAddressNameExists(value) + || d.editMode && d.storedName == value + } + errorMessage: qsTr("Name already in use") + } + ] + input.clearable: true + input.rightPadding: 16 + + onKeyPressed: { + d.submit(event) } - ] - input.clearable: true - input.rightPadding: 16 - - onKeyPressed: { - d.submit(event) } - } - StatusInput { - id: addressInput - implicitWidth: parent.width - label: qsTr("Address") - objectName: "savedAddressAddressInput" - input.edit.objectName: "savedAddressAddressInputEdit" - placeholderText: qsTr("Ethereum address") - maximumHeight: 66 - input.implicitHeight: Math.min(Math.max(input.edit.contentHeight + topPadding + bottomPadding, minimumHeight), maximumHeight) // setting height instead does not work - enabled: !(d.editMode || d.addAddress) - validators: [ - StatusMinLengthValidator { - minLength: 1 - errorMessage: qsTr("Please enter an ethereum address") - }, - StatusValidator { - errorMessage: d.addressAlreadyAdded? qsTr("This address is already saved") : qsTr("Ethereum address invalid") - validate: function (value) { - if (value !== Constants.zeroAddress) { - if (Utils.isValidEns(value)) { - return true - } - if (Utils.isValidAddressWithChainPrefix(value)) { - if (d.editMode) { + StatusInput { + id: addressInput + implicitWidth: d.componentWidth + anchors.horizontalCenter: parent.horizontalCenter + label: qsTr("Address") + objectName: "savedAddressAddressInput" + input.edit.objectName: "savedAddressAddressInputEdit" + placeholderText: qsTr("Ethereum address") + maximumHeight: 66 + input.implicitHeight: Math.min(Math.max(input.edit.contentHeight + topPadding + bottomPadding, minimumHeight), maximumHeight) // setting height instead does not work + enabled: !(d.editMode || d.addAddress) + validators: [ + StatusMinLengthValidator { + minLength: 1 + errorMessage: qsTr("Please enter an ethereum address") + }, + StatusValidator { + errorMessage: d.addressAlreadyAdded? qsTr("This address is already saved") : qsTr("Ethereum address invalid") + validate: function (value) { + if (value !== Constants.zeroAddress) { + if (Utils.isValidEns(value)) { return true } - const prefixAndAddress = Utils.splitToChainPrefixAndAddress(value) - return d.checkIfAddressIsAlreadyAddded(prefixAndAddress.address) - } - } - - return false - } - } - ] - asyncValidators: [ - StatusAsyncValidator { - id: resolvingEnsName - name: "resolving-ens-name" - errorMessage: d.addressAlreadyAdded? qsTr("This address is already saved") : qsTr("Ethereum address invalid") - asyncOperation: (value) => { - if (!Utils.isValidEns(value)) { - resolvingEnsName.asyncComplete("not-ens") - return - } - d.resolvingEnsName = true - d.validateEnsAsync(value) + if (Utils.isValidAddressWithChainPrefix(value)) { + if (d.editMode) { + return true } - validate: (value) => { - if (d.editMode || value === "not-ens") { - return true - } - if (!!value) { - return d.checkIfAddressIsAlreadyAddded(value) - } - return false - } - - Connections { - target: mainModule - function onResolvedENS(resolvedPubKey: string, resolvedAddress: string, uuid: string) { - if (uuid !== d.uuid) { - return + const prefixAndAddress = Utils.splitToChainPrefixAndAddress(value) + return d.checkIfAddressIsAlreadyAddded(prefixAndAddress.address) + } } - d.resolvingEnsName = false - d.address = resolvedAddress - resolvingEnsName.asyncComplete(resolvedAddress) - } - } - } - ] - input.edit.textFormat: TextEdit.RichText - input.asset.name: addressInput.valid && !d.editMode ? "checkbox" : "" - input.asset.color: enabled ? Theme.palette.primaryColor1 : Theme.palette.baseColor1 - input.rightPadding: 16 - input.leftIcon: false - - multiline: true - - property string plainText: input.edit.getText(0, text.length) - - onTextChanged: { - if (skipTextUpdate || !d.initialized) - return - - d.addressAlreadyAdded = false - plainText = input.edit.getText(0, text.length) - - if (input.edit.previousText != plainText) { - let newText = plainText - const prefixAndAddress = Utils.splitToChainPrefixAndAddress(plainText) - - if (!Utils.isLikelyEnsName(plainText)) { - newText = WalletUtils.colorizedChainPrefix(prefixAndAddress.prefix) + - prefixAndAddress.address - } - - setRichText(newText) - - // Reset - if (plainText.length == 0) { - d.resetAddressValues() - return - } - - // Update root values - if (Utils.isLikelyEnsName(plainText)) { - d.resolvingEnsName = true - d.ens = plainText - d.address = Constants.zeroAddress - d.chainShortNames = "" - } - else { - d.resolvingEnsName = false - d.ens = "" - d.address = prefixAndAddress.address - d.chainShortNames = prefixAndAddress.prefix - - let prefixArrWithColumn = d.getPrefixArrayWithColumns(prefixAndAddress.prefix) - if (!prefixArrWithColumn) - prefixArrWithColumn = [] - - allNetworksModelCopy.setEnabledNetworks(prefixArrWithColumn) - } - } - } - - onKeyPressed: { - d.submit(event) - } - - property bool skipTextUpdate: false - - function setPlainText(newText) { - text = newText - } - - function setRichText(val) { - skipTextUpdate = true - input.edit.previousText = plainText - const curPos = input.cursorPosition - setPlainText(val) - input.cursorPosition = curPos - skipTextUpdate = false - } - - function getUnknownPrefixes(prefixes) { - let unknownPrefixes = prefixes.filter(e => { - for (let i = 0; i < allNetworksModelCopy.count; i++) { - if (e == allNetworksModelCopy.get(i).shortName) return false + } } - return true - }) + ] + asyncValidators: [ + StatusAsyncValidator { + id: resolvingEnsName + name: "resolving-ens-name" + errorMessage: d.addressAlreadyAdded? qsTr("This address is already saved") : qsTr("Ethereum address invalid") + asyncOperation: (value) => { + if (!Utils.isValidEns(value)) { + resolvingEnsName.asyncComplete("not-ens") + return + } + d.resolvingEnsName = true + d.validateEnsAsync(value) + } + validate: (value) => { + if (d.editMode || value === "not-ens") { + return true + } + if (!!value) { + return d.checkIfAddressIsAlreadyAddded(value) + } + return false + } - return unknownPrefixes - } + Connections { + target: mainModule + function onResolvedENS(resolvedPubKey: string, resolvedAddress: string, uuid: string) { + if (uuid !== d.uuid) { + return + } + d.resolvingEnsName = false + d.address = resolvedAddress + resolvingEnsName.asyncComplete(resolvedAddress) + } + } + } + ] - // Add all chain short names from model, while keeping existing - function syncChainPrefixWithModel(prefix, model) { - let prefixes = prefix.split(":").filter(Boolean) - let prefixStr = "" + input.edit.textFormat: TextEdit.RichText + input.asset.name: addressInput.valid && !d.editMode ? "checkbox" : "" + input.asset.color: enabled ? Theme.palette.primaryColor1 : Theme.palette.baseColor1 + input.rightPadding: 16 + input.leftIcon: false - // Keep unknown prefixes from user input, the rest must be taken - // from the model - for (let i = 0; i < model.count; i++) { - const item = model.get(i) - prefixStr += item.shortName + ":" - // Remove all added prefixes from initial array - prefixes = prefixes.filter(e => e !== item.shortName) - } + multiline: true - const unknownPrefixes = getUnknownPrefixes(prefixes) - if (unknownPrefixes.length > 0) { - prefixStr += unknownPrefixes.join(":") + ":" - } + property string plainText: input.edit.getText(0, text.length) - return prefixStr - } - } + onTextChanged: { + if (skipTextUpdate || !d.initialized) + return - StatusColorSelectorGrid { - id: colorSelection - objectName: "addSavedAddressColor" - width: parent.width - model: Theme.palette.customisationColorsArray - title.color: Theme.palette.directColor1 - title.font.pixelSize: Constants.addAccountPopup.labelFontSize1 - title.text: qsTr("Colour") - selectedColorIndex: -1 + d.addressAlreadyAdded = false + plainText = input.edit.getText(0, text.length) - onSelectedColorChanged: { - d.colorId = Utils.getIdForColor(selectedColor) - } - } + if (input.edit.previousText != plainText) { + let newText = plainText + const prefixAndAddress = Utils.splitToChainPrefixAndAddress(plainText) - StatusNetworkSelector { - id: networkSelector - objectName: "addSavedAddressNetworkSelector" - title: "Network preference" - implicitWidth: parent.width - enabled: addressInput.valid && !d.addressInputIsENS - defaultItemText: "Add networks" - defaultItemImageSource: "add" - rightButtonVisible: true + if (!Utils.isLikelyEnsName(plainText)) { + newText = WalletUtils.colorizedChainPrefix(prefixAndAddress.prefix) + + prefixAndAddress.address + } - property bool modelUpdateBlocked: false + setRichText(newText) - function blockModelUpdate(value) { - modelUpdateBlocked = value - } + // Reset + if (plainText.length == 0) { + d.resetAddressValues() + return + } - itemsModel: SortFilterProxyModel { - sourceModel: allNetworksModelCopy - filters: ValueFilter { - roleName: "isEnabled" - value: true - } + // Update root values + if (Utils.isLikelyEnsName(plainText)) { + d.resolvingEnsName = true + d.ens = plainText + d.address = Constants.zeroAddress + d.chainShortNames = "" + } + else { + d.resolvingEnsName = false + d.ens = "" + d.address = prefixAndAddress.address + d.chainShortNames = prefixAndAddress.prefix - onCountChanged: { - if (!networkSelector.modelUpdateBlocked && d.initialized) { - // Initially source model is empty, filter proxy is also empty, but does - // extra work and mistakenly overwrites d.chainShortNames property - if (sourceModel.count != 0) { - const prefixAndAddress = Utils.splitToChainPrefixAndAddress(addressInput.plainText) - const syncedPrefix = addressInput.syncChainPrefixWithModel(prefixAndAddress.prefix, this) - d.chainShortNames = syncedPrefix - addressInput.setPlainText(syncedPrefix + prefixAndAddress.address) + let prefixArrWithColumn = d.getPrefixArrayWithColumns(prefixAndAddress.prefix) + if (!prefixArrWithColumn) + prefixArrWithColumn = [] + + allNetworksModelCopy.setEnabledNetworks(prefixArrWithColumn) } } } + + onKeyPressed: { + d.submit(event) + } + + property bool skipTextUpdate: false + + function setPlainText(newText) { + text = newText + } + + function setRichText(val) { + skipTextUpdate = true + input.edit.previousText = plainText + const curPos = input.cursorPosition + setPlainText(val) + input.cursorPosition = curPos + skipTextUpdate = false + } + + function getUnknownPrefixes(prefixes) { + let unknownPrefixes = prefixes.filter(e => { + for (let i = 0; i < allNetworksModelCopy.count; i++) { + if (e == allNetworksModelCopy.get(i).shortName) + return false + } + return true + }) + + return unknownPrefixes + } + + // Add all chain short names from model, while keeping existing + function syncChainPrefixWithModel(prefix, model) { + let prefixes = prefix.split(":").filter(Boolean) + let prefixStr = "" + + // Keep unknown prefixes from user input, the rest must be taken + // from the model + for (let i = 0; i < model.count; i++) { + const item = model.get(i) + prefixStr += item.shortName + ":" + // Remove all added prefixes from initial array + prefixes = prefixes.filter(e => e !== item.shortName) + } + + const unknownPrefixes = getUnknownPrefixes(prefixes) + if (unknownPrefixes.length > 0) { + prefixStr += unknownPrefixes.join(":") + ":" + } + + return prefixStr + } } - addButton.highlighted: networkSelectPopup.visible - addButton.onClicked: { - networkSelectPopup.openAtPosition(addButton.x, networkSelector.y + addButton.height + Style.current.xlPadding) + StatusColorSelectorGrid { + id: colorSelection + objectName: "addSavedAddressColor" + width: d.componentWidth + anchors.horizontalCenter: parent.horizontalCenter + model: Theme.palette.customisationColorsArray + title.color: Theme.palette.directColor1 + title.font.pixelSize: Constants.addAccountPopup.labelFontSize1 + title.text: qsTr("Colour") + selectedColorIndex: -1 + + onSelectedColorChanged: { + d.colorId = Utils.getIdForColor(selectedColor) + } } - onItemClicked: function (item, index, mouse) { - // Append first item - if (index === 0 && defaultItem.visible) - networkSelectPopup.openAtPosition(defaultItem.x, networkSelector.y + defaultItem.height + Style.current.xlPadding) - } + StatusNetworkSelector { + id: networkSelector + objectName: "addSavedAddressNetworkSelector" + title: "Network preference" + implicitWidth: d.componentWidth + anchors.horizontalCenter: parent.horizontalCenter - onItemRightButtonClicked: function (item, index, mouse) { - item.modelRef.isEnabled = !item.modelRef.isEnabled - d.chainShortNamesDirty = true + enabled: addressInput.valid && !d.addressInputIsENS + defaultItemText: "Add networks" + defaultItemImageSource: "add" + rightButtonVisible: true + + property bool modelUpdateBlocked: false + + function blockModelUpdate(value) { + modelUpdateBlocked = value + } + + itemsModel: SortFilterProxyModel { + sourceModel: allNetworksModelCopy + filters: ValueFilter { + roleName: "isEnabled" + value: true + } + + onCountChanged: { + if (!networkSelector.modelUpdateBlocked && d.initialized) { + // Initially source model is empty, filter proxy is also empty, but does + // extra work and mistakenly overwrites d.chainShortNames property + if (sourceModel.count != 0) { + const prefixAndAddress = Utils.splitToChainPrefixAndAddress(addressInput.plainText) + const syncedPrefix = addressInput.syncChainPrefixWithModel(prefixAndAddress.prefix, this) + d.chainShortNames = syncedPrefix + addressInput.setPlainText(syncedPrefix + prefixAndAddress.address) + } + } + } + } + + addButton.highlighted: networkSelectPopup.visible + addButton.onClicked: { + networkSelectPopup.openAtPosition(addButton.x, networkSelector.y + addButton.height + Style.current.xlPadding) + } + + onItemClicked: function (item, index, mouse) { + // Append first item + if (index === 0 && defaultItem.visible) + networkSelectPopup.openAtPosition(defaultItem.x, networkSelector.y + defaultItem.height + Style.current.xlPadding) + } + + onItemRightButtonClicked: function (item, index, mouse) { + item.modelRef.isEnabled = !item.modelRef.isEnabled + d.chainShortNamesDirty = true + } } } } @@ -454,9 +470,9 @@ StatusDialog { } onToggleNetwork: (network) => { - network.isEnabled = !network.isEnabled - d.chainShortNamesDirty = true - } + network.isEnabled = !network.isEnabled + d.chainShortNamesDirty = true + } closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside @@ -470,19 +486,17 @@ StatusDialog { dim: false } - footer: StatusDialogFooter { - rightButtons: ObjectModel { - StatusButton { - text: d.editMode? qsTr("Save") : qsTr("Add address") - enabled: d.valid && d.dirty && !d.resolvingEnsName - loading: d.resolvingEnsName - onClicked: { - d.submit() - } - objectName: "addSavedAddress" + rightButtons: [ + StatusButton { + text: d.editMode? qsTr("Save") : qsTr("Add address") + enabled: d.valid && d.dirty && !d.resolvingEnsName + loading: d.resolvingEnsName + onClicked: { + d.submit() } + objectName: "addSavedAddress" } - } + ] CloneModel { id: allNetworksModelCopy