fix(savedaddresses): making add/edit saved address popup's content scrollable when there's no enough space for the content

This commit is contained in:
Sale Djenic 2024-01-10 14:31:44 +01:00 committed by saledjenic
parent b515f536d1
commit 919d4baf53
1 changed files with 301 additions and 287 deletions

View File

@ -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